1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-04-30 18:41:35 +00:00

Compare commits

..

No commits in common. "6d6583bfe698ceb1dda3d0a3bb856c1b8ac838e3" and "5558341c8c479dc0abaa6c84a88345acdda3f058" have entirely different histories.

237 changed files with 5975 additions and 9919 deletions

View File

@ -7,6 +7,21 @@ stages:
variables: variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
# Debian Bullseye
py39-django42:
stage: test
image: debian:bullseye
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py39-django42
# Ubuntu 22.04 # Ubuntu 22.04
py310-django42: py310-django42:
stage: test stage: test
@ -39,6 +54,8 @@ py311-django42:
python3-bs4 python3-setuptools tox texlive-xetex python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py311-django42 script: tox -e py311-django42
linters: linters:
stage: quality-assurance stage: quality-assurance
image: debian:bookworm image: debian:bookworm

View File

@ -58,13 +58,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial (env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
``` ```
6. (Optionnel) **Création d'une clé privée OpenID Connect** 6. Enjoy :
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
7. Enjoy :
```bash ```bash
(env)$ ./manage.py runserver 0.0.0.0:8000 (env)$ ./manage.py runserver 0.0.0.0:8000
@ -234,13 +228,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
(env)$ ./manage.py check # pas de bêtise qui traine (env)$ ./manage.py check # pas de bêtise qui traine
(env)$ ./manage.py migrate (env)$ ./manage.py migrate
7. **Création d'une clé privée OpenID Connect** 7. *Enjoy \o/*
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
8. *Enjoy \o/*
### Installation avec Docker ### Installation avec Docker

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'activity.apps.ActivityConfig' default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# 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
@ -35,7 +35,7 @@ class GuestAdmin(admin.ModelAdmin):
""" """
Admin customisation for Guest Admin customisation for Guest
""" """
list_display = ('last_name', 'first_name', 'school', 'activity', 'inviter') list_display = ('last_name', 'first_name', 'activity', 'inviter')
form = GuestForm form = GuestForm

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.filters import RegexSafeSearchFilter from api.filters import RegexSafeSearchFilter
@ -51,9 +51,9 @@ class GuestViewSet(ReadProtectedModelViewSet):
queryset = Guest.objects.order_by('id') queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer serializer_class = GuestSerializer
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'school', 'inviter', 'inviter__alias__name', filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
'inviter__alias__normalized_name', ] 'inviter__alias__normalized_name', ]
search_fields = ['$activity__name', '$last_name', '$first_name', '$school', '$inviter__user__email', '$inviter__alias__name', search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
'$inviter__alias__normalized_name', ] '$inviter__alias__normalized_name', ]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 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 datetime import timedelta
@ -107,7 +107,7 @@ class GuestForm(forms.ModelForm):
class Meta: class Meta:
model = Guest model = Guest
fields = ('last_name', 'first_name', 'school', 'inviter', ) fields = ('last_name', 'first_name', 'inviter', )
widgets = { widgets = {
"inviter": Autocomplete( "inviter": Autocomplete(
NoteUser, NoteUser,

View File

@ -1,24 +0,0 @@
# 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 = [
('note', '0006_trust'),
('activity', '0004_opener'),
]
operations = [
migrations.AlterModelOptions(
name='opener',
options={'verbose_name': 'Opener', 'verbose_name_plural': 'Openers'},
),
migrations.AlterField(
model_name='opener',
name='opener',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_responsible', to='note.note', verbose_name='Opener'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 4.2.20 on 2025-03-25 09:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("activity", "0005_alter_opener_options_alter_opener_opener"),
]
operations = [
migrations.AddField(
model_name="guest",
name="school",
field=models.CharField(default="", max_length=255, verbose_name="school"),
preserve_default=False,
),
]

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import os import os
@ -201,8 +201,7 @@ class Entry(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists(): if qs.exists():
raise ValidationError(_("Already entered on ") raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, ))
+ _("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(qs.get().time), ))
if self.guest: if self.guest:
self.note = self.guest.inviter self.note = self.guest.inviter
@ -248,11 +247,6 @@ class Guest(models.Model):
verbose_name=_("first name"), verbose_name=_("first name"),
) )
school = models.CharField(
max_length=255,
verbose_name=_("school"),
)
inviter = models.ForeignKey( inviter = models.ForeignKey(
NoteUser, NoteUser,
on_delete=models.PROTECT, on_delete=models.PROTECT,

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone from django.utils import timezone
@ -51,11 +51,11 @@ class GuestTable(tables.Table):
} }
model = Guest model = Guest
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ("last_name", "first_name", "inviter", "school") fields = ("last_name", "first_name", "inviter", )
def render_entry(self, record): def render_entry(self, record):
if record.has_entry: if record.has_entry:
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(record.entry.time)))) return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, )))
return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> ' return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize())) '{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 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 datetime import timedelta
@ -50,7 +50,6 @@ class TestActivities(TestCase):
inviter=self.user.note, inviter=self.user.note,
last_name="GUEST", last_name="GUEST",
first_name="Guest", first_name="Guest",
school="School",
) )
def test_activity_list(self): def test_activity_list(self):
@ -157,7 +156,6 @@ class TestActivities(TestCase):
inviter=self.user.note.id, inviter=self.user.note.id,
last_name="GUEST2", last_name="GUEST2",
first_name="Guest", first_name="Guest",
school="School",
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -169,7 +167,6 @@ class TestActivities(TestCase):
inviter=self.user.note.id, inviter=self.user.note.id,
last_name="GUEST2", last_name="GUEST2",
first_name="Guest", first_name="Guest",
school="School",
)) ))
self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200) self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200)
@ -203,7 +200,6 @@ class TestActivityAPI(TestAPI):
inviter=self.user.note, inviter=self.user.note,
last_name="GUEST", last_name="GUEST",
first_name="Guest", first_name="Guest",
school="School",
) )
self.entry = Entry.objects.create( self.entry = Entry.objects.create(

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from hashlib import md5 from hashlib import md5
@ -168,7 +168,6 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
activity=activity, activity=activity,
first_name="", first_name="",
last_name="", last_name="",
school="",
inviter=self.request.user.note, inviter=self.request.user.note,
) )
@ -266,11 +265,12 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
# 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)
# Keep only valid members # Keep only 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"))
@ -330,7 +330,7 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
activities_open = Activity.objects.filter(open=True, activity_type__manage_entries=True).filter( activities_open = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all() PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
context["activities_open"] = [a for a in activities_open context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request, if PermissionBackend.check_perm(self.request,

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'api.apps.APIConfig' default_app_config = 'api.apps.APIConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import json import json

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings
@ -47,10 +47,6 @@ if "wei" in settings.INSTALLED_APPS:
from wei.api.urls import register_wei_urls from wei.api.urls import register_wei_urls
register_wei_urls(router, 'wei') register_wei_urls(router, 'wei')
if "wrapped" in settings.INSTALLED_APPS:
from wrapped.api.urls import register_wrapped_urls
register_wrapped_urls(router, 'wrapped')
app_name = 'api' app_name = 'api'
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.models import User from django.contrib.auth.models import User

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import re import re

View File

@ -1,59 +1,37 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# 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

@ -1,9 +1,9 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers from rest_framework import serializers
from ..models import Allergen, BasicFood, TransformedFood, QRCode from ..models import Allergen, BasicFood, QRCode, TransformedFood
class AllergenSerializer(serializers.ModelSerializer): class AllergenSerializer(serializers.ModelSerializer):
@ -11,6 +11,7 @@ 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__'
@ -21,26 +22,29 @@ 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-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import AllergenViewSet, BasicFoodViewSet, TransformedFoodViewSet, QRCodeViewSet from .views import AllergenViewSet, BasicFoodViewSet, QRCodeViewSet, TransformedFoodViewSet
def register_food_urls(router, path): def register_food_urls(router, path):
@ -9,6 +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 + '/basicfood', BasicFoodViewSet) router.register(path + '/basic_food', BasicFoodViewSet)
router.register(path + '/transformedfood', TransformedFoodViewSet)
router.register(path + '/qrcode', QRCodeViewSet) router.register(path + '/qrcode', QRCodeViewSet)
router.register(path + '/transformed_food', TransformedFoodViewSet)

View File

@ -1,12 +1,12 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter from rest_framework.filters import SearchFilter
from .serializers import AllergenSerializer, BasicFoodSerializer, TransformedFoodSerializer, QRCodeSerializer from .serializers import AllergenSerializer, BasicFoodSerializer, QRCodeSerializer, TransformedFoodSerializer
from ..models import Allergen, BasicFood, TransformedFood, QRCode from ..models import Allergen, BasicFood, QRCode, TransformedFood
class AllergenViewSet(ReadProtectedModelViewSet): class AllergenViewSet(ReadProtectedModelViewSet):
@ -26,7 +26,7 @@ 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
@ -35,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.
@ -59,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,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,44 +1,44 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
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 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(
is_ready=False,
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)
@ -51,103 +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',)

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

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 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 datetime import timedelta
@ -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

@ -1,21 +1,19 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# 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,48 +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 %}
<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,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 %}

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

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path
@ -8,13 +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('detail/<int:pk>', views.FoodDetailView.as_view(), name='food_view'), path('<int:slug>/create_qrcode/basic', views.QRCodeBasicFoodCreateView.as_view(), name='qrcode_basic_create'),
path('detail/basic/<int:pk>', views.BasicFoodDetailView.as_view(), name='basicfood_view'), path('create/transformed', views.TransformedFoodCreateView.as_view(), name='transformed_create'),
path('detail/transformed/<int:pk>', views.TransformedFoodDetailView.as_view(), name='transformedfood_view'), path('update/basic/<int:pk>', views.BasicFoodUpdateView.as_view(), name='basic_update'),
path('add/ingredient/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'), path('update/transformed/<int:pk>', views.TransformedFoodUpdateView.as_view(), name='transformed_update'),
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,402 +1,421 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 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 from django.http import HttpResponseRedirect
from django_tables2.views import MultiTableMixin
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django.views.generic import DetailView, UpdateView from django.views.generic import DetailView, UpdateView
from django.views.generic.list import ListView from django.views.generic.list import ListView
from django.urls import reverse_lazy from django.forms import HiddenInput
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import Club, Membership
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 AddIngredientForms, BasicFoodForms, TransformedFoodForms, BasicFoodUpdateForms, TransformedFoodUpdateForms, QRCodeForms from .models import BasicFood, Food, QRCode, TransformedFood
from .tables import FoodTable from .tables import TransformedFoodTable
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()).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) basic_food_form = BasicFoodForms(data=self.request.POST)
# tables club if not basic_food_form.is_valid():
bureau_role_pk = 4 return self.form_invalid(form)
clubs = Club.objects.filter(membership__in=Membership.objects.filter(
user=self.request.user, roles=bureau_role_pk).filter( ans = super().form_valid(form)
date_end__gte=timezone.now())) form.instance.update()
club_table = [] return ans
for club in clubs:
club_table.append(self.get_queryset().order_by('expiry_date').filter( def get_success_url(self, **kwargs):
owner=club, end_of_life='').filter( self.object.refresh_from_db()
PermissionBackend.filter_queryset(self.request, Food, 'view') return reverse('food:food_view', kwargs={"pk": self.object.pk})
))
return [search_table, open_table, served_table] + club_table def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
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): return context
context[name] = table
context['club_tables'] = tables[3:]
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 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()
context['can_add_meal'] = PermissionBackend.check_perm(self.request, 'food.transformedfood_add')
return context return context
class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView): class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
""" """
A view to add qrcode 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):
return BasicFood( qrcode = kwargs["slug"]
name="", if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
owner_id=1, return super().get(*args, **kwargs)
expiry_date=timezone.now(), else:
is_ready=True, return HttpResponseRedirect(reverse("food:qrcode_create", kwargs=kwargs))
arrival_date=timezone.now(),
date_type='DLC',
)
@transaction.atomic def get_context_data(self, **kwargs):
def form_valid(self, form): context = super().get_context_data(**kwargs)
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) qr_code_number = self.kwargs['slug']
food.is_ready = False qrcode = self.model.objects.get(qr_code_number=qr_code_number)
food.save()
food.refresh_from_db()
qrcode = QRCode() model = qrcode.food_container.polymorphic_ctype.model
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):
return TransformedFood(
name="",
owner_id=1,
expiry_date=timezone.now(),
is_ready=True,
)
@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})
class AddIngredientView(ProtectQuerysetMixin, 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()
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):
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):
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()
return context
def get(self, *args, **kwargs):
kwargs['stop_redirect'] = (Food.objects.get(pk=kwargs['pk']).polymorphic_ctype.model == 'transformedfood')
return super().get(*args, **kwargs)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'logs.apps.LogsConfig' default_app_config = 'logs.apps.LogsConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import ChangelogViewSet from .views import ChangelogViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig' default_app_config = 'member.apps.MemberConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# 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

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet from .views import ProfileViewSet, ClubViewSet, MembershipViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from cas_server.auth import DjangoAuthUser # pragma: no cover from cas_server.auth import DjangoAuthUser # pragma: no cover

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import io import io
@ -23,7 +23,7 @@ from .models import Profile, Club, Membership
class CustomAuthenticationForm(AuthenticationForm): class CustomAuthenticationForm(AuthenticationForm):
permission_mask = forms.ModelChoiceField( permission_mask = forms.ModelChoiceField(
label=_("Permission mask"), label=_("Permission mask"),
queryset=PermissionMask.objects.order_by("-rank"), queryset=PermissionMask.objects.order_by("rank"),
empty_label=None, empty_label=None,
) )
@ -44,7 +44,6 @@ class ProfileForm(forms.ModelForm):
""" """
A form for the extras field provided by the :model:`member.Profile` model. A form for the extras field provided by the :model:`member.Profile` model.
""" """
# Remove widget=forms.HiddenInput() if you want to use report frequency.
report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency")) report_frequency = forms.IntegerField(required=False, initial=0, label=_("Report frequency"))
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date")) last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
@ -77,8 +76,7 @@ class ProfileForm(forms.ModelForm):
class Meta: class Meta:
model = Profile model = Profile
fields = '__all__' fields = '__all__'
# Remove ml_[asso]_registration from exclude if the concerned association uses nk20 to manage its mailing list. exclude = ('user', 'email_confirmed', 'registration_valid', )
exclude = ('user', 'email_confirmed', 'registration_valid', 'ml_sport_registration', )
class ImageForm(forms.Form): class ImageForm(forms.Form):

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import hashlib import hashlib

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import datetime import datetime

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date from datetime import date
@ -42,12 +42,12 @@ class UserTable(tables.Table):
""" """
alias = tables.Column() alias = tables.Column()
section = tables.Column(accessor='profile__section', orderable=False) section = tables.Column(accessor='profile__section')
# Override the column to let replace the URL # Override the column to let replace the URL
email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email)) email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"), orderable=False) balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
def render_email(self, record, value): def render_email(self, record, value):
# Replace the email by a dash if the user can't see the profile detail # Replace the email by a dash if the user can't see the profile detail

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{{ title }} {{ title }}
</h3> </h3>
<div class="card-body"> <div class="card-body">
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note..."> <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note">
<div class="form-check"> <div class="form-check">
<label class="form-check-label" for="only_active"> <label class="form-check-label" for="only_active">
<input type="checkbox" class="checkboxinput form-check-input" id="only_active" <input type="checkbox" class="checkboxinput form-check-input" id="only_active"

View File

@ -20,14 +20,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
</form> </form>
</div> </div>
<!-- MODAL TO CROP THE IMAGE --> <!-- MODAL TO CROP THE IMAGE -->
<div class="modal fade" id="modalCrop" data-backdrop="static"> <div class="modal fade" id="modalCrop">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body-wrapper" style="width: 500px; height: 500px; padding: 16px;"> <div class="modal-body">
<div class="modal-body" style="width: 100%; height: 100%; padding: 0"> <img src="" id="modal-image" style="max-width: 100%;">
<img src="" id="modal-image" style="display: block; max-width: 100%;"> </div>
</div>
</div>
<div class="modal-footer"> <div class="modal-footer">
<div class="btn-group pull-left" role="group"> <div class="btn-group pull-left" role="group">
<button type="button" class="btn btn-default" id="js-zoom-in"> <button type="button" class="btn btn-default" id="js-zoom-in">

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date from datetime import date

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 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, date from datetime import timedelta, date
@ -26,7 +26,6 @@ from note_kfet.middlewares import _set_current_request
from permission.backends import PermissionBackend from permission.backends import PermissionBackend
from permission.models import Role from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from django import forms
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \ from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
CustomAuthenticationForm, MembershipRolesForm CustomAuthenticationForm, MembershipRolesForm
@ -73,24 +72,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.") form.fields['email'].help_text = _("This address must be valid.")
profile_form = self.profile_form(instance=context['user_object'].profile, if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
data=self.request.POST if self.request.POST else None) context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
data=self.request.POST if self.request.POST else None)
if not self.object.profile.report_frequency: if not self.object.profile.report_frequency:
del profile_form.fields["last_report"] del context['profile_form'].fields["last_report"]
fields_to_check = list(profile_form.fields.keys())
fields_modifiable = False
# Delete the fields for which the user does not have the permission to modify
for field_name in fields_to_check:
if not PermissionBackend.check_perm(self.request, f"member.change_profile_{field_name}", context['user_object'].profile):
profile_form.fields[field_name].widget = forms.HiddenInput()
else:
fields_modifiable = True
if fields_modifiable:
context['profile_form'] = profile_form
return context return context

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'note.apps.NoteConfig' default_app_config = 'note.apps.NoteConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# 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

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \ from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime from datetime import datetime

View File

@ -1,25 +0,0 @@
# 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'),
('note', '0006_trust'),
]
operations = [
migrations.AlterField(
model_name='note',
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'),
),
migrations.AlterField(
model_name='transaction',
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

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import unicodedata import unicodedata

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone from django.utils import timezone

View File

@ -1,4 +1,4 @@
// Copyright (C) 2018-2025 by BDE ENS Paris-Saclay // Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// When a transaction is performed, lock the interface to prevent spam clicks. // When a transaction is performed, lock the interface to prevent spam clicks.
@ -245,7 +245,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
invalidity_reason: 'Solde insuffisant', invalidity_reason: 'Solde insuffisant',
polymorphic_ctype: type, polymorphic_ctype: type,
resourcetype: 'RecurrentTransaction', resourcetype: 'RecurrentTransaction',
source: source.id, source: source,
source_alias: source_alias, source_alias: source_alias,
destination: dest, destination: dest,
template: template template: template
@ -294,10 +294,3 @@ searchbar.addEventListener("keyup", function (e) {
if (firstMatch && e.key === "Enter") if (firstMatch && e.key === "Enter")
firstMatch.click() firstMatch.click()
}); });
function createshiny() {
const list_btn = document.querySelectorAll('.btn-outline-dark')
const shiny_class = list_btn[Math.floor(Math.random() * list_btn.length)].classList
shiny_class.replace('btn-outline-dark', 'btn-outline-dark-shiny')
}
createshiny()

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import html import html
@ -260,13 +260,11 @@ class ButtonTable(tables.Table):
text=_('edit'), text=_('edit'),
accessor='pk', accessor='pk',
verbose_name=_("Edit"), verbose_name=_("Edit"),
orderable=False,
) )
hideshow = tables.Column( hideshow = tables.Column(
verbose_name=_("Hide/Show"), verbose_name=_("Hide/Show"),
accessor="pk", accessor="pk",
orderable=False,
attrs={ attrs={
'td': { 'td': {
'class': 'col-sm-1', 'class': 'col-sm-1',
@ -278,8 +276,7 @@ class ButtonTable(tables.Table):
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')}, extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}}, attrs={'td': {'class': 'col-sm-1'}},
verbose_name=_("Delete"), verbose_name=_("Delete"), )
orderable=False, )
def render_amount(self, value): def render_amount(self, value):
return pretty_money(value) return pretty_money(value)

View File

@ -89,7 +89,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ul> </ul>
<div class="card-body"> <div class="card-body">
<select id="debit_type" class="form-control custom-select d-none"> <select id="debit_type" class="form-control custom-select d-none">
{% for special_type in special_types|slice:"::-1" %} {% for special_type in special_types %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option> <option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django import template from django import template

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django import template from django import template

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from api.tests import TestAPI from api.tests import TestAPI

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import json import json

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'permission.apps.PermissionConfig' default_app_config = 'permission.apps.PermissionConfig'

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# 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
@ -31,4 +31,3 @@ class RoleAdmin(admin.ModelAdmin):
Admin customisation for Role Admin customisation for Role
""" """
list_display = ('name', ) list_display = ('name', )
filter_horizontal = ('permissions',)

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers from rest_framework import serializers

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .views import PermissionViewSet, RoleViewSet from .views import PermissionViewSet, RoleViewSet

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend

Some files were not shown because too many files have changed in this diff Show More