1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 18:08:21 +02:00

Compare commits

..

17 Commits

46 changed files with 1826 additions and 2389 deletions

View File

@ -1,19 +0,0 @@
# 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'),
),
]

View File

@ -234,7 +234,7 @@ class Guest(models.Model):
""" """
activity = models.ForeignKey( activity = models.ForeignKey(
Activity, Activity,
on_delete=models.CASCADE, on_delete=models.PROTECT,
related_name='+', related_name='+',
) )

View File

@ -1,6 +1,8 @@
# 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
import datetime
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -77,6 +79,9 @@ def get_row_class(record):
c += " table-info" c += " table-info"
elif record.note.balance < 0: elif record.note.balance < 0:
c += " table-danger" c += " table-danger"
# MODE VIEUXCON=ON
if (datetime.datetime.utcnow().timestamp() - record.note.created_at.timestamp()) > 3600 * 24 * 365 * 2.5:
c += " font-weight-bold underline"
return c return c

View File

@ -95,23 +95,5 @@ 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 %}

View File

@ -70,10 +70,7 @@ 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 not activity.valid and ".delete_"|has_perm:activity %} {% if activity.activity_type.can_invite and not activity_started %}
<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 %}

View File

@ -15,5 +15,4 @@ 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'),
] ]

View File

@ -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, JsonResponse from django.http import HttpResponse
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,34 +153,6 @@ 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`
@ -292,13 +264,22 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
balance=F("note__balance")) balance=F("note__balance"))
# Keep only users that have a note # Keep only users that have a note
note_qs = note_qs.filter(note__noteuser__isnull=False) note_qs = note_qs.filter(note__noteuser__isnull=False).exclude(note__inactivity_reason='forced')
if activity.activity_type.name != "Pot Vieux":
# Keep only members
note_qs = note_qs.filter(
note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(),
note__noteuser__user__memberships__date_end__gte=timezone.now(),
)
# Keep only valid members # Keep only valid members
note_qs = note_qs.filter( # note_qs = note_qs.filter(
note__noteuser__user__memberships__club=activity.attendees_club, # note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(), # note__noteuser__user__memberships__date_start__lte=timezone.now(),
note__noteuser__user__memberships__date_end__gte=timezone.now()).exclude(note__inactivity_reason='forced') # note__noteuser__user__memberships__date_end__gte=timezone.now(),
# )
# Filter with permission backend # Filter with permission backend
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")) note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))

View File

@ -2,58 +2,36 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin
from polymorphic.admin import PolymorphicChildModelAdmin, PolymorphicParentModelAdmin from django.db import transaction
from note_kfet.admin import admin_site from note_kfet.admin import admin_site
from .models import Allergen, Food, BasicFood, TransformedFood, QRCode from .models import Allergen, BasicFood, QRCode, TransformedFood
@admin.register(Allergen, site=admin_site)
class AllergenAdmin(admin.ModelAdmin):
"""
Admin customisation for Allergen
"""
ordering = ['name']
@admin.register(Food, site=admin_site)
class FoodAdmin(PolymorphicParentModelAdmin):
"""
Admin customisation for Food
"""
child_models = (Food, BasicFood, TransformedFood)
list_display = ('name', 'expiry_date', 'owner', 'is_ready')
list_filter = ('is_ready', 'end_of_life')
search_fields = ['name']
ordering = ['expiry_date', 'name']
@admin.register(BasicFood, site=admin_site)
class BasicFood(PolymorphicChildModelAdmin):
"""
Admin customisation for BasicFood
"""
list_display = ('name', 'expiry_date', 'date_type', 'owner', 'is_ready')
list_filter = ('is_ready', 'date_type', 'end_of_life')
search_fields = ['name']
ordering = ['expiry_date', 'name']
@admin.register(TransformedFood, site=admin_site)
class TransformedFood(PolymorphicChildModelAdmin):
"""
Admin customisation for TransformedFood
"""
list_display = ('name', 'expiry_date', 'shelf_life', 'owner', 'is_ready')
list_filter = ('is_ready', 'end_of_life', 'shelf_life')
search_fields = ['name']
ordering = ['expiry_date', 'name']
@admin.register(QRCode, site=admin_site) @admin.register(QRCode, site=admin_site)
class QRCodeAdmin(admin.ModelAdmin): class QRCodeAdmin(admin.ModelAdmin):
""" pass
Admin customisation for QRCode
"""
list_diplay = ('qr_code_number', 'food_container') @admin.register(BasicFood, site=admin_site)
search_fields = ['food_container__name'] class BasicFoodAdmin(admin.ModelAdmin):
@transaction.atomic
def save_related(self, *args, **kwargs):
ans = super().save_related(*args, **kwargs)
args[1].instance.update()
return ans
@admin.register(TransformedFood, site=admin_site)
class TransformedFoodAdmin(admin.ModelAdmin):
exclude = ["allergens", "expiry_date"]
@transaction.atomic
def save_related(self, request, form, *args, **kwargs):
super().save_related(request, form, *args, **kwargs)
form.instance.update()
@admin.register(Allergen, site=admin_site)
class AllergenAdmin(admin.ModelAdmin):
pass

View File

@ -3,7 +3,7 @@
from rest_framework import serializers from rest_framework import serializers
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode from ..models import Allergen, BasicFood, QRCode, TransformedFood
class AllergenSerializer(serializers.ModelSerializer): class AllergenSerializer(serializers.ModelSerializer):
@ -11,46 +11,40 @@ class AllergenSerializer(serializers.ModelSerializer):
REST API Serializer for Allergen. REST API Serializer for Allergen.
The djangorestframework plugin will analyse the model `Allergen` and parse all fields in the API. The djangorestframework plugin will analyse the model `Allergen` and parse all fields in the API.
""" """
class Meta: class Meta:
model = Allergen model = Allergen
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.
The djangorestframework plugin will analyse the model `BasicFood` and parse all fields in the API. The djangorestframework plugin will analyse the model `BasicFood` and parse all fields in the API.
""" """
class Meta: class Meta:
model = BasicFood model = BasicFood
fields = '__all__' fields = '__all__'
class TransformedFoodSerializer(serializers.ModelSerializer):
"""
REST API Serializer for TransformedFood.
The djangorestframework plugin will analyse the model `TransformedFood` and parse all fields in the API.
"""
class Meta:
model = TransformedFood
fields = '__all__'
class QRCodeSerializer(serializers.ModelSerializer): class QRCodeSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for QRCode. REST API Serializer for QRCode.
The djangorestframework plugin will analyse the model `QRCode` and parse all fields in the API. The djangorestframework plugin will analyse the model `QRCode` and parse all fields in the API.
""" """
class Meta: class Meta:
model = QRCode model = QRCode
fields = '__all__' fields = '__all__'
class TransformedFoodSerializer(serializers.ModelSerializer):
"""
REST API Serializer for TransformedFood.
The djangorestframework plugin will analyse the model `TransformedFood` and parse all fields in the API.
"""
class Meta:
model = TransformedFood
fields = '__all__'

View File

@ -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, FoodViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet from .views import AllergenViewSet, BasicFoodViewSet, QRCodeViewSet, TransformedFoodViewSet
def register_food_urls(router, path): def register_food_urls(router, path):
@ -9,7 +9,6 @@ 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 + '/basic_food', BasicFoodViewSet)
router.register(path + '/basicfood', BasicFoodViewSet)
router.register(path + '/transformedfood', TransformedFoodViewSet)
router.register(path + '/qrcode', QRCodeViewSet) router.register(path + '/qrcode', QRCodeViewSet)
router.register(path + '/transformed_food', TransformedFoodViewSet)

View File

@ -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, FoodSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer from .serializers import AllergenSerializer, BasicFoodSerializer, QRCodeSerializer, TransformedFoodSerializer
from ..models import Allergen, Food, BasicFood, TransformedFood, QRCode from ..models import Allergen, BasicFood, QRCode, TransformedFood
class AllergenViewSet(ReadProtectedModelViewSet): class AllergenViewSet(ReadProtectedModelViewSet):
@ -22,24 +22,11 @@ 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.
The djangorestframework plugin will get all `BasicFood` objects, serialize it to JSON with the given serializer, The djangorestframework plugin will get all `BasicFood` objects, serialize it to JSON with the given serializer,
then render it on /api/food/basicfood/ then render it on /api/food/basic_food/
""" """
queryset = BasicFood.objects.order_by('id') queryset = BasicFood.objects.order_by('id')
serializer_class = BasicFoodSerializer serializer_class = BasicFoodSerializer
@ -48,19 +35,6 @@ class BasicFoodViewSet(ReadProtectedModelViewSet):
search_fields = ['$name', ] search_fields = ['$name', ]
class TransformedFoodViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `TransformedFood` objects, serialize it to JSON with the given serializer,
then render it on /api/food/transformedfood/
"""
queryset = TransformedFood.objects.order_by('id')
serializer_class = TransformedFoodSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class QRCodeViewSet(ReadProtectedModelViewSet): class QRCodeViewSet(ReadProtectedModelViewSet):
""" """
REST API View set. REST API View set.
@ -72,3 +46,16 @@ class QRCodeViewSet(ReadProtectedModelViewSet):
filter_backends = [DjangoFilterBackend, SearchFilter] filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['qr_code_number', ] filterset_fields = ['qr_code_number', ]
search_fields = ['$qr_code_number', ] search_fields = ['$qr_code_number', ]
class TransformedFoodViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `TransformedFood` objects, serialize it to JSON with the given serializer,
then render it on /api/food/transformed_food/
"""
queryset = TransformedFood.objects.order_by('id')
serializer_class = TransformedFoodSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]

View File

@ -1,100 +0,0 @@
[
{
"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"
}
}
]

View File

@ -3,41 +3,42 @@
from random import shuffle from random import shuffle
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from django import forms from django import forms
from django.forms.widgets import NumberInput
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from member.models import Club from member.models import Club
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from note_kfet.inputs import Autocomplete 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 Food, BasicFood, TransformedFood, QRCode from .models import BasicFood, QRCode, TransformedFood
class QRCodeForms(forms.ModelForm): class AddIngredientForms(forms.ModelForm):
""" """
Form for create QRCode for container Form for add an ingredient
""" """
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['ingredient'].queryset = self.fields['ingredient'].queryset.filter(
end_of_life__isnull=True,
polymorphic_ctype__model='transformedfood', polymorphic_ctype__model='transformedfood',
).filter(PermissionBackend.filter_queryset( is_ready=False,
get_current_request(), is_active=True,
TransformedFood, was_eaten=False,
"view", )
)) # Caution, the logic is inverted here, we flip the logic on saving in AddIngredientView
self.fields['is_active'].initial = True
self.fields['is_active'].label = _("Fully used")
class Meta: class Meta:
model = QRCode model = TransformedFood
fields = ('food_container',) fields = ('ingredient', 'is_active')
class BasicFoodForms(forms.ModelForm): class BasicFoodForms(forms.ModelForm):
""" """
Form for add basicfood Form for add non-transformed food
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -50,138 +51,64 @@ class BasicFoodForms(forms.ModelForm):
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all()) clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs) shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..." self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
self.fields['order'].widget.attrs["placeholder"] = _("Specific order given to GCKs")
class Meta: class Meta:
model = BasicFood model = BasicFood
fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens', 'order',) fields = ('name', 'owner', 'date_type', 'expiry_date', 'is_active', 'was_eaten', 'allergens',)
widgets = { widgets = {
"owner": Autocomplete( "owner": Autocomplete(
model=Club, model=Club,
attrs={"api_url": "/api/members/club/"}, attrs={"api_url": "/api/members/club/"},
), ),
"expiry_date": DateTimePickerInput(), 'expiry_date': DateTimePickerInput(),
} }
class QRCodeForms(forms.ModelForm):
"""
Form for create QRCode
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['food_container'].queryset = self.fields['food_container'].queryset.filter(
is_active=True,
was_eaten=False,
polymorphic_ctype__model='transformedfood',
)
class Meta:
model = QRCode
fields = ('food_container',)
class TransformedFoodForms(forms.ModelForm): class TransformedFoodForms(forms.ModelForm):
""" """
Form for add transformedfood Form for add transformed food
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['name'].required = True self.fields['name'].required = True
self.fields['owner'].required = True self.fields['owner'].required = True
self.fields['creation_date'].required = True
self.fields['creation_date'].initial = timezone.now
self.fields['is_active'].initial = True
self.fields['is_ready'].initial = False
self.fields['was_eaten'].initial = False
# Some example # Some example
self.fields['name'].widget.attrs.update({"placeholder": _("Lasagna")}) self.fields['name'].widget.attrs.update({"placeholder": _("Lasagna")})
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all()) clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs) shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..." self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
self.fields['order'].widget.attrs["placeholder"] = _("Specific order given to GCKs")
class Meta: class Meta:
model = TransformedFood model = TransformedFood
fields = ('name', 'owner', 'order',) fields = ('name', 'creation_date', 'owner', 'is_active', 'is_ready', 'was_eaten', 'shelf_life')
widgets = { widgets = {
"owner": Autocomplete( "owner": Autocomplete(
model=Club, model=Club,
attrs={"api_url": "/api/members/club/"}, attrs={"api_url": "/api/members/club/"},
), ),
'creation_date': DateTimePickerInput(),
} }
class BasicFoodUpdateForms(forms.ModelForm):
"""
Form for update basicfood object
"""
class Meta:
model = BasicFood
fields = ('name', 'owner', 'date_type', 'expiry_date', 'end_of_life', 'is_ready', 'order', 'allergens')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
"expiry_date": DateTimePickerInput(),
}
class TransformedFoodUpdateForms(forms.ModelForm):
"""
Form for update transformedfood object
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['shelf_life'].label = _('Shelf life (in hours)')
class Meta:
model = TransformedFood
fields = ('name', 'owner', 'end_of_life', 'is_ready', 'order', 'shelf_life')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
"expiry_date": DateTimePickerInput(),
"shelf_life": NumberInput(),
}
class AddIngredientForms(forms.ModelForm):
"""
Form for add an ingredient
"""
fully_used = forms.BooleanField()
fully_used.initial = True
fully_used.required = False
fully_used.label = _("Fully used")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# TODO find a better way to get pk (be not url scheme dependant)
pk = get_current_request().path.split('/')[-1]
self.fields['ingredients'].queryset = self.fields['ingredients'].queryset.filter(
polymorphic_ctype__model="transformedfood",
is_ready=False,
end_of_life='',
).filter(PermissionBackend.filter_queryset(get_current_request(), TransformedFood, "change")).exclude(pk=pk)
class Meta:
model = TransformedFood
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,
)

View File

@ -1,199 +1,84 @@
# Generated by Django 4.2.20 on 2025-04-17 21:43 # Generated by Django 2.2.28 on 2024-07-05 08:57
import datetime
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import django.utils.timezone import django.utils.timezone
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
("contenttypes", "0002_remove_content_type_name"), ('contenttypes', '0002_remove_content_type_name'),
("member", "0013_auto_20240801_1436"), ('member', '0011_profile_vss_charter_read'),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name="Allergen", name='Allergen',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=255, verbose_name='name')),
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, verbose_name="name")),
], ],
options={ options={
"verbose_name": "Allergen", 'verbose_name': 'Allergen',
"verbose_name_plural": "Allergens", 'verbose_name_plural': 'Allergens',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name="Food", name='Food',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('name', models.CharField(max_length=255, verbose_name='name')),
models.AutoField( ('expiry_date', models.DateTimeField(verbose_name='expiry date')),
auto_created=True, ('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
primary_key=True, ('is_ready', models.BooleanField(default=False, verbose_name='is ready')),
serialize=False, ('allergens', models.ManyToManyField(blank=True, to='food.Allergen', verbose_name='allergen')),
verbose_name="ID", ('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')),
), ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_food.food_set+', to='contenttypes.ContentType')),
),
("name", models.CharField(max_length=255, verbose_name="name")),
("expiry_date", models.DateTimeField(verbose_name="expiry date")),
(
"end_of_life",
models.CharField(max_length=255, verbose_name="end of life"),
),
(
"is_ready",
models.BooleanField(max_length=255, verbose_name="is ready"),
),
("order", models.CharField(max_length=255, verbose_name="order")),
(
"allergens",
models.ManyToManyField(
blank=True, to="food.allergen", verbose_name="allergens"
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT,
related_name="+",
to="member.club",
verbose_name="owner",
),
),
(
"polymorphic_ctype",
models.ForeignKey(
editable=False,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="polymorphic_%(app_label)s.%(class)s_set+",
to="contenttypes.contenttype",
),
),
], ],
options={ options={
"verbose_name": "Food", 'verbose_name': 'foods',
"verbose_name_plural": "Foods",
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name="BasicFood", name='BasicFood',
fields=[ fields=[
( ('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')),
"food_ptr", ('date_type', models.CharField(choices=[('DLC', 'DLC'), ('DDM', 'DDM')], max_length=255)),
models.OneToOneField( ('arrival_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='arrival date')),
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="food.food",
),
),
(
"arrival_date",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="arrival date"
),
),
(
"date_type",
models.CharField(
choices=[("DLC", "DLC"), ("DDM", "DDM")], max_length=255
),
),
], ],
options={ options={
"verbose_name": "Basic food", 'verbose_name': 'Basic food',
"verbose_name_plural": "Basic foods", 'verbose_name_plural': 'Basic foods',
}, },
bases=("food.food",), bases=('food.food',),
), ),
migrations.CreateModel( migrations.CreateModel(
name="QRCode", name='QRCode',
fields=[ fields=[
( ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
"id", ('qr_code_number', models.PositiveIntegerField(unique=True, verbose_name='QR-code number')),
models.AutoField( ('food_container', models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.Food', verbose_name='food container')),
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"qr_code_number",
models.PositiveIntegerField(
unique=True, verbose_name="qr code number"
),
),
(
"food_container",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="QR_code",
to="food.food",
verbose_name="food container",
),
),
], ],
options={ options={
"verbose_name": "QR-code", 'verbose_name': 'QR-code',
"verbose_name_plural": "QR-codes", 'verbose_name_plural': 'QR-codes',
}, },
), ),
migrations.CreateModel( migrations.CreateModel(
name="TransformedFood", name='TransformedFood',
fields=[ fields=[
( ('food_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='food.Food')),
"food_ptr", ('creation_date', models.DateTimeField(verbose_name='creation date')),
models.OneToOneField( ('is_active', models.BooleanField(default=True, verbose_name='is active')),
auto_created=True, ('ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.Food', verbose_name='transformed ingredient')),
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="food.food",
),
),
(
"creation_date",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="creation date"
),
),
(
"shelf_life",
models.DurationField(
default=datetime.timedelta(days=3), verbose_name="shelf life"
),
),
(
"ingredients",
models.ManyToManyField(
blank=True,
related_name="transformed_ingredient_inv",
to="food.food",
verbose_name="transformed ingredient",
),
),
], ],
options={ options={
"verbose_name": "Transformed food", 'verbose_name': 'Transformed food',
"verbose_name_plural": "Transformed foods", 'verbose_name_plural': 'Transformed foods',
}, },
bases=("food.food",), bases=('food.food',),
), ),
] ]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.28 on 2024-07-06 20:37
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('food', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='transformedfood',
name='shelf_life',
field=models.DurationField(default=datetime.timedelta(days=3), verbose_name='shelf life'),
),
]

View File

@ -0,0 +1,62 @@
from django.db import migrations
def create_14_mandatory_allergens(apps, schema_editor):
"""
There are 14 mandatory allergens, they are pre-injected
"""
Allergen = apps.get_model("food", "allergen")
Allergen.objects.get_or_create(
name="Gluten",
)
Allergen.objects.get_or_create(
name="Fruits à coques",
)
Allergen.objects.get_or_create(
name="Crustacés",
)
Allergen.objects.get_or_create(
name="Céléri",
)
Allergen.objects.get_or_create(
name="Oeufs",
)
Allergen.objects.get_or_create(
name="Moutarde",
)
Allergen.objects.get_or_create(
name="Poissons",
)
Allergen.objects.get_or_create(
name="Soja",
)
Allergen.objects.get_or_create(
name="Lait",
)
Allergen.objects.get_or_create(
name="Sulfites",
)
Allergen.objects.get_or_create(
name="Sésame",
)
Allergen.objects.get_or_create(
name="Lupin",
)
Allergen.objects.get_or_create(
name="Arachides",
)
Allergen.objects.get_or_create(
name="Mollusques",
)
class Migration(migrations.Migration):
dependencies = [
('food', '0002_transformedfood_shelf_life'),
]
operations = [
migrations.RunPython(create_14_mandatory_allergens),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.28 on 2024-08-13 21:58
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('food', '0003_create_14_allergens_mandatory'),
]
operations = [
migrations.RemoveField(
model_name='transformedfood',
name='is_active',
),
migrations.AddField(
model_name='food',
name='is_active',
field=models.BooleanField(default=True, verbose_name='is active'),
),
migrations.AlterField(
model_name='qrcode',
name='food_container',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='QR_code', to='food.Food', verbose_name='food container'),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 4.2.15 on 2024-08-28 08:00
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('food', '0004_auto_20240813_2358'),
]
operations = [
migrations.AlterField(
model_name='food',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
]

View File

@ -6,13 +6,37 @@ from datetime import timedelta
from django.db import models, transaction from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel
from member.models import Club from member.models import Club
from polymorphic.models import PolymorphicModel
class QRCode(models.Model):
"""
An QRCode model
"""
qr_code_number = models.PositiveIntegerField(
verbose_name=_("QR-code number"),
unique=True,
)
food_container = models.ForeignKey(
'Food',
on_delete=models.CASCADE,
related_name='QR_code',
verbose_name=_('food container'),
)
class Meta:
verbose_name = _("QR-code")
verbose_name_plural = _("QR-codes")
def __str__(self):
return _("QR-code number {qr_code_number}").format(qr_code_number=self.qr_code_number)
class Allergen(models.Model): class Allergen(models.Model):
""" """
Allergen and alimentary restrictions A list of allergen and alimentary restrictions
""" """
name = models.CharField( name = models.CharField(
verbose_name=_('name'), verbose_name=_('name'),
@ -20,19 +44,16 @@ class Allergen(models.Model):
) )
class Meta: class Meta:
verbose_name = _("Allergen") verbose_name = _('Allergen')
verbose_name_plural = _("Allergens") verbose_name_plural = _('Allergens')
def __str__(self): def __str__(self):
return self.name return self.name
class Food(PolymorphicModel): class Food(PolymorphicModel):
"""
Describe any type of food
"""
name = models.CharField( name = models.CharField(
verbose_name=_("name"), verbose_name=_('name'),
max_length=255, max_length=255,
) )
@ -46,7 +67,7 @@ class Food(PolymorphicModel):
allergens = models.ManyToManyField( allergens = models.ManyToManyField(
Allergen, Allergen,
blank=True, blank=True,
verbose_name=_('allergens'), verbose_name=_('allergen'),
) )
expiry_date = models.DateTimeField( expiry_date = models.DateTimeField(
@ -54,69 +75,41 @@ class Food(PolymorphicModel):
null=False, null=False,
) )
end_of_life = models.CharField( was_eaten = models.BooleanField(
blank=True, default=False,
verbose_name=_('end of life'), verbose_name=_('was eaten'),
max_length=255,
) )
# is_ready != is_active : is_ready signifie que la nourriture est prête à être manger,
# is_active signifie que la nourriture n'est pas encore archivé
# il sert dans les cas où il est plus intéressant que de l'open soit conservé (confiture par ex)
is_ready = models.BooleanField( is_ready = models.BooleanField(
default=False,
verbose_name=_('is ready'), verbose_name=_('is ready'),
max_length=255,
) )
order = models.CharField( is_active = models.BooleanField(
blank=True, default=True,
verbose_name=_('order'), verbose_name=_('is active'),
max_length=255,
) )
def __str__(self): def __str__(self):
return self.name return self.name
@transaction.atomic @transaction.atomic
def update_allergens(self): def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
# update parents return super().save(force_insert, force_update, using, update_fields)
for parent in self.transformed_ingredient_inv.iterator():
old_allergens = list(parent.allergens.all()).copy()
parent.allergens.clear()
for child in parent.ingredients.iterator():
if child.pk != self.pk:
parent.allergens.set(parent.allergens.union(child.allergens.all()))
parent.allergens.set(parent.allergens.union(self.allergens.all()))
if old_allergens != list(parent.allergens.all()):
parent.save(old_allergens=old_allergens)
def update_expiry_date(self):
# update parents
for parent in self.transformed_ingredient_inv.iterator():
old_expiry_date = parent.expiry_date
parent.expiry_date = parent.shelf_life + parent.creation_date
for child in parent.ingredients.iterator():
if (child.pk != self.pk
and not (child.polymorphic_ctype.model == 'basicfood'
and child.date_type == 'DDM')):
parent.expiry_date = min(parent.expiry_date, child.expiry_date)
if self.polymorphic_ctype.model == 'basicfood' and self.date_type == 'DLC':
parent.expiry_date = min(parent.expiry_date, self.expiry_date)
if old_expiry_date != parent.expiry_date:
parent.save()
class Meta: class Meta:
verbose_name = _('Food') verbose_name = _('food')
verbose_name_plural = _('Foods') verbose_name = _('foods')
class BasicFood(Food): class BasicFood(Food):
""" """
A basic food is a food directly buy and stored Food which has been directly buy on supermarket
""" """
arrival_date = models.DateTimeField(
default=timezone.now,
verbose_name=_('arrival date'),
)
date_type = models.CharField( date_type = models.CharField(
max_length=255, max_length=255,
choices=( choices=(
@ -125,70 +118,50 @@ class BasicFood(Food):
) )
) )
arrival_date = models.DateTimeField(
verbose_name=_('arrival date'),
default=timezone.now,
)
# label = models.ImageField(
# verbose_name=_('food label'),
# max_length=255,
# blank=False,
# null=False,
# upload_to='label/',
# )
@transaction.atomic @transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None, **kwargs): def update_allergens(self):
created = self.pk is None # update parents
if not created: for parent in self.transformed_ingredient_inv.iterator():
# Check if important fields are updated parent.update_allergens()
old_food = Food.objects.select_for_update().get(pk=self.pk)
if not hasattr(self, "_force_save"):
# Allergens
if ('old_allergens' in kwargs @transaction.atomic
and list(self.allergens.all()) != kwargs['old_allergens']): def update_expiry_date(self):
self.update_allergens() # update parents
for parent in self.transformed_ingredient_inv.iterator():
parent.update_expiry_date()
# Expiry date @transaction.atomic
if ((self.expiry_date != old_food.expiry_date def update(self):
and self.date_type == 'DLC') self.update_allergens()
or old_food.date_type != self.date_type): self.update_expiry_date()
self.update_expiry_date()
return super().save(force_insert, force_update, using, update_fields)
@staticmethod
def get_lastests_objects(number, distinct_field, order_by_field):
"""
Get the last object with distinct field and ranked with order_by
This methods exist because we can't distinct with one field and
order with another
"""
foods = BasicFood.objects.order_by(order_by_field).all()
field = []
for food in foods:
if getattr(food, distinct_field) in field:
continue
else:
field.append(getattr(food, distinct_field))
number -= 1
yield food
if not number:
return
class Meta: class Meta:
verbose_name = _('Basic food') verbose_name = _('Basic food')
verbose_name_plural = _('Basic foods') verbose_name_plural = _('Basic foods')
def __str__(self):
return self.name
class TransformedFood(Food): class TransformedFood(Food):
""" """
A transformed food is a food with ingredients Transformed food are a mix between basic food and meal
""" """
creation_date = models.DateTimeField( creation_date = models.DateTimeField(
default=timezone.now,
verbose_name=_('creation date'), verbose_name=_('creation date'),
) )
# Without microbiological analyzes, the storage time is 3 days ingredient = models.ManyToManyField(
shelf_life = models.DurationField(
default=timedelta(days=3),
verbose_name=_('shelf life'),
)
ingredients = models.ManyToManyField(
Food, Food,
blank=True, blank=True,
symmetrical=False, symmetrical=False,
@ -196,91 +169,58 @@ class TransformedFood(Food):
verbose_name=_('transformed ingredient'), verbose_name=_('transformed ingredient'),
) )
def check_cycle(self, ingredients, origin, checked): # Without microbiological analyzes, the storage time is 3 days
for ingredient in ingredients: shelf_life = models.DurationField(
if ingredient == origin: verbose_name=_("shelf life"),
# We break the cycle default=timedelta(days=3),
self.ingredients.remove(ingredient) )
if ingredient.polymorphic_ctype.model == 'transformedfood' and ingredient not in checked:
ingredient.check_cycle(ingredient.ingredients.all(), origin, checked)
checked.append(ingredient)
@transaction.atomic @transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None, **kwargs): def archive(self):
created = self.pk is None # When a meal are archived, if it was eaten, update ingredient fully used for this meal
if not created: raise NotImplementedError
# Check if important fields are updated
update = {'allergens': False, 'expiry_date': False}
old_food = Food.objects.select_for_update().get(pk=self.pk)
if not hasattr(self, "_force_save"):
# Allergens
# Unfortunately with the many-to-many relation we can't access
# to old allergens
if ('old_allergens' in kwargs
and list(self.allergens.all()) != kwargs['old_allergens']):
update['allergens'] = True
# Expiry date @transaction.atomic
update['expiry_date'] = (self.shelf_life != old_food.shelf_life def update_allergens(self):
or self.creation_date != old_food.creation_date) # When allergens are changed, simply update the parents' allergens
if update['expiry_date']: old_allergens = list(self.allergens.all())
self.expiry_date = self.creation_date + self.shelf_life self.allergens.clear()
# Unfortunately with the set method ingredients are already save, for ingredient in self.ingredient.iterator():
# we check cycle after if possible self.allergens.set(self.allergens.union(ingredient.allergens.all()))
if ('old_ingredients' in kwargs
and list(self.ingredients.all()) != list(kwargs['old_ingredients'])):
update['allergens'] = True
update['expiry_date'] = True
# it's preferable to keep a queryset but we allow list too if old_allergens == list(self.allergens.all()):
if type(kwargs['old_ingredients']) is list: return
kwargs['old_ingredients'] = Food.objects.filter( super().save()
pk__in=[food.pk for food in kwargs['old_ingredients']])
self.check_cycle(self.ingredients.all().difference(kwargs['old_ingredients']), self, [])
if update['allergens']:
self.update_allergens()
if update['expiry_date']:
self.update_expiry_date()
if created: # update parents
self.expiry_date = self.shelf_life + self.creation_date for parent in self.transformed_ingredient_inv.iterator():
parent.update_allergens()
# We save here because we need pk for many-to-many relation @transaction.atomic
super().save(force_insert, force_update, using, update_fields) def update_expiry_date(self):
# When expiry_date is changed, simply update the parents' expiry_date
old_expiry_date = self.expiry_date
self.expiry_date = self.creation_date + self.shelf_life
for ingredient in self.ingredient.iterator():
self.expiry_date = min(self.expiry_date, ingredient.expiry_date)
for child in self.ingredients.iterator(): if old_expiry_date == self.expiry_date:
self.allergens.set(self.allergens.union(child.allergens.all())) return
if not (child.polymorphic_ctype.model == 'basicfood' and child.date_type == 'DDM'): super().save()
self.expiry_date = min(self.expiry_date, child.expiry_date)
return super().save(force_insert, force_update, using, update_fields) # update parents
for parent in self.transformed_ingredient_inv.iterator():
parent.update_expiry_date()
@transaction.atomic
def update(self):
self.update_allergens()
self.update_expiry_date()
@transaction.atomic
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Transformed food') verbose_name = _('Transformed food')
verbose_name_plural = _('Transformed foods') verbose_name_plural = _('Transformed foods')
def __str__(self):
return self.name
class QRCode(models.Model):
"""
QR-code for register food
"""
qr_code_number = models.PositiveIntegerField(
unique=True,
verbose_name=_('qr code number'),
)
food_container = models.ForeignKey(
Food,
on_delete=models.CASCADE,
related_name='QR_code',
verbose_name=_('food container'),
)
class Meta:
verbose_name = _('QR-code')
verbose_name_plural = _('QR-codes')
def __str__(self):
return _('QR-code number') + ' ' + str(self.qr_code_number)

View File

@ -2,20 +2,18 @@
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables import django_tables2 as tables
from django_tables2 import A
from .models import Food from .models import TransformedFood
class FoodTable(tables.Table): class TransformedFoodTable(tables.Table):
""" name = tables.LinkColumn(
List all foods. 'food:food_view',
""" args=[A('pk'), ],
)
class Meta: class Meta:
model = Food model = TransformedFood
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('name', 'owner', 'allergens', 'expiry_date') fields = ('name', "owner", "allergens", "expiry_date")
row_attrs = {
'class': 'table-row',
'data-href': lambda record: 'detail/' + str(record.pk),
'style': 'cursor:pointer',
}

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% comment %}
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 }} {{ food.name }}
</h3>
<div class="card-body">
<ul>
<li><p>{% trans 'Owner' %} : {{ food.owner }}</p></li>
<li><p>{% trans 'Arrival date' %} : {{ food.arrival_date }}</p></li>
<li><p>{% trans 'Expiry date' %} : {{ food.expiry_date }} ({{ food.date_type }})</p></li>
<li>{% trans 'Allergens' %} :</li>
<ul>
{% for allergen in food.allergens.iterator %}
<li>{{ allergen.name }}</li>
{% endfor %}
</ul>
<p>
<li><p>{% trans 'Active' %} : {{ food.is_active }}<p></li>
<li><p>{% trans 'Eaten' %} : {{ food.was_eaten }}<p></li>
</ul>
{% if can_update %}
<a class="btn btn-sm btn-warning" href="{% url "food:basic_update" pk=food.pk %}">{% trans 'Update' %}</a>
{% endif %}
{% if can_add_ingredient %}
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans 'Add to a meal' %}
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,6 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% comment %} {% comment %}
Copyright (C) by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %} {% endcomment %}
{% load i18n crispy_forms_tags %} {% load i18n crispy_forms_tags %}

View File

@ -0,0 +1,55 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% 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">
<a class="btn btn-sm btn-success" href="{% url "food:qrcode_basic_create" slug=slug %}">
{% trans 'New basic food' %}
</a>
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
<div class="card-body" id="profile_infos">
<h4>{% trans "Copy constructor" %}</h4>
<table class="table">
<thead>
<tr>
<th class="orderable">
{% trans "Name" %}
</th>
<th class="orderable">
{% trans "Owner" %}
</th>
<th class="orderable">
{% trans "Arrival date" %}
</th>
<th class="orderable">
{% trans "Expiry date" %}
</th>
</tr>
</thead>
<tbody>
{% for basic in last_basic %}
<tr>
<td><a href="{% url "food:qrcode_basic_create" slug=slug %}?copy={{ basic.pk }}">{{ basic.name }}</a></td>
<td>{{ basic.owner }}</td>
<td>{{ basic.arrival_date }}</td>
<td>{{ basic.expiry_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,53 +0,0 @@
{% 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 }} {{ food.name }}
</h3>
<div class="card-body">
<ul>
{% for field, value in fields %}
<li> {{ field }} : {{ value }}</li>
{% endfor %}
{% if meals %}
<li> {% trans "Contained in" %} :
{% for meal in meals %}
<a href="{% url "food:transformedfood_view" pk=meal.pk %}">{{ meal.name }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
</li>
{% endif %}
{% if foods %}
<li> {% trans "Contain" %} :
{% for food in foods %}
<a href="{% url "food:food_view" pk=food.pk %}">{{ food.name }}</a>{% if not forloop.last %},{% endif %}
{% endfor %}
</li>
{% endif %}
</ul>
{% if update %}
<a class="btn btn-sm btn-secondary" href="{% url "food:food_update" pk=food.pk %}">
{% trans "Update" %}
</a>
{% endif %}
{% if add_ingredient %}
<a class="btn btn-sm btn-primary" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans "Add to a meal" %}
</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 %}
<a class="btn btn-sm btn-primary" href="{% url "food:food_list" %}">
{% trans "Return to the food list" %}
</a>
</div>
</div>
{% endblock %}

View File

@ -1,71 +0,0 @@
{% extends "base_search.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
{{ block.super }}
<br>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Meal served" %}
</h3>
{% if can_add_meal %}
<div class="card-footer">
<a class="btn btn-sm btn-primary" href="{% url 'food:transformedfood_create' %}">
{% trans "New meal" %}
</a>
</div>
{% endif %}
{% if served.data %}
{% render_table served %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no meal served." %}
</div>
</div>
</div>
{% endif %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Free food" %}
</h3>
{% if open.data %}
{% render_table open %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no free food." %}
</div>
</div>
{% endif %}
</div>
{% if club_tables %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Food of your clubs" %}
</h3>
</div>
{% for table in club_tables %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Food of club" %} {{ table.prefix }}
</h3>
{% if table.data %}
{% render_table table %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "Yours club has not food yet." %}
</div>
</div>
{% endif %}
</div>
{% endfor %}
{% endif %}
</div>
{% endblock %}

View File

@ -1,116 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form"></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 %}

View File

@ -1,52 +0,0 @@
{% 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 %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form | crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
<div class="card-body">
<h4>
{% trans "Copy constructor" %}
<a class="btn btn-secondary" href="{% url "food:basicfood_create" slug=slug %}">{% trans "New food" %}</a>
</h4>
<table class="table">
<thead>
<tr>
<th class="orderable">
{% trans "Name" %}
</th>
<th class="orderable">
{% trans "Owner" %}
</th>
<th class="orderable">
{% trans "Expiry date" %}
</th>
</tr>
</thead>
<tbody>
{% for food in last_items %}
<tr>
<td><a href="{% url "food:basicfood_create" slug=slug %}?copy={{ food.pk }}">{{ food.name }}</a></td>
<td>{{ food.owner }}</td>
<td>{{ food.expiry_date }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,39 @@
{% extends "base.html" %}
{% comment %}
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 }} {% trans 'number' %} {{ qrcode.qr_code_number }}
</h3>
<div class="card-body">
<ul>
<li><p>{% trans 'Name' %} : {{ qrcode.food_container.name }}</p></li>
<li><p>{% trans 'Owner' %} : {{ qrcode.food_container.owner }}</p></li>
<li><p>{% trans 'Expiry date' %} : {{ qrcode.food_container.expiry_date }}</p></li>
</ul>
{% if qrcode.food_container.polymorphic_ctype.model == 'basicfood' and can_update_basic %}
<a class="btn btn-sm btn-warning" href="{% url "food:basic_update" pk=qrcode.food_container.pk %}" data-turbolinks="false">
{% trans 'Update' %}
</a>
{% elif can_update_transformed %}
<a class="btn btn-sm btn-warning" href="{% url "food:transformed_update" pk=qrcode.food_container.pk %}">
{% trans 'Update' %}
</a>
{% endif %}
{% if can_view_detail %}
<a class="btn btn-sm btn-primary" href="{% url "food:food_view" pk=qrcode.food_container.pk %}">
{% trans 'View details' %}
</a>
{% endif %}
{% if can_add_ingredient %}
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=qrcode.food_container.pk %}">
{% trans 'Add to a meal' %}
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,51 @@
{% extends "base.html" %}
{% comment %}
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 }} {{ food.name }}
</h3>
<div class="card-body">
<ul>
<li><p>{% trans 'Owner' %} : {{ food.owner }}</p></li>
{% if can_see_ready %}
<li><p>{% trans 'Ready' %} : {{ food.is_ready }}</p></li>
{% endif %}
<li><p>{% trans 'Creation date' %} : {{ food.creation_date }}</p></li>
<li><p>{% trans 'Expiry date' %} : {{ food.expiry_date }}</p></li>
<li>{% trans 'Allergens' %} :</li>
<ul>
{% for allergen in food.allergens.iterator %}
<li>{{ allergen.name }}</li>
{% endfor %}
</ul>
<p>
<li>{% trans 'Ingredients' %} :</li>
<ul>
{% for ingredient in food.ingredient.iterator %}
<li><a href="{% url "food:food_view" pk=ingredient.pk %}">{{ ingredient.name }}</a></li>
{% endfor %}
</ul>
<p>
<li><p>{% trans 'Shelf life' %} : {{ food.shelf_life }}</p></li>
<li><p>{% trans 'Ready' %} : {{ food.is_ready }}</p></li>
<li><p>{% trans 'Active' %} : {{ food.is_active }}</p></li>
<li><p>{% trans 'Eaten' %} : {{ food.was_eaten }}</p></li>
</ul>
{% if can_update %}
<a class="btn btn-sm btn-warning" href="{% url "food:transformed_update" pk=food.pk %}">
{% trans 'Update' %}
</a>
{% endif %}
{% if can_add_ingredient %}
<a class="btn btn-sm btn-success" href="{% url "food:add_ingredient" pk=food.pk %}">
{% trans 'Add to a meal' %}
</a>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{{ title }}
</h3>
<div class="card-body" id="form">
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
</form>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,60 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load render_table from django_tables2 %}
{% load i18n %}
{% block content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Meal served" %}
</h3>
{% if can_create_meal %}
<div class="card-footer">
<a class="btn btn-sm btn-success" href="{% url 'food:transformed_create' %}" data-turbolinks="false">
{% trans 'New meal' %}
</a>
</div>
{% endif %}
{% if served.data %}
{% render_table served %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no meal served." %}
</div>
</div>
{% endif %}
</div>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Open" %}
</h3>
{% if open.data %}
{% render_table open %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no free meal." %}
</div>
</div>
{% endif %}
</div>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "All meals" %}
</h3>
{% if table.data %}
{% render_table table %}
{% else %}
<div class="card-body">
<div class="alert alert-warning">
{% trans "There is no meal." %}
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,87 +0,0 @@
{% 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 %}

3
apps/food/tests.py Normal file
View File

@ -0,0 +1,3 @@
# from django.test import TestCase
# Create your tests here.

View File

@ -1,170 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from ..api.views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet
from ..models import Allergen, BasicFood, TransformedFood, QRCode
class TestFood(TestCase):
"""
Test food
"""
fixtures = ('initial',)
def setUp(self):
self.user = User.objects.create_superuser(
username='admintoto',
password='toto1234',
email='toto@example.com'
)
self.client.force_login(self.user)
sess = self.client.session
sess['permission_mask'] = 42
sess.save()
self.allergen = Allergen.objects.create(
name='allergen',
)
self.basicfood = BasicFood.objects.create(
name='basicfood',
owner_id=1,
expiry_date=timezone.now(),
is_ready=False,
date_type='DLC',
)
self.transformedfood = TransformedFood.objects.create(
name='transformedfood',
owner_id=1,
expiry_date=timezone.now(),
is_ready=False,
)
self.qrcode = QRCode.objects.create(
qr_code_number=1,
food_container=self.basicfood,
)
def test_food_list(self):
"""
Display food list
"""
response = self.client.get(reverse('food:food_list'))
self.assertEqual(response.status_code, 200)
def test_qrcode_create(self):
"""
Display QRCode creation
"""
response = self.client.get(reverse('food:qrcode_create'))
self.assertEqual(response.status_code, 200)
def test_basicfood_create(self):
"""
Display BasicFood creation
"""
response = self.client.get(reverse('food:basicfood_create'))
self.assertEqual(response.status_code, 200)
def test_transformedfood_create(self):
"""
Display TransformedFood creation
"""
response = self.client.get(reverse('food:transformedfood_create'))
self.assertEqual(response.status_code, 200)
def test_food_create(self):
"""
Display Food update
"""
response = self.client.get(reverse('food:food_update'))
self.assertEqual(response.status_code, 200)
def test_food_view(self):
"""
Display Food detail
"""
response = self.client.get(reverse('food:food_view'))
self.assertEqual(response.status_code, 302)
def test_basicfood_view(self):
"""
Display BasicFood detail
"""
response = self.client.get(reverse('food:basicfood_view'))
self.assertEqual(response.status_code, 200)
def test_transformedfood_view(self):
"""
Display TransformedFood detail
"""
response = self.client.get(reverse('food:transformedfood_view'))
self.assertEqual(response.status_code, 200)
def test_add_ingredient(self):
"""
Display add ingredient view
"""
response = self.client.get(reverse('food:add_ingredient'))
self.assertEqual(response.status_code, 200)
class TestFoodAPI(TestAPI):
def setUp(self) -> None:
super().setUP()
self.allergen = Allergen.objects.create(
name='name',
)
self.basicfood = BasicFood.objects.create(
name='basicfood',
owner_id=1,
expiry_date=timezone.now(),
is_ready=False,
date_type='DLC',
)
self.transformedfood = TransformedFood.objects.create(
name='transformedfood',
owner_id=1,
expiry_date=timezone.now(),
is_ready=False,
)
self.qrcode = QRCode.objects.create(
qr_code_number=1,
food_container=self.basicfood,
)
def test_allergen_api(self):
"""
Load Allergen API page and test all filters and permissions
"""
self.check_viewset(AllergenViewSet, '/api/food/allergen/')
def test_basicfood_api(self):
"""
Load BasicFood API page and test all filters and permissions
"""
self.check_viewset(BasicFoodViewSet, '/api/food/basicfood/')
def test_transformedfood_api(self):
"""
Load TransformedFood API page and test all filters and permissions
"""
self.check_viewset(TransformedFoodViewSet, '/api/food/transformedfood/')
def test_qrcode_api(self):
"""
Load QRCode API page and test all filters and permissions
"""
self.check_viewset(QRCodeViewSet, '/api/food/qrcode/')

View File

@ -8,14 +8,14 @@ from . import views
app_name = 'food' app_name = 'food'
urlpatterns = [ urlpatterns = [
path('', views.FoodListView.as_view(), name='food_list'), path('', views.TransformedListView.as_view(), name='food_list'),
path('<int:slug>', views.QRCodeCreateView.as_view(), name='qrcode_create'), path('<int:slug>', views.QRCodeView.as_view(), name='qrcode_view'),
path('<int:slug>/add/basic', views.BasicFoodCreateView.as_view(), name='basicfood_create'), path('detail/<int:pk>', views.FoodView.as_view(), name='food_view'),
path('add/transformed', views.TransformedFoodCreateView.as_view(), name='transformedfood_create'),
path('update/<int:pk>', views.FoodUpdateView.as_view(), name='food_update'), path('<int:slug>/create_qrcode', views.QRCodeCreateView.as_view(), name='qrcode_create'),
path('update/ingredients/<int:pk>', views.ManageIngredientsView.as_view(), name='manage_ingredients'), path('<int:slug>/create_qrcode/basic', views.QRCodeBasicFoodCreateView.as_view(), name='qrcode_basic_create'),
path('detail/<int:pk>', views.FoodDetailView.as_view(), name='food_view'), path('create/transformed', views.TransformedFoodCreateView.as_view(), name='transformed_create'),
path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'), path('update/basic/<int:pk>', views.BasicFoodUpdateView.as_view(), name='basic_update'),
path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'), path('update/transformed/<int:pk>', views.TransformedFoodUpdateView.as_view(), name='transformed_update'),
path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'), path('add/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
] ]

View File

@ -1,53 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
seconds = (_('second'), _('seconds'))
minutes = (_('minute'), _('minutes'))
hours = (_('hour'), _('hours'))
days = (_('day'), _('days'))
weeks = (_('week'), _('weeks'))
def plural(x):
if x == 1:
return 0
return 1
def pretty_duration(duration):
"""
I receive datetime.timedelta object
You receive string object
"""
text = []
sec = duration.seconds
d = duration.days
if d >= 7:
w = d // 7
text.append(str(w) + ' ' + weeks[plural(w)])
d -= w * 7
if d > 0:
text.append(str(d) + ' ' + days[plural(d)])
if sec >= 3600:
h = sec // 3600
text.append(str(h) + ' ' + hours[plural(h)])
sec -= h * 3600
if sec >= 60:
m = sec // 60
text.append(str(m) + ' ' + minutes[plural(m)])
sec -= m * 60
if sec > 0:
text.append(str(sec) + ' ' + seconds[plural(sec)])
if len(text) == 0:
return ''
if len(text) == 1:
return text[0]
if len(text) >= 2:
return ', '.join(t for t in text[:-1]) + ' ' + _('and') + ' ' + text[-1]

View File

@ -1,508 +1,421 @@
# 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 datetime import timedelta
from api.viewsets import is_regex
from django_tables2.views import MultiTableMixin
from django.db import transaction from django.db import transaction
from django.db.models import Q from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect, Http404 from django.http import HttpResponseRedirect
from django.views.generic import DetailView, UpdateView, CreateView from django_tables2.views import MultiTableMixin
from django.views.generic.list import ListView from django.urls import reverse
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from member.models import Club, Membership from django.utils import timezone
from django.views.generic import DetailView, UpdateView
from django.views.generic.list import ListView
from django.forms import HiddenInput
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView, LoginRequiredMixin from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .models import Food, BasicFood, TransformedFood, QRCode from .forms import AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms
from .forms import QRCodeForms, BasicFoodForms, TransformedFoodForms, \ from .models import BasicFood, Food, QRCode, TransformedFood
ManageIngredientsForm, ManageIngredientsFormSet, AddIngredientForms, \ from .tables import TransformedFoodTable
BasicFoodUpdateForms, TransformedFoodUpdateForms
from .tables import FoodTable
from .utils import pretty_duration
class FoodListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView): class AddIngredientView(ProtectQuerysetMixin, UpdateView):
""" """
Display Food A view to add an ingredient
""" """
model = Food model = Food
tables = [FoodTable, FoodTable, FoodTable, ] template_name = 'food/add_ingredient_form.html'
extra_context = {"title": _('Food')} extra_context = {"title": _("Add the ingredient")}
template_name = 'food/food_list.html' form_class = AddIngredientForms
def get_queryset(self, **kwargs): def get_context_data(self, **kwargs):
return super().get_queryset(**kwargs).distinct() context = super().get_context_data(**kwargs)
context["pk"] = self.kwargs["pk"]
return context
def get_tables(self): @transaction.atomic
bureau_role_pk = 4 def form_valid(self, form):
clubs = Club.objects.filter(membership__in=Membership.objects.filter( form.instance.creater = self.request.user
user=self.request.user, roles=bureau_role_pk).filter( food = Food.objects.get(pk=self.kwargs['pk'])
date_end__gte=timezone.now())) add_ingredient_form = AddIngredientForms(data=self.request.POST)
if food.is_ready:
form.add_error(None, _("The product is already prepared"))
return self.form_invalid(form)
if not add_ingredient_form.is_valid():
return self.form_invalid(form)
tables = [FoodTable] * (clubs.count() + 3) # We flip logic ""fully used = not is_active""
self.tables = tables food.is_active = not food.is_active
tables = super().get_tables() # Save the aliment and the allergens associed
tables[0].prefix = 'search-' for transformed_pk in self.request.POST.getlist('ingredient'):
tables[1].prefix = 'open-' transformed = TransformedFood.objects.get(pk=transformed_pk)
tables[2].prefix = 'served-' if not transformed.is_ready:
for i in range(clubs.count()): transformed.ingredient.add(food)
tables[i + 3].prefix = clubs[i].name transformed.update()
return tables food.save()
def get_tables_data(self): return HttpResponseRedirect(self.get_success_url())
# table search
qs = self.get_queryset().order_by('name')
if "search" in self.request.GET and self.request.GET['search']:
pattern = self.request.GET['search']
# check regex def get_success_url(self, **kwargs):
valid_regex = is_regex(pattern) return reverse('food:food_list')
suffix = '__iregex' if valid_regex else '__istartswith'
prefix = '^' if valid_regex else ''
qs = qs.filter(Q(**{f'name{suffix}': prefix + pattern})) class BasicFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
else: """
qs = qs.none() A view to update a basic food
search_table = qs.filter(PermissionBackend.filter_queryset(self.request, Food, 'view')) """
# table open model = BasicFood
open_table = self.get_queryset().order_by('expiry_date').filter( form_class = BasicFoodForms
Q(polymorphic_ctype__model='transformedfood') template_name = 'food/basicfood_form.html'
| Q(polymorphic_ctype__model='basicfood', basicfood__date_type='DLC')).filter( extra_context = {"title": _("Update an aliment")}
expiry_date__lt=timezone.now(), end_of_life='').filter(
PermissionBackend.filter_queryset(self.request, Food, 'view')) @transaction.atomic
# table served def form_valid(self, form):
served_table = self.get_queryset().order_by('-pk').filter( form.instance.creater = self.request.user
end_of_life='', is_ready=True).exclude( basic_food_form = BasicFoodForms(data=self.request.POST)
Q(polymorphic_ctype__model='basicfood', if not basic_food_form.is_valid():
basicfood__date_type='DLC', return self.form_invalid(form)
expiry_date__lte=timezone.now(),)
| Q(polymorphic_ctype__model='transformedfood', ans = super().form_valid(form)
expiry_date__lte=timezone.now(), form.instance.update()
)) return ans
# tables club
bureau_role_pk = 4 def get_success_url(self, **kwargs):
clubs = Club.objects.filter(membership__in=Membership.objects.filter( self.object.refresh_from_db()
user=self.request.user, roles=bureau_role_pk).filter( return reverse('food:food_view', kwargs={"pk": self.object.pk})
date_end__gte=timezone.now()))
club_table = [] def get_context_data(self, **kwargs):
for club in clubs: context = super().get_context_data(**kwargs)
club_table.append(self.get_queryset().order_by('expiry_date').filter( return context
owner=club, end_of_life='').filter(
PermissionBackend.filter_queryset(self.request, Food, 'view')
)) class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
return [search_table, open_table, served_table] + club_table """
A view to see a food
"""
model = Food
extra_context = {"title": _("Details of:")}
context_object_name = "food"
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
tables = context['tables'] context["can_update"] = PermissionBackend.check_perm(self.request, "food.change_food")
# for extends base_search.html we need to name 'search_table' in 'table' context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
for name, table in zip(['table', 'open', 'served'], tables):
context[name] = table
context['club_tables'] = tables[3:]
context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add')
return context return context
class QRCodeCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
#####################################################################
# TO DO
# - this feature is very pratical for meat or fish, nevertheless we can implement this later
# - fix picture save
# - implement solution crop and convert image (reuse or recode ImageForm from members apps)
#####################################################################
""" """
A view to add qrcode A view to add a basic food with a qrcode
"""
model = BasicFood
form_class = BasicFoodForms
template_name = 'food/basicfood_form.html'
extra_context = {"title": _("Add a new basic food with QRCode")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
basic_food_form = BasicFoodForms(data=self.request.POST)
if not basic_food_form.is_valid():
return self.form_invalid(form)
# Save the aliment and the allergens associed
basic_food = form.save(commit=False)
# We assume the date of labeling and the same as the date of arrival
basic_food.arrival_date = timezone.now()
basic_food.is_ready = False
basic_food.is_active = True
basic_food.was_eaten = False
basic_food._force_save = True
basic_food.save()
basic_food.refresh_from_db()
qrcode = QRCode()
qrcode.qr_code_number = self.kwargs['slug']
qrcode.food_container = basic_food
qrcode.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
def get_sample_object(self):
# We choose a club which may work or BDE else
owner_id = 1
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food = BasicFood(name="", expiry_date=timezone.now(), owner_id=club_id)
if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
owner_id = club_id
return BasicFood(
name="",
expiry_date=timezone.now(),
owner_id=owner_id,
)
def get_context_data(self, **kwargs):
# Some field are hidden on create
context = super().get_context_data(**kwargs)
form = context['form']
form.fields['is_active'].widget = HiddenInput()
form.fields['was_eaten'].widget = HiddenInput()
copy = self.request.GET.get('copy', None)
if copy is not None:
basic = BasicFood.objects.get(pk=copy)
for field in ['date_type', 'expiry_date', 'name', 'owner']:
form.fields[field].initial = getattr(basic, field)
for field in ['allergens']:
form.fields[field].initial = getattr(basic, field).all()
return context
class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add a new qrcode
""" """
model = QRCode model = QRCode
template_name = 'food/qrcode.html' template_name = 'food/create_qrcode_form.html'
form_class = QRCodeForms form_class = QRCodeForms
extra_context = {"title": _("Add a new QRCode")} extra_context = {"title": _("Add a new QRCode")}
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
qrcode = kwargs["slug"] qrcode = kwargs["slug"]
if self.model.objects.filter(qr_code_number=qrcode).count() > 0: if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
pk = self.model.objects.get(qr_code_number=qrcode).food_container.pk return HttpResponseRedirect(reverse("food:qrcode_view", kwargs=kwargs))
return HttpResponseRedirect(reverse_lazy("food:food_view", kwargs={"pk": pk}))
else: else:
return super().get(*args, **kwargs) return super().get(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["slug"] = self.kwargs["slug"]
context["last_basic"] = BasicFood.objects.order_by('-pk').all()[:10]
return context
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.creater = self.request.user
qrcode_food_form = QRCodeForms(data=self.request.POST) qrcode_food_form = QRCodeForms(data=self.request.POST)
if not qrcode_food_form.is_valid(): if not qrcode_food_form.is_valid():
return self.form_invalid(form) return self.form_invalid(form)
# Save the qrcode
qrcode = form.save(commit=False) qrcode = form.save(commit=False)
qrcode.qr_code_number = self.kwargs['slug'] qrcode.qr_code_number = self.kwargs["slug"]
qrcode._force_save = True qrcode._force_save = True
qrcode.save() qrcode.save()
qrcode.refresh_from_db() qrcode.refresh_from_db()
qrcode.food_container.save()
return super().form_valid(form) return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['slug'] = self.kwargs['slug']
# get last 10 BasicFood objects with distincts 'name' ordered by '-pk'
# we can't use .distinct and .order_by with differents columns hence the generator
context['last_items'] = [food for food in BasicFood.get_lastests_objects(10, 'name', '-pk')]
return context
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
self.object.refresh_from_db() self.object.refresh_from_db()
return reverse_lazy('food:food_view', kwargs={'pk': self.object.food_container.pk}) return reverse('food:qrcode_view', kwargs={"slug": self.kwargs['slug']})
def get_sample_object(self): def get_sample_object(self):
return QRCode( return QRCode(
qr_code_number=self.kwargs['slug'], qr_code_number=self.kwargs["slug"],
food_container_id=1, food_container_id=1
) )
class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
A view to add basicfood A view to see a qrcode
""" """
model = BasicFood model = QRCode
form_class = BasicFoodForms extra_context = {"title": _("QRCode")}
extra_context = {"title": _("Add an aliment")} context_object_name = "qrcode"
template_name = "food/food_update.html" slug_field = "qr_code_number"
def get_sample_object(self): def get(self, *args, **kwargs):
# We choose a club which may work or BDE else qrcode = kwargs["slug"]
food = BasicFood( if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
name="", return super().get(*args, **kwargs)
owner_id=1, else:
expiry_date=timezone.now(), return HttpResponseRedirect(reverse("food:qrcode_create", kwargs=kwargs))
is_ready=True,
arrival_date=timezone.now(),
date_type='DLC',
)
for membership in self.request.user.memberships.all(): def get_context_data(self, **kwargs):
club_id = membership.club.id context = super().get_context_data(**kwargs)
food.owner_id = club_id
if PermissionBackend.check_perm(self.request, "food.add_basicfood", food):
return food
return food qr_code_number = self.kwargs['slug']
qrcode = self.model.objects.get(qr_code_number=qr_code_number)
@transaction.atomic model = qrcode.food_container.polymorphic_ctype.model
def form_valid(self, form):
if QRCode.objects.filter(qr_code_number=self.kwargs['slug']).count() > 0:
return HttpResponseRedirect(reverse_lazy('food:qrcode_create', kwargs={'slug': self.kwargs['slug']}))
food_form = BasicFoodForms(data=self.request.POST)
if not food_form.is_valid():
return self.form_invalid(form)
food = form.save(commit=False)
food.is_ready = False
food.save()
food.refresh_from_db()
qrcode = QRCode()
qrcode.qr_code_number = self.kwargs['slug']
qrcode.food_container = food
qrcode.save()
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse_lazy('food:basicfood_view', kwargs={"pk": self.object.pk})
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
copy = self.request.GET.get('copy', None)
if copy is not None:
food = BasicFood.objects.get(pk=copy)
print(context['form'].fields)
for field in context['form'].fields:
if field == 'allergens':
context['form'].fields[field].initial = getattr(food, field).all()
else:
context['form'].fields[field].initial = getattr(food, field)
if model == "basicfood":
context["can_update_basic"] = PermissionBackend.check_perm(self.request, "food.change_basicfood")
context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_basicfood")
if model == "transformedfood":
context["can_update_transformed"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
context["can_view_detail"] = PermissionBackend.check_perm(self.request, "food.view_transformedfood")
context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
return context return context
class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView): class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
""" """
A view to add transformedfood A view to add a tranformed food
""" """
model = TransformedFood model = TransformedFood
template_name = 'food/transformedfood_form.html'
form_class = TransformedFoodForms form_class = TransformedFoodForms
extra_context = {"title": _("Add a meal")} extra_context = {"title": _("Add a new meal")}
template_name = "food/food_update.html"
def get_sample_object(self):
# We choose a club which may work or BDE else
food = TransformedFood(
name="",
owner_id=1,
expiry_date=timezone.now(),
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
def form_valid(self, form):
form.instance.expiry_date = timezone.now() + timedelta(days=3)
form.instance.is_ready = False
return super().form_valid(form)
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse_lazy('food:transformedfood_view', kwargs={"pk": self.object.pk})
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
"""
model = Food
extra_context = {"title": _("Add the ingredient:")}
form_class = AddIngredientForms
template_name = 'food/food_update.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['title'] += ' ' + self.object.name
return context
@transaction.atomic
def form_valid(self, form):
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:
old_ingredients = list(meal.ingredients.all()).copy()
old_allergens = list(meal.allergens.all()).copy()
meal.ingredients.add(self.object.pk)
# update allergen and expiry date if necessary
if not (self.object.polymorphic_ctype.model == 'basicfood'
and self.object.date_type == 'DDM'):
meal.expiry_date = min(meal.expiry_date, self.object.expiry_date)
meal.allergens.set(meal.allergens.union(self.object.allergens.all()))
meal.save(old_ingredients=old_ingredients, old_allergens=old_allergens)
if 'fully_used' in form.data:
if not self.object.end_of_life:
self.object.end_of_life = _(f'Food fully used in : {meal.name}')
else:
self.object.end_of_life += ', ' + meal.name
if 'fully_used' in form.data:
self.object.is_ready = False
self.object.save()
# We redirect only the first parent
parent_pk = meals[0].pk
return HttpResponseRedirect(self.get_success_url(parent_pk=parent_pk))
def get_success_url(self, **kwargs):
return reverse_lazy('food:transformedfood_view', kwargs={"pk": kwargs['parent_pk']})
class FoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
A view to update Food
"""
model = Food
extra_context = {"title": _("Update an aliment")}
template_name = 'food/food_update.html'
@transaction.atomic @transaction.atomic
def form_valid(self, form): def form_valid(self, form):
form.instance.creater = self.request.user form.instance.creater = self.request.user
food = Food.objects.get(pk=self.kwargs['pk']) transformed_food_form = TransformedFoodForms(data=self.request.POST)
old_allergens = list(food.allergens.all()).copy() if not transformed_food_form.is_valid():
if food.polymorphic_ctype.model == 'transformedfood':
old_ingredients = food.ingredients.all()
form.instance.shelf_life = timedelta(
seconds=int(form.data['shelf_life']) * 60 * 60)
food_form = self.get_form_class()(data=self.request.POST)
if not food_form.is_valid():
return self.form_invalid(form) return self.form_invalid(form)
# Save the aliment and allergens associated
transformed_food = form.save(commit=False)
transformed_food.expiry_date = transformed_food.creation_date
transformed_food.is_active = True
transformed_food.is_ready = False
transformed_food.was_eaten = False
transformed_food._force_save = True
transformed_food.save()
transformed_food.refresh_from_db()
ans = super().form_valid(form) ans = super().form_valid(form)
if food.polymorphic_ctype.model == 'transformedfood': transformed_food.update()
form.instance.save(old_ingredients=old_ingredients)
else:
form.instance.save(old_allergens=old_allergens)
return ans return ans
def get_form_class(self, **kwargs):
food = Food.objects.get(pk=self.kwargs['pk'])
if food.polymorphic_ctype.model == 'basicfood':
return BasicFoodUpdateForms
else:
return TransformedFoodUpdateForms
def get_form(self, **kwargs):
form = super().get_form(**kwargs)
if 'shelf_life' in form.initial:
hours = form.initial['shelf_life'].days * 24 + form.initial['shelf_life'].seconds // 3600
form.initial['shelf_life'] = hours
return form
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
self.object.refresh_from_db() self.object.refresh_from_db()
return reverse_lazy('food:food_view', kwargs={"pk": self.object.pk}) return reverse('food:food_view', kwargs={"pk": self.object.pk})
def get_sample_object(self):
# We choose a club which may work or BDE else
owner_id = 1
for membership in self.request.user.memberships.all():
club_id = membership.club.id
food = TransformedFood(name="",
creation_date=timezone.now(),
expiry_date=timezone.now(),
owner_id=club_id)
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
owner_id = club_id
break
return TransformedFood(
name="",
owner_id=owner_id,
creation_date=timezone.now(),
expiry_date=timezone.now(),
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Some field are hidden on create
form = context['form']
form.fields['is_active'].widget = HiddenInput()
form.fields['is_ready'].widget = HiddenInput()
form.fields['was_eaten'].widget = HiddenInput()
form.fields['shelf_life'].widget = HiddenInput()
return context
class FoodDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): class TransformedFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
""" """
A view to see a food A view to update transformed product
""" """
model = Food model = TransformedFood
extra_context = {"title": _('Details of:')} template_name = 'food/transformedfood_form.html'
context_object_name = "food" form_class = TransformedFoodForms
template_name = "food/food_detail.html" extra_context = {'title': _('Update a meal')}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
transformedfood_form = TransformedFoodForms(data=self.request.POST)
if not transformedfood_form.is_valid():
return self.form_invalid(form)
ans = super().form_valid(form)
form.instance.update()
return ans
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
fields = ["name", "owner", "expiry_date", "allergens", "is_ready", "end_of_life", "order"]
fields = dict([(field, getattr(self.object, field)) for field in fields])
if fields["is_ready"]:
fields["is_ready"] = _("Yes")
else:
fields["is_ready"] = _("No")
fields["allergens"] = ", ".join(
allergen.name for allergen in fields["allergens"].all())
context["fields"] = [(
Food._meta.get_field(field).verbose_name.capitalize(),
value) for field, value in fields.items()]
context["meals"] = self.object.transformed_ingredient_inv.all()
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"))
return context return context
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
if 'stop_redirect' in kwargs and kwargs['stop_redirect']:
return super().get(*args, **kwargs)
kwargs = {'pk': kwargs['pk']}
if model == 'basicfood':
return HttpResponseRedirect(reverse_lazy("food:basicfood_view", kwargs=kwargs))
return HttpResponseRedirect(reverse_lazy("food:transformedfood_view", kwargs=kwargs))
class TransformedListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
"""
Displays ready TransformedFood
"""
model = TransformedFood
tables = [TransformedFoodTable, TransformedFoodTable, TransformedFoodTable]
extra_context = {"title": _("Transformed food")}
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).distinct()
def get_tables(self):
tables = super().get_tables()
tables[0].prefix = "all-"
tables[1].prefix = "open-"
tables[2].prefix = "served-"
return tables
def get_tables_data(self):
# first table = all transformed food, second table = free, third = served
return [
self.get_queryset().order_by("-creation_date"),
TransformedFood.objects.filter(is_ready=True, is_active=True, was_eaten=False, expiry_date__lt=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, TransformedFood, "view"))
.distinct()
.order_by("-creation_date"),
TransformedFood.objects.filter(is_ready=True, is_active=True, was_eaten=False, expiry_date__gte=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, TransformedFood, "view"))
.distinct()
.order_by("-creation_date")
]
class BasicFoodDetailView(FoodDetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
fields = ['arrival_date', 'date_type']
for field in fields: # We choose a club which should work
context["fields"].append(( for membership in self.request.user.memberships.all():
BasicFood._meta.get_field(field).verbose_name.capitalize(), club_id = membership.club.id
getattr(self.object, field) food = TransformedFood(
)) name="",
owner_id=club_id,
creation_date=timezone.now(),
expiry_date=timezone.now(),
)
if PermissionBackend.check_perm(self.request, "food.add_transformedfood", food):
context['can_create_meal'] = True
break
tables = context["tables"]
for name, table in zip(["table", "open", "served"], tables):
context[name] = table
return context return context
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')
return super().get(*args, **kwargs)
class TransformedFoodDetailView(FoodDetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["fields"].append((
TransformedFood._meta.get_field("creation_date").verbose_name.capitalize(),
self.object.creation_date
))
context["fields"].append((
TransformedFood._meta.get_field("shelf_life").verbose_name.capitalize(),
pretty_duration(self.object.shelf_life)
))
context["foods"] = self.object.ingredients.all()
context["manage_ingredients"] = True
return context
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')
return super().get(*args, **kwargs)

View File

@ -3307,184 +3307,452 @@
} }
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 211, "pk": 211,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"qrcode" "transformedfood"
], ],
"query": "{}", "query": "{}",
"type": "view", "type": "view",
"mask": 2, "mask": 3,
"permanent": false, "field": "",
"description": "Voir n'importe quel QR-code" "permanent": false,
} "description": "Voir tout les plats"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 212, "pk": 212,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"allergen" "transformedfood"
], ],
"query": "{}", "query": "{\"owner\": [\"club\"]}",
"type": "view", "type": "view",
"mask": 1, "mask": 3,
"permanent": false, "field": "",
"description": "Voir n'importe quel allergène" "permanent": false,
} "description": "Voir tout les plats de son club"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 213, "pk": 213,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"food" "transformedfood"
], ],
"query": "{}", "query": "{\"is_ready\": true, \"is_active\": true, \"was_eaten\": false}",
"type": "view", "type": "view",
"mask": 2, "mask": 1,
"permanent": false, "field": "",
"description": "Voir n'importe quelle bouffe" "permanent": false,
} "description": "Voir les plats préparés actifs servis"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 214, "pk": 214,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"qrcode" "qrcode"
], ],
"query": "{}", "query": "{}",
"type": "add", "type": "add",
"mask": 2, "mask": 3,
"permanent": false, "field": "",
"description": "Ajouter n'importe quel QR-code" "permanent": false,
} "description": "Initialiser un QR code de traçabilité"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 215, "pk": 215,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"food" "basicfood"
], ],
"query": "{}", "query": "{\"owner\": [\"club\"]}",
"type": "add", "type": "add",
"mask": 2, "mask": 3,
"permanent": false, "field": "",
"description": "Ajouter n'importe quelle bouffe" "permanent": false,
} "description": "Créer un nouvel ingrédient pour son club"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 216, "pk": 216,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"food" "basicfood"
], ],
"query": "{}", "query": "{}",
"type": "change", "type": "add",
"mask": 2, "mask": 3,
"permanent": false, "field": "",
"description": "Modifier n'importe quelle bouffe" "permanent": false,
} "description": "Créer un nouvel ingrédient"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 217, "pk": 217,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"qrcode" "basicfood"
], ],
"query": "{\"food_container__owner\": [\"club\"]}", "query": "{}",
"type": "view", "type": "view",
"mask": 2, "mask": 3,
"permanent": false, "field": "",
"description": "Voir un QR-code lié à son club" "permanent": false,
} "description": "Voir toute la bouffe"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 218, "pk": 218,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"food" "basicfood"
], ],
"query": "{\"owner\": [\"club\"]}", "query": "{\"is_active\": true}",
"type": "view", "type": "view",
"mask": 2, "mask": 3,
"permanent": false, "field": "",
"description": "Voir la bouffe de son club" "permanent": false,
} "description": "Voir toute la bouffe active"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 219, "pk": 219,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"qrcode" "basicfood"
], ],
"query": "{\"food_container__owner\": [\"club\"]}", "query": "{\"is_active\": true, \"owner\": [\"club\"]}",
"type": "add", "type": "view",
"mask": 2, "mask": 3,
"permanent": false, "field": "",
"description": "Ajouter un QR-code pour son club" "permanent": false,
} "description": "Voir la bouffe active de son club"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 220, "pk": 220,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"food" "basicfood"
], ],
"query": "{\"owner\": [\"club\"]}", "query": "{}",
"type": "add", "type": "change",
"mask": 2, "mask": 3,
"permanent": false, "field": "",
"description": "Ajouter de la bouffe appartenant à son club" "permanent": false,
} "description": "Modifier de la bouffe"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 221, "pk": 221,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"food" "basicfood"
], ],
"query": "{\"owner\": [\"club\"]}", "query": "{\"is_active\": true, \"was_eaten\": false}",
"type": "change", "type": "change",
"mask": 2, "mask": 3,
"permanent": false, "field": "allergens",
"description": "Modifier la bouffe appartenant à son club" "permanent": false,
} "description": "Modifier les allergènes de la bouffe existante"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
"pk": 222, "pk": 222,
"fields": { "fields": {
"model": [ "model": [
"food", "food",
"food" "basicfood"
], ],
"query": "{\"end_of_life\": \"\"}", "query": "{\"is_active\": true, \"was_eaten\": false, \"owner\": [\"club\"]}",
"type": "view", "type": "change",
"mask": 1, "mask": 3,
"permanent": false, "field": "allergens",
"description": "Voir la bouffe servie" "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_active\": true}",
"type": "change",
"mask": 3,
"field": "was_eaten",
"permanent": false,
"description": "Indiquer si un plat a été mangé"
}
},
{
"model": "permission.permission",
"pk": 227,
"fields": {
"model": [
"food",
"transformedfood"
],
"query": "{\"is_active\": 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_active\": true}",
"type": "change",
"mask": 3,
"field": "is_active",
"permanent": false,
"description": "Archiver un plat"
}
},
{
"model": "permission.permission",
"pk": 229,
"fields": {
"model": [
"food",
"basicfood"
],
"query": "{\"is_active\": true}",
"type": "change",
"mask": 3,
"field": "is_active",
"permanent": false,
"description": "Archiver de la bouffe"
}
},
{
"model": "permission.permission",
"pk": 230,
"fields": {
"model": [
"food",
"transformedfood"
],
"query": "{\"is_active\": true}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir tout les plats actifs"
}
},
{
"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_active\": 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_active\": 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\"], \"is_active\": true}",
"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": {
"model": [
"food",
"food"
],
"query": "{}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir bouffe"
}
},
{
"model": "permission.permission",
"pk": 236,
"fields": {
"model": [
"food",
"food"
],
"query": "{\"is_active\": true}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir bouffe active"
}
},
{
"model": "permission.permission",
"pk": 237,
"fields": {
"model": [
"food",
"food"
],
"query": "{\"is_active\": true, \"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"
}
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
@ -3547,8 +3815,8 @@
"mask": 2, "mask": 2,
"field": "", "field": "",
"permanent": false, "permanent": false,
"description": "Créer une transaction vers la note d'un club tant que la source reste au dessus de -20 €" "description": "Créer une transaction vers la note d'un club"
} }
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
@ -4091,8 +4359,7 @@
158, 158,
159, 159,
160, 160,
212, 213
222
] ]
} }
}, },
@ -4133,11 +4400,16 @@
50, 50,
141, 141,
169, 169,
217, 212,
218, 214,
219, 215,
220, 219,
221, 222,
224,
227,
233,
234,
237,
247, 247,
258, 258,
259 259
@ -4318,7 +4590,21 @@
166, 166,
167, 167,
168, 168,
182 182,
212,
214,
215,
218,
221,
224,
226,
227,
228,
229,
230,
232,
234,
236
] ]
} }
}, },
@ -4526,7 +4812,10 @@
168, 168,
176, 176,
177, 177,
197 178,
197,
211,
244
] ]
} }
}, },
@ -4554,11 +4843,15 @@
"permissions": [ "permissions": [
137, 137,
211, 211,
212, 214,
213, 216,
214, 217,
215, 220,
216 223,
225,
231,
235,
238
] ]
} }
}, },

View File

@ -19,9 +19,8 @@ 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
* fin de vie * a été mangé (booléen)
* est prêt (booléen) * est prêt (booléen)
* consigne (pour les GCKs)
BasicFood BasicFood
~~~~~~~~~ ~~~~~~~~~
@ -41,7 +40,7 @@ Les TransformedFood correspondent aux produits préparés à la Kfet. Ils peuven
Le modèle regroupe : Le modèle regroupe :
* Durée de conservation (par défaut 3 jours) * Durée de consommation (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

View File

@ -12,7 +12,6 @@ Applications de la Note Kfet 2020
../api/index ../api/index
registration registration
logs logs
food
treasury treasury
wei wei
wrapped wrapped
@ -67,8 +66,6 @@ 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>`_ :

View File

@ -183,7 +183,6 @@ Contributeur⋅rices
* korenst1 * korenst1
* nicomarg * nicomarg
* PAC * PAC
* Quark
* ÿnérant * ÿnérant

View File

@ -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-30 11:44+0200\n" "POT-Creation-Date: 2025-03-25 11:16+0100\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"
@ -25,7 +25,7 @@ msgid "This opener already exists"
msgstr "Cette amitié existe déjà" msgstr "Cette amitié existe déjà"
#: apps/activity/apps.py:10 apps/activity/models.py:129 #: apps/activity/apps.py:10 apps/activity/models.py:129
#: apps/activity/models.py:169 apps/activity/models.py:329 #: apps/activity/models.py:169 apps/activity/models.py:328
msgid "activity" msgid "activity"
msgstr "activité" msgstr "activité"
@ -37,29 +37,29 @@ msgstr "La note du club est inactive."
msgid "The end date must be after the start date." msgid "The end date must be after the start date."
msgstr "La date de fin doit être après celle de début." msgstr "La date de fin doit être après celle de début."
#: apps/activity/forms.py:83 apps/activity/models.py:277 #: apps/activity/forms.py:83 apps/activity/models.py:276
msgid "You can't invite someone once the activity is started." msgid "You can't invite someone once the activity is started."
msgstr "" msgstr ""
"Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré." "Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré."
#: apps/activity/forms.py:86 apps/activity/models.py:280 #: apps/activity/forms.py:86 apps/activity/models.py:279
msgid "This activity is not validated yet." msgid "This activity is not validated yet."
msgstr "Cette activité n'est pas encore validée." msgstr "Cette activité n'est pas encore validée."
#: apps/activity/forms.py:96 apps/activity/models.py:288 #: apps/activity/forms.py:96 apps/activity/models.py:287
msgid "This person has been already invited 5 times this year." msgid "This person has been already invited 5 times this year."
msgstr "Cette personne a déjà été invitée 5 fois cette année." msgstr "Cette personne a déjà été invitée 5 fois cette année."
#: apps/activity/forms.py:100 apps/activity/models.py:292 #: apps/activity/forms.py:100 apps/activity/models.py:291
msgid "This person is already invited." msgid "This person is already invited."
msgstr "Cette personne est déjà invitée." msgstr "Cette personne est déjà invitée."
#: apps/activity/forms.py:104 apps/activity/models.py:296 #: apps/activity/forms.py:104 apps/activity/models.py:295
msgid "You can't invite more than 3 people to this activity." msgid "You can't invite more than 3 people to this activity."
msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
#: apps/activity/models.py:28 apps/activity/models.py:63 apps/food/models.py:18 #: apps/activity/models.py:28 apps/activity/models.py:63 apps/food/models.py:42
#: apps/food/models.py:35 apps/member/models.py:203 #: apps/food/models.py:56 apps/member/models.py:203
#: apps/member/templates/member/includes/club_info.html:4 #: apps/member/templates/member/includes/club_info.html:4
#: apps/member/templates/member/includes/profile_info.html:4 #: apps/member/templates/member/includes/profile_info.html:4
#: apps/note/models/notes.py:263 apps/note/models/transactions.py:26 #: apps/note/models/notes.py:263 apps/note/models/transactions.py:26
@ -121,7 +121,7 @@ msgid "type"
msgstr "type" msgstr "type"
#: apps/activity/models.py:91 apps/logs/models.py:22 apps/member/models.py:325 #: apps/activity/models.py:91 apps/logs/models.py:22 apps/member/models.py:325
#: apps/note/models/notes.py:148 apps/treasury/models.py:294 #: apps/note/models/notes.py:148 apps/treasury/models.py:293
#: apps/wei/models.py:171 apps/wei/templates/wei/attribute_bus_1A.html:13 #: apps/wei/models.py:171 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15 #: apps/wei/templates/wei/survey.html:15
msgid "user" msgid "user"
@ -205,21 +205,21 @@ msgstr "Entrée de la note {note} pour l'activité « {activity} »"
msgid "Already entered on " msgid "Already entered on "
msgstr "Déjà rentré·e le " msgstr "Déjà rentré·e le "
#: apps/activity/models.py:205 apps/activity/tables.py:58 #: apps/activity/models.py:204 apps/activity/tables.py:58
msgid "{:%Y-%m-%d %H:%M:%S}" msgid "{:%Y-%m-%d %H:%M:%S}"
msgstr "{:%d/%m/%Y %H:%M:%S}" msgstr "{:%d/%m/%Y %H:%M:%S}"
#: apps/activity/models.py:213 #: apps/activity/models.py:212
msgid "The balance is negative." msgid "The balance is negative."
msgstr "La note est en négatif." msgstr "La note est en négatif."
#: apps/activity/models.py:243 #: apps/activity/models.py:242
#: apps/treasury/templates/treasury/sogecredit_detail.html:14 #: apps/treasury/templates/treasury/sogecredit_detail.html:14
#: apps/wei/templates/wei/attribute_bus_1A.html:16 #: apps/wei/templates/wei/attribute_bus_1A.html:16
msgid "last name" msgid "last name"
msgstr "nom de famille" msgstr "nom de famille"
#: apps/activity/models.py:248 #: apps/activity/models.py:247
#: apps/member/templates/member/includes/profile_info.html:4 #: apps/member/templates/member/includes/profile_info.html:4
#: apps/registration/templates/registration/future_profile_detail.html:16 #: apps/registration/templates/registration/future_profile_detail.html:16
#: apps/treasury/templates/treasury/sogecredit_detail.html:17 #: apps/treasury/templates/treasury/sogecredit_detail.html:17
@ -228,36 +228,36 @@ msgstr "nom de famille"
msgid "first name" msgid "first name"
msgstr "prénom" msgstr "prénom"
#: apps/activity/models.py:253 #: apps/activity/models.py:252
msgid "school" msgid "school"
msgstr "école" msgstr "école"
#: apps/activity/models.py:260 #: apps/activity/models.py:259
msgid "inviter" msgid "inviter"
msgstr "hôte" msgstr "hôte"
#: apps/activity/models.py:264 #: apps/activity/models.py:263
msgid "guest" msgid "guest"
msgstr "invité·e" msgstr "invité·e"
#: apps/activity/models.py:265 #: apps/activity/models.py:264
msgid "guests" msgid "guests"
msgstr "invité·e·s" msgstr "invité·e·s"
#: apps/activity/models.py:318 #: apps/activity/models.py:317
msgid "Invitation" msgid "Invitation"
msgstr "Invitation" msgstr "Invitation"
#: apps/activity/models.py:336 apps/activity/models.py:340 #: apps/activity/models.py:335 apps/activity/models.py:339
msgid "Opener" msgid "Opener"
msgstr "Ouvreur⋅se" msgstr "Ouvreur⋅se"
#: apps/activity/models.py:341 #: apps/activity/models.py:340
#: apps/activity/templates/activity/activity_detail.html:16 #: apps/activity/templates/activity/activity_detail.html:16
msgid "Openers" msgid "Openers"
msgstr "Ouvreur⋅ses" msgstr "Ouvreur⋅ses"
#: apps/activity/models.py:345 #: apps/activity/models.py:344
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
#| msgid "Entry for {note} to the activity {activity}" #| msgid "Entry for {note} to the activity {activity}"
msgid "{opener} is opener of activity {acivity}" msgid "{opener} is opener of activity {acivity}"
@ -285,7 +285,7 @@ msgstr "Entré·e le "
msgid "remove" msgid "remove"
msgstr "supprimer" msgstr "supprimer"
#: apps/activity/tables.py:84 apps/note/forms.py:69 apps/treasury/models.py:209 #: apps/activity/tables.py:84 apps/note/forms.py:69 apps/treasury/models.py:208
msgid "Type" msgid "Type"
msgstr "Type" msgstr "Type"
@ -382,10 +382,10 @@ msgid "Entry done!"
msgstr "Entrée effectuée !" 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/add_ingredient_form.html:16
#: apps/food/templates/food/manage_ingredients.html:48 #: apps/food/templates/food/basicfood_form.html:16
#: apps/food/templates/food/qrcode.html:18 #: apps/food/templates/food/create_qrcode_form.html:20
#: apps/food/templates/food/transformedfood_update.html:45 #: apps/food/templates/food/transformedfood_form.html:16
#: 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
@ -493,309 +493,257 @@ msgstr "Entrées pour l'activité « {} »"
msgid "API" msgid "API"
msgstr "API" msgstr "API"
#: apps/food/apps.py:11 #: apps/food/apps.py:11 apps/food/models.py:105
msgid "food" msgid "food"
msgstr "bouffe" msgstr "bouffe"
#: apps/food/forms.py:49 #: apps/food/forms.py:32
msgid "Pasta METRO 5kg"
msgstr "Pâtes METRO 5kg"
#: apps/food/forms.py:53 apps/food/forms.py:81
msgid "Specific order given to GCKs"
msgstr ""
#: apps/food/forms.py:77
msgid "Lasagna"
msgstr "Lasagnes"
#: apps/food/forms.py:116
msgid "Shelf life (in hours)"
msgstr "Durée de vie (en heure)"
#: 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/forms.py:50
#: apps/food/templates/food/transformedfood_update.html:23 msgid "Pasta METRO 5kg"
#: apps/note/templates/note/transaction_form.html:132 msgstr "Pâtes METRO 5kg"
#: apps/treasury/models.py:61
msgid "Name"
msgstr "Nom"
#: apps/food/forms.py:181 #: apps/food/forms.py:100
#, fuzzy msgid "Lasagna"
#| msgid "QR-code number" msgstr "Lasagnes"
msgid "QR code number"
msgstr "numéro de QR-code"
#: apps/food/models.py:23 #: apps/food/models.py:18
msgid "Allergen"
msgstr "Allergène"
#: apps/food/models.py:24
msgid "Allergens"
msgstr "Allergènes"
#: apps/food/models.py:43
msgid "owner"
msgstr "propriétaire"
#: apps/food/models.py:49
msgid "allergens"
msgstr "allergènes"
#: apps/food/models.py:53
msgid "expiry date"
msgstr "date de péremption"
#: apps/food/models.py:59
msgid "end of life"
msgstr "fin de vie"
#: apps/food/models.py:64
msgid "is ready"
msgstr "est prêt"
#: apps/food/models.py:70
msgid "order"
msgstr "consigne"
#: apps/food/models.py:107 apps/food/views.py:34
#: note_kfet/templates/base.html:72
msgid "Food"
msgstr "Bouffe"
#: apps/food/models.py:108
msgid "Foods"
msgstr "Bouffes"
#: apps/food/models.py:117
msgid "arrival date"
msgstr "date d'arrivée"
#: apps/food/models.py:169
msgid "Basic food"
msgstr "Aliment basique"
#: apps/food/models.py:170
msgid "Basic foods"
msgstr "Aliments basiques"
#: apps/food/models.py:182
msgid "creation date"
msgstr "date de création"
#: apps/food/models.py:188
msgid "shelf life"
msgstr "durée de vie"
#: apps/food/models.py:196
msgid "transformed ingredient"
msgstr "ingrédients tranformées"
#: apps/food/models.py:258
msgid "Transformed food"
msgstr "Aliment transformé"
#: apps/food/models.py:259
msgid "Transformed foods"
msgstr "Aliments transformés"
#: apps/food/models.py:271
msgid "qr code number"
msgstr "numéro de QR-code"
#: apps/food/models.py:278
msgid "food container"
msgstr "récipient"
#: apps/food/models.py:282
msgid "QR-code"
msgstr "QR-code"
#: apps/food/models.py:283
msgid "QR-codes"
msgstr "QR-codes"
#: 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"
#: apps/food/templates/food/food_detail.html:19 #: apps/food/models.py:26
msgid "Contained in" msgid "food container"
msgstr "Contenu dans" msgstr "récipient"
#: apps/food/templates/food/food_detail.html:26 #: apps/food/models.py:30
msgid "Contain" msgid "QR-code"
msgstr "Contient" msgstr "QR-code"
#: apps/food/templates/food/food_detail.html:35 #: apps/food/models.py:31
msgid "Update" msgid "QR-codes"
msgstr "Modifier" msgstr "QR-codes"
#: apps/food/templates/food/food_detail.html:40 #: apps/food/models.py:34
msgid "Add to a meal" #, python-brace-format
msgstr "Ajouter à un plat" msgid "QR-code number {qr_code_number}"
msgstr "numéro du QR-code {qr_code_number}"
#: apps/food/templates/food/food_detail.html:45 #: apps/food/models.py:47
msgid "Manage ingredients" msgid "Allergen"
msgstr "Gérer les ingrédients" msgstr "Allergène"
#: apps/food/templates/food/food_detail.html:49 #: apps/food/models.py:48 apps/food/templates/food/basicfood_detail.html:17
msgid "Return to the food list" #: apps/food/templates/food/transformedfood_detail.html:20
msgstr "Retour à la liste de nourriture" msgid "Allergens"
msgstr "Allergènes"
#: apps/food/templates/food/food_list.html:14 #: apps/food/models.py:64
msgid "Meal served" msgid "owner"
msgstr "Plat servis" msgstr "propriétaire"
#: apps/food/templates/food/food_list.html:19 #: apps/food/models.py:70
msgid "New meal" msgid "allergen"
msgstr "Nouveau plat" msgstr "allergène"
#: apps/food/templates/food/food_list.html:28 #: apps/food/models.py:74
msgid "There is no meal served." msgid "expiry date"
msgstr "Il n'y a pas de plat servi." msgstr "date de péremption"
#: apps/food/templates/food/food_list.html:35 #: apps/food/models.py:80
msgid "Free food" msgid "was eaten"
msgstr "Open" msgstr "a été mangé"
#: apps/food/templates/food/food_list.html:42 #: apps/food/models.py:89
msgid "There is no free food." msgid "is ready"
msgstr "Il n'y a pas de bouffe en open" msgstr "est prêt"
#: apps/food/templates/food/food_list.html:50 #: apps/food/models.py:94
msgid "Food of your clubs" msgid "is active"
msgstr "Bouffe de tes clubs" msgstr "est en cours"
#: apps/food/templates/food/food_list.html:56 #: apps/food/models.py:106
msgid "Food of club" msgid "foods"
msgstr "Bouffe du club" msgstr "bouffes"
#: apps/food/templates/food/food_list.html:63 #: apps/food/models.py:122
msgid "Yours club has not food yet." msgid "arrival date"
msgstr "Ton club n'a pas de bouffe pour l'instant" msgstr "date d'arrivée"
#: apps/food/templates/food/manage_ingredients.html:45 #: apps/food/models.py:152
#: apps/food/templates/food/transformedfood_update.html:42 msgid "Basic food"
msgid "Add ingredient" msgstr "Aliment basique"
msgstr "Ajouter un ingrédient"
#: apps/food/templates/food/manage_ingredients.html:46 #: apps/food/models.py:153
#: apps/food/templates/food/transformedfood_update.html:43 msgid "Basic foods"
msgid "Remove ingredient" msgstr "Aliments basiques"
msgstr "Enlever un ingrédient"
#: apps/food/templates/food/qrcode.html:22 #: apps/food/models.py:161
msgid "Copy constructor" msgid "creation date"
msgstr "Constructeur de copie" msgstr "date de création"
#: apps/food/templates/food/qrcode.html:23 #: apps/food/models.py:169
msgid "New food" msgid "transformed ingredient"
msgstr "Nouvel aliment" msgstr "ingrédients tranformées"
#: apps/food/templates/food/qrcode.html:32 #: apps/food/models.py:174
msgid "shelf life"
msgstr "durée de vie"
#: apps/food/models.py:225 apps/food/views.py:375
msgid "Transformed food"
msgstr "Aliment transformé"
#: apps/food/models.py:226
msgid "Transformed foods"
msgstr "Aliments transformés"
#: apps/food/templates/food/basicfood_detail.html:14
#: apps/food/templates/food/create_qrcode_form.html:31
#: apps/food/templates/food/qrcode_detail.html:15
#: apps/food/templates/food/transformedfood_detail.html:14
msgid "Owner" msgid "Owner"
msgstr "Propriétaire" msgstr "Propriétaire"
#: apps/food/templates/food/qrcode.html:35 #: apps/food/templates/food/basicfood_detail.html:15
#: apps/food/templates/food/create_qrcode_form.html:34
msgid "Arrival date"
msgstr "Date d'arrivée"
#: apps/food/templates/food/basicfood_detail.html:16
#: apps/food/templates/food/create_qrcode_form.html:37
#: apps/food/templates/food/qrcode_detail.html:16
#: apps/food/templates/food/transformedfood_detail.html:19
msgid "Expiry date" msgid "Expiry date"
msgstr "Date de péremption" msgstr "Date de péremption"
#: apps/food/utils.py:6 #: apps/food/templates/food/basicfood_detail.html:24
msgid "second" #: apps/food/templates/food/transformedfood_detail.html:36
msgstr "seconde" msgid "Active"
msgstr "Actif"
#: apps/food/utils.py:6 #: apps/food/templates/food/basicfood_detail.html:25
msgid "seconds" #: apps/food/templates/food/transformedfood_detail.html:37
msgstr "secondes" msgid "Eaten"
msgstr "Mangé"
#: apps/food/utils.py:7 #: apps/food/templates/food/basicfood_detail.html:28
msgid "minute" #: apps/food/templates/food/qrcode_detail.html:20
msgstr "minute" #: apps/food/templates/food/qrcode_detail.html:24
#: apps/food/templates/food/transformedfood_detail.html:41
msgid "Update"
msgstr "Modifier"
#: apps/food/utils.py:7 #: apps/food/templates/food/basicfood_detail.html:32
msgid "minutes" #: apps/food/templates/food/qrcode_detail.html:34
msgstr "minutes" #: apps/food/templates/food/transformedfood_detail.html:46
msgid "Add to a meal"
msgstr "Ajouter à un plat"
#: apps/food/utils.py:8 #: apps/food/templates/food/create_qrcode_form.html:15
msgid "hour" msgid "New basic food"
msgstr "heure" msgstr "Nouvel aliment basique"
#: apps/food/utils.py:8 #: apps/food/templates/food/create_qrcode_form.html:23
msgid "hours" msgid "Copy constructor"
msgstr "heures" msgstr "Constructeur de copie"
#: apps/food/utils.py:9 #: apps/food/templates/food/create_qrcode_form.html:28
msgid "day" #: apps/food/templates/food/qrcode_detail.html:14
msgstr "jour" #: apps/note/templates/note/transaction_form.html:132
#: apps/treasury/models.py:60
msgid "Name"
msgstr "Nom"
#: apps/food/utils.py:9 apps/member/templates/member/includes/club_info.html:27 #: apps/food/templates/food/qrcode_detail.html:10
msgid "days" msgid "number"
msgstr "jours" msgstr "numéro"
#: apps/food/utils.py:10 #: apps/food/templates/food/qrcode_detail.html:29
msgid "week" msgid "View details"
msgstr "semaine" msgstr "Voir plus"
#: apps/food/utils.py:10 #: apps/food/templates/food/transformedfood_detail.html:16
msgid "weeks" #: apps/food/templates/food/transformedfood_detail.html:35
msgstr "semaines" msgid "Ready"
msgstr "Prêt"
#: apps/food/utils.py:53 #: apps/food/templates/food/transformedfood_detail.html:18
msgid "and" msgid "Creation date"
msgstr "et" msgstr "Date de création"
#: apps/food/views.py:118 #: apps/food/templates/food/transformedfood_detail.html:27
msgid "Add a new QRCode" msgid "Ingredients"
msgstr "Ajouter un nouveau QR-code" msgstr "Ingrédients"
#: apps/food/views.py:167 #: apps/food/templates/food/transformedfood_detail.html:34
msgid "Add an aliment" msgid "Shelf life"
msgstr "Ajouter un nouvel aliment" msgstr "Durée de vie"
#: apps/food/views.py:226 #: apps/food/templates/food/transformedfood_list.html:11
msgid "Add a meal" msgid "Meal served"
msgstr "Ajouter un plat" msgstr "Plat servis"
#: apps/food/views.py:262 #: apps/food/templates/food/transformedfood_list.html:16
msgid "Manage ingredients of:" msgid "New meal"
msgstr "Gestion des ingrédienrs de :" msgstr "Nouveau plat"
#: apps/food/views.py:276 apps/food/views.py:284 #: apps/food/templates/food/transformedfood_list.html:25
#, python-brace-format msgid "There is no meal served."
msgid "Fully used in {meal}" msgstr "Il n'y a pas de plat servi."
msgstr "Aliment entièrement utilisé dans : {meal}"
#: apps/food/views.py:323 #: apps/food/templates/food/transformedfood_list.html:33
msgid "Add the ingredient:" msgid "Open"
msgstr "Ajouter l'ingrédient" msgstr "Open"
#: apps/food/views.py:349 #: apps/food/templates/food/transformedfood_list.html:40
#, python-brace-format msgid "There is no free meal."
msgid "Food fully used in : {meal.name}" msgstr "Il n'y a pas de plat en open"
msgstr "Aliment entièrement utilisé dans : {meal.name}"
#: apps/food/views.py:368 #: apps/food/templates/food/transformedfood_list.html:48
msgid "All meals"
msgstr "Tout les plats"
#: apps/food/templates/food/transformedfood_list.html:55
msgid "There is no meal."
msgstr "Il n'y a pas de plat"
#: apps/food/views.py:28
msgid "Add the ingredient"
msgstr "Ajouter un ingrédient"
#: apps/food/views.py:42
msgid "The product is already prepared"
msgstr "Le produit est déjà prêt"
#: apps/food/views.py:70
msgid "Update an aliment" msgid "Update an aliment"
msgstr "Modifier un aliment" msgstr "Modifier un aliment"
#: apps/food/views.py:416 #: apps/food/views.py:97
msgid "Details of:" msgid "Details of:"
msgstr "Détails de :" msgstr "Détails de:"
#: apps/food/views.py:426 apps/treasury/tables.py:149 #: apps/food/views.py:121
msgid "Yes" msgid "Add a new basic food with QRCode"
msgstr "Oui" msgstr "Ajouter un nouvel ingrédient avec un QR-code"
#: apps/food/views.py:428 apps/member/models.py:99 apps/treasury/tables.py:149 #: apps/food/views.py:194
msgid "No" msgid "Add a new QRCode"
msgstr "Non" msgstr "Ajouter un nouveau QR-code"
#: apps/food/views.py:245
msgid "QRCode"
msgstr "QR-code"
#: apps/food/views.py:281
msgid "Add a new meal"
msgstr "Ajouter un nouveau plat"
#: apps/food/views.py:347
msgid "Update a meal"
msgstr "Modifier le plat"
#: apps/logs/apps.py:11 #: apps/logs/apps.py:11
msgid "Logs" msgid "Logs"
@ -1096,6 +1044,10 @@ msgstr "payé⋅e"
msgid "Tells if the user receive a salary." msgid "Tells if the user receive a salary."
msgstr "Indique si l'utilisateur⋅rice perçoit un salaire." msgstr "Indique si l'utilisateur⋅rice perçoit un salaire."
#: apps/member/models.py:99 apps/treasury/tables.py:149
msgid "No"
msgstr "Non"
#: apps/member/models.py:100 #: apps/member/models.py:100
msgid "Yes (receive them in french)" msgid "Yes (receive them in french)"
msgstr "Oui (les recevoir en français)" msgstr "Oui (les recevoir en français)"
@ -1405,6 +1357,10 @@ msgstr "Il n'y a pas d'adhésion trouvée avec cette entrée."
msgid "Club Parent" msgid "Club Parent"
msgstr "Club parent" msgstr "Club parent"
#: apps/member/templates/member/includes/club_info.html:27
msgid "days"
msgstr "jours"
#: apps/member/templates/member/includes/club_info.html:31 #: apps/member/templates/member/includes/club_info.html:31
#: apps/wei/templates/wei/base.html:40 #: apps/wei/templates/wei/base.html:40
msgid "membership fee" msgid "membership fee"
@ -1500,11 +1456,11 @@ msgstr "Introspection :"
msgid "Show my applications" msgid "Show my applications"
msgstr "Voir mes applications" msgstr "Voir mes applications"
#: apps/member/templates/member/picture_update.html:40 #: apps/member/templates/member/picture_update.html:38
msgid "Nevermind" msgid "Nevermind"
msgstr "Annuler" msgstr "Annuler"
#: apps/member/templates/member/picture_update.html:41 #: apps/member/templates/member/picture_update.html:39
msgid "Crop and upload" msgid "Crop and upload"
msgstr "Recadrer et envoyer" msgstr "Recadrer et envoyer"
@ -1943,7 +1899,7 @@ msgstr "Ce champ est requis."
msgid "membership transaction" msgid "membership transaction"
msgstr "transaction d'adhésion" msgstr "transaction d'adhésion"
#: apps/note/models/transactions.py:381 apps/treasury/models.py:301 #: apps/note/models/transactions.py:381 apps/treasury/models.py:300
msgid "membership transactions" msgid "membership transactions"
msgstr "transactions d'adhésion" msgstr "transactions d'adhésion"
@ -2556,7 +2512,7 @@ msgstr "Invalider l'inscription"
msgid "Treasury" msgid "Treasury"
msgstr "Trésorerie" msgstr "Trésorerie"
#: apps/treasury/forms.py:26 apps/treasury/models.py:113 #: apps/treasury/forms.py:26 apps/treasury/models.py:112
#: apps/treasury/templates/treasury/invoice_form.html:22 #: apps/treasury/templates/treasury/invoice_form.html:22
msgid "This invoice is locked and can no longer be edited." msgid "This invoice is locked and can no longer be edited."
msgstr "Cette facture est verrouillée et ne peut plus être éditée." msgstr "Cette facture est verrouillée et ne peut plus être éditée."
@ -2569,7 +2525,7 @@ msgstr "La remise est déjà fermée."
msgid "You can't change the type of the remittance." msgid "You can't change the type of the remittance."
msgstr "Vous ne pouvez pas changer le type de la remise." msgstr "Vous ne pouvez pas changer le type de la remise."
#: apps/treasury/forms.py:125 apps/treasury/models.py:276 #: apps/treasury/forms.py:125 apps/treasury/models.py:275
#: apps/treasury/tables.py:99 apps/treasury/tables.py:108 #: apps/treasury/tables.py:99 apps/treasury/tables.py:108
#: apps/treasury/templates/treasury/invoice_list.html:16 #: apps/treasury/templates/treasury/invoice_list.html:16
#: apps/treasury/templates/treasury/remittance_list.html:16 #: apps/treasury/templates/treasury/remittance_list.html:16
@ -2585,139 +2541,139 @@ msgstr "Pas de remise associée"
msgid "Invoice identifier" msgid "Invoice identifier"
msgstr "Numéro de facture" msgstr "Numéro de facture"
#: apps/treasury/models.py:43 apps/wrapped/models.py:28 #: apps/treasury/models.py:42 apps/wrapped/models.py:28
#: apps/wrapped/models.py:29 #: apps/wrapped/models.py:29
msgid "BDE" msgid "BDE"
msgstr "BDE" msgstr "BDE"
#: apps/treasury/models.py:47 #: apps/treasury/models.py:46
msgid "Quotation" msgid "Quotation"
msgstr "Devis" msgstr "Devis"
#: apps/treasury/models.py:52 #: apps/treasury/models.py:51
msgid "Object" msgid "Object"
msgstr "Objet" msgstr "Objet"
#: apps/treasury/models.py:56 #: apps/treasury/models.py:55
msgid "Description" msgid "Description"
msgstr "Description" msgstr "Description"
#: apps/treasury/models.py:65 #: apps/treasury/models.py:64
msgid "Address" msgid "Address"
msgstr "Adresse" msgstr "Adresse"
#: apps/treasury/models.py:70 apps/treasury/models.py:203 #: apps/treasury/models.py:69 apps/treasury/models.py:202
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
#: apps/treasury/models.py:76 #: apps/treasury/models.py:75
msgid "Payment date" msgid "Payment date"
msgstr "Date de paiement" msgstr "Date de paiement"
#: apps/treasury/models.py:80 #: apps/treasury/models.py:79
msgid "Acquitted" msgid "Acquitted"
msgstr "Acquittée" msgstr "Acquittée"
#: apps/treasury/models.py:85 #: apps/treasury/models.py:84
msgid "Locked" msgid "Locked"
msgstr "Verrouillée" msgstr "Verrouillée"
#: apps/treasury/models.py:86 #: apps/treasury/models.py:85
msgid "An invoice can't be edited when it is locked." msgid "An invoice can't be edited when it is locked."
msgstr "Une facture ne peut plus être modifiée si elle est verrouillée." msgstr "Une facture ne peut plus être modifiée si elle est verrouillée."
#: apps/treasury/models.py:92 #: apps/treasury/models.py:91
msgid "tex source" msgid "tex source"
msgstr "fichier TeX source" msgstr "fichier TeX source"
#: apps/treasury/models.py:96 apps/treasury/models.py:141 #: apps/treasury/models.py:95 apps/treasury/models.py:140
msgid "invoice" msgid "invoice"
msgstr "facture" msgstr "facture"
#: apps/treasury/models.py:97 #: apps/treasury/models.py:96
msgid "invoices" msgid "invoices"
msgstr "factures" msgstr "factures"
#: apps/treasury/models.py:100 #: apps/treasury/models.py:99
#, python-brace-format #, python-brace-format
msgid "Invoice #{id}" msgid "Invoice #{id}"
msgstr "Facture n°{id}" msgstr "Facture n°{id}"
#: apps/treasury/models.py:146 #: apps/treasury/models.py:145
msgid "Designation" msgid "Designation"
msgstr "Désignation" msgstr "Désignation"
#: apps/treasury/models.py:152 #: apps/treasury/models.py:151
msgid "Quantity" msgid "Quantity"
msgstr "Quantité" msgstr "Quantité"
#: apps/treasury/models.py:157 #: apps/treasury/models.py:156
msgid "Unit price" msgid "Unit price"
msgstr "Prix unitaire" msgstr "Prix unitaire"
#: apps/treasury/models.py:161 #: apps/treasury/models.py:160
msgid "product" msgid "product"
msgstr "produit" msgstr "produit"
#: apps/treasury/models.py:162 #: apps/treasury/models.py:161
msgid "products" msgid "products"
msgstr "produits" msgstr "produits"
#: apps/treasury/models.py:190 #: apps/treasury/models.py:189
msgid "remittance type" msgid "remittance type"
msgstr "type de remise" msgstr "type de remise"
#: apps/treasury/models.py:191 #: apps/treasury/models.py:190
msgid "remittance types" msgid "remittance types"
msgstr "types de remises" msgstr "types de remises"
#: apps/treasury/models.py:214 #: apps/treasury/models.py:213
msgid "Comment" msgid "Comment"
msgstr "Commentaire" msgstr "Commentaire"
#: apps/treasury/models.py:219 #: apps/treasury/models.py:218
msgid "Closed" msgid "Closed"
msgstr "Fermée" msgstr "Fermée"
#: apps/treasury/models.py:223 #: apps/treasury/models.py:222
msgid "remittance" msgid "remittance"
msgstr "remise" msgstr "remise"
#: apps/treasury/models.py:224 #: apps/treasury/models.py:223
msgid "remittances" msgid "remittances"
msgstr "remises" msgstr "remises"
#: apps/treasury/models.py:227 #: apps/treasury/models.py:226
msgid "Remittance #{:d}: {}" msgid "Remittance #{:d}: {}"
msgstr "Remise n°{:d} : {}" msgstr "Remise n°{:d} : {}"
#: apps/treasury/models.py:280 #: apps/treasury/models.py:279
msgid "special transaction proxy" msgid "special transaction proxy"
msgstr "proxy de transaction spéciale" msgstr "proxy de transaction spéciale"
#: apps/treasury/models.py:281 #: apps/treasury/models.py:280
msgid "special transaction proxies" msgid "special transaction proxies"
msgstr "proxys de transactions spéciales" msgstr "proxys de transactions spéciales"
#: apps/treasury/models.py:307 #: apps/treasury/models.py:306
msgid "credit transaction" msgid "credit transaction"
msgstr "transaction de crédit" msgstr "transaction de crédit"
#: apps/treasury/models.py:312 #: apps/treasury/models.py:311
#: apps/treasury/templates/treasury/sogecredit_detail.html:10 #: apps/treasury/templates/treasury/sogecredit_detail.html:10
msgid "Credit from the Société générale" msgid "Credit from the Société générale"
msgstr "Crédit de la Société générale" msgstr "Crédit de la Société générale"
#: apps/treasury/models.py:313 #: apps/treasury/models.py:312
msgid "Credits from the Société générale" msgid "Credits from the Société générale"
msgstr "Crédits de la Société générale" msgstr "Crédits de la Société générale"
#: apps/treasury/models.py:316 #: apps/treasury/models.py:315
#, python-brace-format #, python-brace-format
msgid "Soge credit for {user}" msgid "Soge credit for {user}"
msgstr "Crédit de la société générale pour l'utilisateur·rice {user}" msgstr "Crédit de la société générale pour l'utilisateur·rice {user}"
#: apps/treasury/models.py:446 #: apps/treasury/models.py:445
msgid "" msgid ""
"This user doesn't have enough money to pay the memberships with its note. " "This user doesn't have enough money to pay the memberships with its note. "
"Please ask her/him to credit the note before invalidating this credit." "Please ask her/him to credit the note before invalidating this credit."
@ -2746,6 +2702,10 @@ msgstr "Nombre de transactions"
msgid "View" msgid "View"
msgstr "Voir" msgstr "Voir"
#: apps/treasury/tables.py:149
msgid "Yes"
msgstr "Oui"
#: apps/treasury/templates/treasury/invoice_confirm_delete.html:10 #: apps/treasury/templates/treasury/invoice_confirm_delete.html:10
#: apps/treasury/views.py:174 #: apps/treasury/views.py:174
msgid "Delete invoice" msgid "Delete invoice"
@ -3852,6 +3812,10 @@ msgstr ""
msgid "Reset" msgid "Reset"
msgstr "Réinitialiser" msgstr "Réinitialiser"
#: note_kfet/templates/base.html:72
msgid "Food"
msgstr "Bouffe"
#: note_kfet/templates/base.html:84 #: note_kfet/templates/base.html:84
msgid "Users" msgid "Users"
msgstr "Utilisateur·rices" msgstr "Utilisateur·rices"
@ -4161,67 +4125,6 @@ msgstr ""
"d'adhésion. Vous devez également valider votre adresse email en suivant le " "d'adhésion. Vous devez également valider votre adresse email en suivant le "
"lien que vous avez reçu." "lien que vous avez reçu."
#, python-brace-format
#~ msgid "QR-code number {qr_code_number}"
#~ msgstr "numéro du QR-code {qr_code_number}"
#~ msgid "was eaten"
#~ msgstr "a été mangé"
#~ msgid "is active"
#~ msgstr "est en cours"
#~ msgid "foods"
#~ msgstr "bouffes"
#~ msgid "Arrival date"
#~ msgstr "Date d'arrivée"
#~ msgid "Active"
#~ msgstr "Actif"
#~ msgid "Eaten"
#~ msgstr "Mangé"
#~ msgid "number"
#~ msgstr "numéro"
#~ msgid "View details"
#~ msgstr "Voir plus"
#~ msgid "Ready"
#~ msgstr "Prêt"
#~ msgid "Creation date"
#~ msgstr "Date de création"
#~ msgid "Ingredients"
#~ msgstr "Ingrédients"
#~ msgid "Open"
#~ msgstr "Open"
#~ msgid "All meals"
#~ msgstr "Tout les plats"
#~ msgid "There is no meal."
#~ msgstr "Il n'y a pas de plat"
#~ msgid "The product is already prepared"
#~ msgstr "Le produit est déjà prêt"
#~ msgid "Add a new basic food with QRCode"
#~ msgstr "Ajouter un nouvel ingrédient avec un QR-code"
#~ msgid "QRCode"
#~ msgstr "QR-code"
#~ msgid "Add a new meal"
#~ msgstr "Ajouter un nouveau plat"
#~ msgid "Update a meal"
#~ msgstr "Modifier le plat"
#, fuzzy #, fuzzy
#~| msgid "invalidate" #~| msgid "invalidate"
#~ msgid "Enter a valid color." #~ msgid "Enter a valid color."
@ -4277,6 +4180,11 @@ msgstr ""
#~ msgid "Enter a number." #~ msgid "Enter a number."
#~ msgstr "numéro de téléphone" #~ msgstr "numéro de téléphone"
#, fuzzy
#~| msgid "add"
#~ msgid "and"
#~ msgstr "ajouter"
#, fuzzy, python-format #, fuzzy, python-format
#~| msgid "A template with this name already exist" #~| msgid "A template with this name already exist"
#~ msgid "%(model_name)s with this %(field_labels)s already exists." #~ msgid "%(model_name)s with this %(field_labels)s already exists."
@ -4538,6 +4446,9 @@ msgstr ""
#~ msgid "Free" #~ msgid "Free"
#~ msgstr "Open" #~ msgstr "Open"
#~ msgid "Add a new aliment"
#~ msgstr "Ajouter un nouvel aliment"
#, fuzzy #, fuzzy
#~| msgid "Transformed food" #~| msgid "Transformed food"
#~ msgid "New transformed food" #~ msgid "New transformed food"

96
note_kfet/static/css/custom.css Executable file → Normal file
View File

@ -61,20 +61,16 @@ mark {
/* Make navbar more readable */ /* Make navbar more readable */
.navbar-dark .navbar-nav .nav-link { .navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, .75); color: rgba(255, 255, 255, .75);
text-shadow: 2px 2px 15px #ffeb40; /* text-shadow: 2px 2px 15px #ffeb40; */
} }
.navbar-brand { /* .navbar-brand { */
text-shadow: 2px 2px 15px #ffeb40; /* text-shadow: 2px 2px 15px #ffeb40; */
} /* }
/* Last BDE colors */ /* Last BDE colors */
.bg-primary { .bg-primary {
/* background-color: rgb(18, 67, 4) !important; */ background-color: rgb(102, 83, 105) !important;
/* MODE VIEUXCON=ON */
/* background-color: rgb(166, 0, 2) !important; */
background-color: rgb(0, 0, 0);
background-image: url('/static/img/rp_bg.png');
} }
html { html {
@ -89,94 +85,52 @@ body {
.btn-outline-primary:hover, .btn-outline-primary:hover,
.btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled).active,
.btn-outline-primary:not(:disabled):not(.disabled):active { .btn-outline-primary:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0); color: #fff;
background-color: rgb(255, 0, 101); background-color: rgb(102, 83, 105);
border-color: rgb(255, 203, 32); border-color: rgb(102, 83, 105);
} }
.btn-outline-primary { .btn-outline-primary {
color: #000; color: rgb(102, 83, 105);
background-color: #ffcb20; background-color: rgba(248, 249, 250, 0.9);
border-color: #000; border-color: rgb(102, 83, 105);
} }
.turbolinks-progress-bar { .turbolinks-progress-bar {
background-color: #ffffff; background-color: #12432E;
} }
.btn-primary:hover, .btn-primary:hover,
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled).active,
.btn-primary:not(:disabled):not(.disabled):active { .btn-primary:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0); color: #fff;
background-color: rgb(255, 0, 101); background-color: rgb(102, 83, 105);
border-color: rgb(255, 203, 32); border-color: rgb(102, 83, 105);
} }
.btn-primary { .btn-primary {
color: #ffcb20; color: rgba(248, 249, 250, 0.9);
background-color: #000000; background-color: rgb(102, 83, 105);
border-color: #ffcd20; border-color: rgb(102, 83, 105);
} }
.border-primary { .border-primary {
border-color: rgb(255, 255, 255) !important; border-color: rgb(115, 15, 115) !important;
}
.btn-secondary {
color: #ff0065;
background-color: #000000;
border-color: #ff0065;
}
.btn-secondary:hover,
.btn-secondary:not(:disabled):not(.disabled).active,
.btn-secondary:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0);
background-color: rgb(255, 203, 32);
border-color: rgb(255, 0, 101);
}
.btn-outline-dark-shiny {
background-color: #222;
border-color: #61605b;
color: rgba(255, 0, 101, 75%);
}
.btn-outline-dark-shiny:hover,
.btn-outline-dark-shiny:not(:disabled):not(.disabled).active,
.btn-outline-dark-shiny:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0);
background-color: rgb(255, 203, 32);
border-color: rgb(255, 0, 101);
}
.btn-outline-dark {
background-color: #222;
border-color: #61605b;
color: rgba(255, 203, 32, 75%);
}
.btn-outline-dark:hover,
.btn-outline-dark:not(:disabled):not(.disabled).active,
.btn-outline-dark:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0);
background-color: rgb(255, 0, 101);
border-color: rgb(255, 203, 32);
} }
a { a {
color: rgb(255, 0, 101); color: rgb(102, 83, 105);
} }
a:hover { a:hover {
color: rgb(255, 203, 32); color: rgb(200, 30, 200);
} }
.form-control:focus { .form-control:focus {
box-shadow: 0 0 0 0.25rem rgb(255 0 101 / 50%); box-shadow: 0 0 0 0.25rem rgba(200, 30, 200, 0.25);
border-color: rgb(255, 0, 101); border-color: rgb(200, 30, 200);
} }
.btn-outline-primary.focus { .btn-outline-primary.focus {
box-shadow: 0 0 0 0.25rem rgb(255 203 32 / 22%); box-shadow: 0 0 0 0.25rem rgba(200, 30, 200, 0.5);
}

View File

@ -96,11 +96,13 @@ function displayStyle (note) {
if (!note) { return '' } if (!note) { return '' }
const balance = note.balance const balance = note.balance
var css = '' var css = ''
var ms_per_year = 31536000000 // 365 * 24 * 3600 * 1000
if (balance < -2000) { css += ' text-danger bg-dark' } if (balance < -2000) { css += ' text-danger bg-dark' }
else if (balance < -1000) { css += ' text-danger' } else if (balance < -1000) { css += ' text-danger' }
else if (balance < 0) { css += ' text-warning' } else if (balance < 0) { css += ' text-warning' }
if (!note.email_confirmed) { css += ' bg-primary' } if (!note.email_confirmed) { css += ' bg-primary' }
else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString())) { css += ' bg-info' } else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString())) { css += ' bg-info' }
if (((Date.now() - Date.parse(note.created_at))/ms_per_year) > 2.5) { css += ' font-weight-bold underline' }
return css return css
} }

View File

@ -164,7 +164,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</nav> </nav>
<div class="{% block containertype %}container{% endblock %} my-3"> <div class="{% block containertype %}container{% endblock %} my-3">
<div id="messages"> <div id="messages">
{% if user.is_authenticated %}
<div class="alert alert-info">
Bravo pour votre diplomation les survis !
</div>
{% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if not user|is_member:"BDE" %} {% if not user|is_member:"BDE" %}
<div class="alert alert-danger"> <div class="alert alert-danger">