1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

Compare commits

...

39 Commits

Author SHA1 Message Date
korenstin
6f67d2c629 Documentation 2024-07-22 15:52:09 +02:00
korenstin
4b97ab2e2a linters 2024-07-22 15:52:09 +02:00
korenstin
dcfd0167e7 Security against the cycles 2024-07-22 15:52:09 +02:00
korenstin
50a680eed2 Open table and shelf life 2024-07-22 15:52:09 +02:00
korenstin
226a2a6357 Automatic allergens and expiry_date update 2024-07-22 15:52:09 +02:00
korenstin
48462f2ffc Adding ingredients to a preparation 2024-07-22 15:52:09 +02:00
korenstin
260513ae3b Migration fixes 2024-07-22 15:52:09 +02:00
korenstin
210a3cc93c Implementing QRcode creation, modifying Allergen model and creating of few views 2024-07-22 15:52:09 +02:00
quark
896095a44c Un peu de nettoyage, rajout de commentaires 2024-07-22 15:52:09 +02:00
quark
3f997f94fa few changes in models, delete default label 2024-07-22 15:52:09 +02:00
quark
0801ad64ae création de forms fonctionnel (form + views + url + html), few changes in models.py 2024-07-22 15:52:09 +02:00
quark
64bd5ed546 création d'un form pour l'ajout d'aliments basiques 2024-07-22 15:52:09 +02:00
quark
4c390dce17 nom app 2024-07-22 15:52:09 +02:00
quark
adacc293f5 First forms 2024-07-22 15:52:09 +02:00
quark
968fa64d37 Réagencement des tables et de leurs attributs 2024-07-22 15:52:09 +02:00
quark
a481adbae4 création de l'interface admin temporaire 2024-07-22 15:52:09 +02:00
quark
4de2e987ef Rajout de la pseudo-doc 2024-07-22 15:52:07 +02:00
quark
9e6342c929 Création de l'apps et de la base de donnée 2024-07-22 15:50:09 +02:00
quark
74de358953 Update README.md 2024-07-22 15:50:09 +02:00
korenstin
b8f81048a5 Merge branch 'fix_ActivityList' into 'main'
Allow to order the 2 tables and to fix the bug of several activities

See merge request bde/nk20!252
2024-07-18 18:17:06 +02:00
korenstin
af819f45a1 Merge branch 'remove_picture' into 'main'
Allow you to delete the profile picture

See merge request bde/nk20!250
2024-07-18 18:02:43 +02:00
korenstin
076d065ffa Merge branch 'main' into 'remove_picture'
# Conflicts:
#   locale/fr/LC_MESSAGES/django.po
2024-07-18 17:52:22 +02:00
korenstin
2da77d9c17 Merge branch 'fix_join_bda' into 'main'
Fix #126 (join_bda)

Closes #126

See merge request bde/nk20!251
2024-07-18 17:14:23 +02:00
korenstin
01584d6330 Merge branch 'modif_perm' into 'main'
Modif perm

See merge request bde/nk20!249
2024-07-18 16:54:23 +02:00
korenstin
4c0a5922c4 Allow to order the 2 tables and to fix the bug of several activities 2024-07-15 22:06:11 +02:00
korenstin
f90b28fc7c Fix #126 (join_bda) 2024-07-15 14:30:46 +02:00
korenstin
925e0f26f5 Allow you to delete the profile picture 2024-07-13 17:37:19 +02:00
quark
c912383f86 oups la virgule oublié 2024-06-24 22:36:22 +02:00
quark
32830e43fd Modify permission for negative 2024-06-24 21:21:22 +02:00
korenstin
11c6a6fa7a modifications permissions consommation pc kfet (Alcool) 2024-06-24 16:57:39 +02:00
korenstin
201d6b114a Merge branch 'new_logo' into 'main'
New logo

See merge request bde/nk20!247
2024-06-03 22:00:03 +02:00
korenstin
19e77df299 Merge branch 'main' into 'new_logo'
# Conflicts:
#   .gitlab-ci.yml
2024-06-03 21:59:44 +02:00
korenstin
5fd6ec5668 Merge branch 'charte_info' into 'main'
Charte info

See merge request bde/nk20!248
2024-06-03 21:53:01 +02:00
korenstin
10a01c5bc2 linters 2024-05-30 20:21:56 +02:00
korenstin
989905ea64 Update .gitlab-ci.yml 2024-05-26 18:41:49 +02:00
korenstin
0218d43a17 Update .gitlab-ci.yml 2024-05-26 16:00:26 +02:00
test
5d30b0e819 charte info 2024-05-26 15:46:50 +02:00
korenstin
ec759dd3c0 error py37-django22 2024-05-23 22:38:09 +02:00
korenstin
2eb965291d new_logo 2024-05-23 21:46:01 +02:00
46 changed files with 1382 additions and 118 deletions

View File

@ -8,19 +8,19 @@ variables:
GIT_SUBMODULE_STRATEGY: recursive
# Debian Buster
py37-django22:
stage: test
image: debian:buster-backports
before_script:
- >
apt-get update &&
apt-get install --no-install-recommends -t buster-backports -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 py37-django22
# py37-django22:
# stage: test
# image: debian:buster-backports
# before_script:
# - >
# apt-get update &&
# apt-get install --no-install-recommends -t buster-backports -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 py37-django22
# Ubuntu 20.04
py38-django22:
@ -56,7 +56,7 @@ py39-django22:
linters:
stage: quality-assurance
image: debian:buster-backports
image: debian:bullseye
before_script:
- apt-get update && apt-get install -y tox
script: tox -e linters

View File

@ -17,6 +17,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
</form>
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script>
var date_end = document.getElementById("id_date_end");
var date_start = document.getElementById("id_date_start");

View File

@ -17,7 +17,8 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView
from django_tables2.views import SingleTableView
from django.views.generic.list import ListView
from django_tables2.views import MultiTableMixin
from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
@ -57,27 +58,40 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
"""
Displays all Activities, and classify if they are on-going or upcoming ones.
"""
model = Activity
table_class = ActivityTable
ordering = ('-date_start',)
tables = [ActivityTable, ActivityTable]
extra_context = {"title": _("Activities")}
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 = "upcoming-"
return tables
def get_tables_data(self):
# first table = all activities, second table = upcoming
return [
self.get_queryset().order_by("-date_start"),
Activity.objects.filter(date_end__gt=timezone.now())
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
.distinct()
.order_by("date_start")
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
context['upcoming'] = ActivityTable(
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
prefix='upcoming-',
order_by='date_start',
)
tables = context["tables"]
for name, table in zip(["table", "upcoming"], tables):
context[name] = table
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
context["started_activities"] = started_activities

0
apps/food/__init__.py Normal file
View File

37
apps/food/admin.py Normal file
View File

@ -0,0 +1,37 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
from django.db import transaction
from note_kfet.admin import admin_site
from .models import Allergen, BasicFood, QRCode, TransformedFood
@admin.register(QRCode, site=admin_site)
class QRCodeAdmin(admin.ModelAdmin):
pass
@admin.register(BasicFood, site=admin_site)
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

11
apps/food/apps.py Normal file
View File

@ -0,0 +1,11 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils.translation import gettext_lazy as _
from django.apps import AppConfig
class FoodkfetConfig(AppConfig):
name = 'food'
verbose_name = _('food')

View File

@ -0,0 +1,107 @@
[
{
"model": "food.allergen",
"pk": 1,
"fields": {
"name": "alcohol"
}
},
{
"model": "food.allergen",
"pk": 2,
"fields": {
"name": "celery"
}
},
{
"model": "food.allergen",
"pk": 3,
"fields": {
"name": "crustecean"
}
},
{
"model": "food.allergen",
"pk": 4,
"fields": {
"name": "egg"
}
},
{
"model": "food.allergen",
"pk": 5,
"fields": {
"name": "fish"
}
},
{
"model": "food.allergen",
"pk": 6,
"fields": {
"name": "gluten"
}
},
{
"model": "food.allergen",
"pk": 7,
"fields": {
"name": "groundnut"
}
},
{
"model": "food.allergen",
"pk": 8,
"fields": {
"name": "lupine"
}
},
{
"model": "food.allergen",
"pk": 9,
"fields": {
"name": "milk"
}
},
{
"model": "food.allergen",
"pk": 10,
"fields": {
"name": "mollusc"
}
},
{
"model": "food.allergen",
"pk": 11,
"fields": {
"name": "mustard"
}
},
{
"model": "food.allergen",
"pk": 12,
"fields": {
"name": "nut"
}
},
{
"model": "food.allergen",
"pk": 13,
"fields": {
"name": "sesame"
}
},
{
"model": "food.allergen",
"pk": 14,
"fields": {
"name": "soy"
}
},
{
"model": "food.allergen",
"pk": 15,
"fields": {
"name": "sulphite"
}
}
]

99
apps/food/forms.py Normal file
View File

@ -0,0 +1,99 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from random import shuffle
from django import forms
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from member.models import Club
from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from .models import BasicFood, QRCode, TransformedFood
class AddIngredientForms(forms.ModelForm):
"""
Form for add an ingredient
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['ingredient'].queryset = self.fields['ingredient'].queryset.filter(is_ready=False)
class Meta:
model = TransformedFood
fields = ('ingredient',)
class BasicFoodForms(forms.ModelForm):
"""
Form for add non-transformed food
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['name'].required = True
self.fields['owner'].required = True
# Some example
self.fields['name'].widget.attrs.update({"placeholder": _("pasta")})
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
class Meta:
model = BasicFood
fields = ('name', 'owner', 'date_type', 'expiry_date', 'allergens')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
'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_ready=False)
class Meta:
model = QRCode
fields = ('food_container',)
class TransformedFoodForms(forms.ModelForm):
"""
Form for add transformed food
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
self.fields['name'].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
# Some example
self.fields['name'].widget.attrs.update({"placeholder": _("lasagna")})
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
shuffle(clubs)
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
class Meta:
model = TransformedFood
fields = ('name', 'creation_date', 'owner', 'is_active', 'shelf_life')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
'creation_date': DateTimePickerInput(),
}

View File

@ -0,0 +1,84 @@
# Generated by Django 2.2.28 on 2024-07-05 08:57
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('member', '0011_profile_vss_charter_read'),
]
operations = [
migrations.CreateModel(
name='Allergen',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
],
options={
'verbose_name': 'Allergen',
'verbose_name_plural': 'Allergens',
},
),
migrations.CreateModel(
name='Food',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='name')),
('expiry_date', models.DateTimeField(verbose_name='expiry date')),
('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
('is_ready', models.BooleanField(default=False, verbose_name='is ready')),
('allergens', models.ManyToManyField(blank=True, to='food.Allergen', verbose_name='allergen')),
('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')),
],
options={
'verbose_name': 'foods',
},
),
migrations.CreateModel(
name='BasicFood',
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')),
('date_type', models.CharField(choices=[('DLC', 'DLC'), ('DDM', 'DDM')], max_length=255)),
('arrival_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='arrival date')),
],
options={
'verbose_name': 'Basic food',
'verbose_name_plural': 'Basic foods',
},
bases=('food.food',),
),
migrations.CreateModel(
name='QRCode',
fields=[
('id', models.AutoField(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.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.Food', verbose_name='food container')),
],
options={
'verbose_name': 'QR-code',
'verbose_name_plural': 'QR-codes',
},
),
migrations.CreateModel(
name='TransformedFood',
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')),
('creation_date', models.DateTimeField(verbose_name='creation date')),
('is_active', models.BooleanField(default=True, verbose_name='is active')),
('ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.Food', verbose_name='transformed ingredient')),
],
options={
'verbose_name': 'Transformed food',
'verbose_name_plural': 'Transformed foods',
},
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

217
apps/food/models.py Normal file
View File

@ -0,0 +1,217 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from django.db import models, transaction
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
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.OneToOneField(
'Food',
on_delete=models.PROTECT,
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):
"""
A list of allergen and alimentary restrictions
"""
name = models.CharField(
verbose_name=_('name'),
max_length=255,
)
class Meta:
verbose_name = _('Allergen')
verbose_name_plural = _('Allergens')
def __str__(self):
return self.name
class Food(PolymorphicModel):
name = models.CharField(
verbose_name=_('name'),
max_length=255,
)
owner = models.ForeignKey(
Club,
on_delete=models.PROTECT,
related_name='+',
verbose_name=_('owner'),
)
allergens = models.ManyToManyField(
Allergen,
blank=True,
verbose_name=_('allergen'),
)
expiry_date = models.DateTimeField(
verbose_name=_('expiry date'),
null=False,
)
was_eaten = models.BooleanField(
default=False,
verbose_name=_('was eaten'),
)
is_ready = models.BooleanField(
default=False,
verbose_name=_('is ready'),
)
def __str__(self):
return self.name
@transaction.atomic
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
return super().save(force_insert, force_update, using, update_fields)
class Meta:
verbose_name = _('food')
verbose_name = _('foods')
class BasicFood(Food):
"""
Food which has been directly buy on supermarket
"""
date_type = models.CharField(
max_length=255,
choices=(
("DLC", "DLC"),
("DDM", "DDM"),
)
)
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
def update_allergens(self):
# update parents
for parent in self.transformed_ingredient_inv.iterator():
parent.update_allergens()
@transaction.atomic
def update_expiry_date(self):
# 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()
class Meta:
verbose_name = _('Basic food')
verbose_name_plural = _('Basic foods')
class TransformedFood(Food):
"""
Transformed food are a mix between basic food and meal
"""
creation_date = models.DateTimeField(
verbose_name=_('creation date'),
)
ingredient = models.ManyToManyField(
Food,
blank=True,
symmetrical=False,
related_name='transformed_ingredient_inv',
verbose_name=_('transformed ingredient'),
)
is_active = models.BooleanField(
default=True,
verbose_name=_('is active'),
)
# Without microbiological analyzes, the storage time is 3 days
shelf_life = models.DurationField(
verbose_name=_("shelf life"),
default=timedelta(days=3),
)
@transaction.atomic
def update_allergens(self):
# When allergens are changed, simply update the parents' allergens
old_allergens = list(self.allergens.all())
self.allergens.clear()
for ingredient in self.ingredient.iterator():
self.allergens.set(self.allergens.union(ingredient.allergens.all()))
if old_allergens == list(self.allergens.all()):
return
super().save()
# update parents
for parent in self.transformed_ingredient_inv.iterator():
parent.update_allergens()
@transaction.atomic
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)
if old_expiry_date == self.expiry_date:
return
super().save()
# 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:
verbose_name = _('Transformed food')
verbose_name_plural = _('Transformed foods')

19
apps/food/tables.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django_tables2 import A
from .models import TransformedFood
class TransformedFoodTable(tables.Table):
name = tables.LinkColumn(
'food:food_view',
args=[A('pk'), ],
)
class Meta:
model = TransformedFood
template_name = 'django_tables2/bootstrap4.html'
fields = ('name', )

View File

@ -0,0 +1,21 @@
{% 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">
HTML not finished <br>
{{ 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,21 @@
{% 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">
HTML not finished <br>
{{ 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,27 @@
{% 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">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>name : {{ food.name }}</p>
<p>owner : {{ food.owner }}</p>
<p>arrival_date : {{ food.arrival_date }}</p>
<p>expiry_date : {{ food.expiry_date }}</p>
<p>allergens :</p>
<ul>
{% for allergen in food.allergens.iterator %}
<li>{{ allergen.name }}</li>
{% endfor %}
</ul>
<a href="{% url "food:basic_update" pk=food.pk %}">Update</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% block content %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
HTML not finished <br>
{{ title }}
</h3>
<div class="row">
<div class="col-xl-12">
<div class="btn-group btn-block">
<a href="{% url "food:basic_create" %}" class="btn btn-sm btn-outline-primary">Basic</a>
<a href="{% url "food:transformed_create" %}" class="btn btn-sm btn-outline-primary">Transformed</a>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% 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">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body" id="form">
<a class="btn btn-sm btn-success" href="{% url "food:qrcode_basic_create" slug=slug %}" data-turbolinks="false">
New basic food
</a>
<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,24 @@
{% 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">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>qrcode : {{ qrcode.qr_code_number }}</p>
<p>name : {{ qrcode.food_container.name }}</p>
{% if qrcode.food_container.polymorphic_ctype.name == 'Basic food' %}
<a href="{% url "food:basic_update" pk=qrcode.food_container.pk %}">Update</a>
{% else %}
<a href="{% url "food:transformed_update" pk=qrcode.food_container.pk %}">Update</a>
{% endif %}
<a href="{% url "food:add_ingredient" pk=qrcode.food_container.pk %}">Add the ingrdient</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,21 @@
{% 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">
HTML not finished <br>
{{ 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,33 @@
{% 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">
HTML not finished <br>
{{ title }}
</h3>
<div class="card-body">
<p>name : {{ food.name }}</p>
<p>owner : {{ food.owner }}</p>
<p>creation_date : {{ food.creation_date }}</p>
<p>expiry_date : {{ food.expiry_date }}</p>
<p>allergens :</p>
<ul>
{% for allergen in food.allergens.iterator %}
<li>{{ allergen.name }}</li>
{% endfor %}
</ul>
<p>ingredients :</p>
<ul>
{% for ingredient in food.ingredient.iterator %}
<li><a href="{% url "food:food_view" pk=ingredient.pk %}">{{ ingredient.name }}</a></li>
{% endfor %}
</ul>
<a href="{% url "food:transformed_update" pk=food.pk %}">Update</a>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% 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">
<div class="card-footer">
<a class="btn btn-sm btn-success" href="{% url 'food:transformed_create' %}" data-turbolinks="false">
New transformed food
</a>
</div>
<h3 class="card-header text-center">
In preparation
</h3>
{% render_table table %}
<h3 class="card-header text-center">
Open
</h3>
{% render_table open_table %}
</div>
{% endblock %}

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

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

24
apps/food/urls.py Normal file
View File

@ -0,0 +1,24 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
from . import views
app_name = 'food'
urlpatterns = [
path('', views.TransfomedListView.as_view(), name='food_list'),
path('<int:slug>', views.QRCodeView.as_view(), name='qrcode_view'),
path('detail/<int:pk>', views.FoodView.as_view(), name='food_view'),
path('<int:slug>/create_qrcode', views.QRCodeCreateView.as_view(), name='qrcode_create'),
path('create', views.FoodCreateView.as_view(), name='food_create'),
path('<int:slug>/create_qrcode/basic', views.QRCodeBasicFoodCreateView.as_view(), name='qrcode_basic_create'),
path('create/transformed', views.TransformedFoodCreateView.as_view(), name='transformed_create'),
path('update/basic/<int:pk>', views.BasicFoodUpdateView.as_view(), name='basic_update'),
path('update/transformed/<int:pk>', views.TransformedFoodUpdateView.as_view(), name='transformed_update'),
path('add/<int:pk>', views.AddIngredientView.as_view(), name='add_ingredient'),
]

278
apps/food/views.py Normal file
View File

@ -0,0 +1,278 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import transaction
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpResponseRedirect
from django_tables2.views import SingleTableView
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, TemplateView
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import AddIngredientForms, BasicFoodForms, QRCodeForms, TransformedFoodForms
from .models import BasicFood, Food, QRCode, TransformedFood
from .tables import TransformedFoodTable
class AddIngredientView(ProtectQuerysetMixin, UpdateView):
"""
A view to add an ingredient
"""
model = Food
template_name = 'food/add_ingredient_form.html'
extra_context = {"title": _("Add the ingredient")}
form_class = AddIngredientForms
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["pk"] = self.kwargs["pk"]
return context
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
food = Food.objects.get(pk=self.kwargs['pk'])
add_ingredient_form = AddIngredientForms(data=self.request.POST)
if not food.is_ready:
form.add_error(None, _("The product isn't ready"))
return self.form_invalid(form)
if not add_ingredient_form.is_valid():
return self.form_invalid(form)
# Save the aliment and the allergens associed
for transformed_pk in self.request.POST.getlist('ingredient'):
transformed = TransformedFood.objects.get(pk=transformed_pk)
if not transformed.is_ready:
transformed.ingredient.add(food)
transformed.update()
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self, **kwargs):
return reverse('food:food_list')
class BasicFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
A view to add a basic food
"""
model = BasicFood
form_class = BasicFoodForms
template_name = 'food/basic_food_form.html'
extra_context = {"title": _("Add a new aliment")}
@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)
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})
class FoodCreateView(ProtectQuerysetMixin, LoginRequiredMixin, TemplateView):
"""
A view to add a new aliment
"""
template_name = 'food/create_food_form.html'
extra_context = {"title": _("Add a new aliment")}
class FoodView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
A view to see a food
"""
model = Food
extra_context = {"title": _("Details")}
context_object_name = "food"
class QRCodeBasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
#####################################################################
# TO DO
# - 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/basic_food_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 = True
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):
return BasicFood(
name="",
expiry_date=timezone.now(),
)
class QRCodeCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add a new qrcode
"""
model = QRCode
template_name = 'food/create_qrcode_form.html'
form_class = QRCodeForms
extra_context = {"title": _("Add a new QRCode")}
def get(self, *args, **kwargs):
qrcode = kwargs["slug"]
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
return HttpResponseRedirect(reverse("food:qrcode_view", kwargs=kwargs))
else:
return super().get(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["slug"] = self.kwargs["slug"]
return context
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
qrcode_food_form = QRCodeForms(data=self.request.POST)
if not qrcode_food_form.is_valid():
return self.form_invalid(form)
# Save the qrcode
qrcode = form.save(commit=False)
qrcode.qr_code_number = self.kwargs["slug"]
qrcode._force_save = True
qrcode.save()
qrcode.refresh_from_db()
qrcode.food_container.is_ready = True
qrcode.food_container.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):
return QRCode(
qr_code_number=self.kwargs["slug"],
)
class QRCodeView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
A view to see a qrcode
"""
model = QRCode
extra_context = {"title": _("QRCode")}
context_object_name = "qrcode"
slug_field = "qr_code_number"
def get(self, *args, **kwargs):
qrcode = kwargs["slug"]
if self.model.objects.filter(qr_code_number=qrcode).count() > 0:
return super().get(*args, **kwargs)
else:
return HttpResponseRedirect(reverse("food:qrcode_create", kwargs=kwargs))
class TransformedFoodFormView(ProtectQuerysetMixin):
"""
A view to add a tranformed food
"""
model = TransformedFood
template_name = 'food/transformed_food_form.html'
form_class = TransformedFoodForms
extra_context = {"title": _("Add a new meal")}
@transaction.atomic
def form_valid(self, form):
form.instance.creater = self.request.user
transformed_food_form = TransformedFoodForms(data=self.request.POST)
if not transformed_food_form.is_valid():
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._force_save = True
transformed_food.save()
transformed_food.refresh_from_db()
ans = super().form_valid(form)
transformed_food.update()
return ans
def get_success_url(self, **kwargs):
self.object.refresh_from_db()
return reverse('food:food_view', kwargs={"pk": self.object.pk})
class TransformedFoodUpdateView(TransformedFoodFormView, LoginRequiredMixin, UpdateView):
pass
class TransformedFoodCreateView(TransformedFoodFormView, ProtectedCreateView):
def get_sample_object(self):
return TransformedFood(
name="",
creation_date=timezone.now(),
)
class TransfomedListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Displays not ready TransformedFood
"""
model = TransformedFood
table_class = TransformedFoodTable
ordering = ('name',)
extra_context = {"title": _("Transformed food")}
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs)\
.filter(is_ready=False)\
.distinct()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['open_table'] = TransformedFoodTable(
TransformedFood.objects.filter(
was_eaten=False,
expiry_date__lt=timezone.now()
),
prefix="open-")
return context

View File

@ -138,6 +138,9 @@ class ImageForm(forms.Form):
return cleaned_data
def is_valid(self):
return super().is_valid() or super().clean().get('image') is None
class ClubForm(forms.ModelForm):
def clean(self):
@ -151,7 +154,7 @@ class ClubForm(forms.ModelForm):
class Meta:
model = Club
fields = '__all__'
exclude = ("add_registration_form",)
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2024-07-15 09:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0011_profile_vss_charter_read'),
]
operations = [
migrations.AddField(
model_name='club',
name='add_registration_form',
field=models.BooleanField(default=False, verbose_name='add to registration form'),
),
]

View File

@ -259,6 +259,11 @@ class Club(models.Model):
help_text=_('Maximal date of a membership, after which members must renew it.'),
)
add_registration_form = models.BooleanField(
verbose_name=_("add to registration form"),
default=False,
)
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")

View File

@ -14,6 +14,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
<form method="post" enctype="multipart/form-data" id="formUpload">
{% csrf_token %}
{{ form |crispy }}
{% if user.note.display_image != "pic/default.png" %}
<input type="submit" class="btn btn-primary" value="{% trans "Remove" %}">
{% endif %}
</form>
</div>
<!-- MODAL TO CROP THE IMAGE -->

View File

@ -326,6 +326,9 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
"""Save image to note"""
image = form.cleaned_data['image']
if image is None:
image = "pic/default.png"
else:
# Rename as a PNG or GIF
extension = image.name.split(".")[-1]
if extension == "gif":

View File

@ -18,6 +18,7 @@ def create_special_notes(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('note', '0001_initial'),
('logs', '0001_initial'),
]
operations = [

View File

@ -23,7 +23,7 @@
<p>
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
est inférieur à 0 € depuis plus de 24h.
est inférieur à 0 €.
</p>
<p>

View File

@ -2591,12 +2591,12 @@
"note",
"transaction"
],
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]",
"query": "[\"OR\", {\"source__balance__gte\": 0}, [\"AND\", [\"NOT\", {\"recurrenttransaction__template__category__name\": \"Alcool\"}], {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}], {\"valid\": false}]",
"type": "add",
"mask": 2,
"field": "",
"permanent": false,
"description": "Créer une transaction quelconque tant que la source reste au-dessus de -20 €"
"description": "Créer une transaction quelconque tant que la source reste positive s'il s'agit d'alcool, sinon au-dessus de -20€"
}
},
{

View File

@ -5,7 +5,6 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
# from member.models import Club
from note.models import NoteSpecial, Alias
from note_kfet.inputs import AmountInput
@ -115,12 +114,3 @@ class ValidationForm(forms.Form):
required=False,
initial=True,
)
# If the bda exists
# if Club.objects.filter(name__iexact="bda").exists():
# The user can join the bda club at the inscription
# join_bda = forms.BooleanField(
# label=_("Join BDA Club"),
# required=False,
# initial=True,
# )

View File

@ -1,6 +1,7 @@
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import forms
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
@ -238,9 +239,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
if Club.objects.filter(name__iexact="BDA").exists():
bda = Club.objects.get(name__iexact="BDA")
fee += bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
for club in Club.objects.filter(add_registration_form=True):
fee += club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
# ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
@ -249,6 +249,16 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
def get_form(self, form_class=None):
form = super().get_form(form_class)
# add clubs that are in registration form
for club in Club.objects.filter(add_registration_form=True).order_by("name"):
form_join_club = forms.BooleanField(
label=_("Join %(club)s Club") % {'club': club.name},
required=False,
initial=False,
)
form.fields.update({f"join_{club.id}": form_join_club})
user = self.get_object()
form.fields["last_name"].initial = user.last_name
form.fields["first_name"].initial = user.first_name
@ -266,11 +276,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
form.add_error(None, _("An alias with a similar name already exists."))
return self.form_invalid(form)
# Check if BDA exist to propose membership at regisration
bda_exists = False
if Club.objects.filter(name__iexact="BDA").exists():
bda_exists = True
# Get form data
# soge = form.cleaned_data["soge"]
credit_type = form.cleaned_data["credit_type"]
@ -280,8 +285,9 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
bank = form.cleaned_data["bank"]
join_bde = form.cleaned_data["join_bde"]
join_kfet = form.cleaned_data["join_kfet"]
if bda_exists:
join_bda = form.cleaned_data["join_bda"]
clubs_registration = Club.objects.filter(add_registration_form=True).order_by("name")
join_clubs = [(club, form.cleaned_data[f"join_{club.id}"]) for club in clubs_registration]
# if soge:
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
@ -303,11 +309,12 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
# Add extra fee for the full membership
fee += kfet_fee if join_kfet else 0
if bda_exists:
bda = Club.objects.get(name__iexact="BDA")
bda_fee = bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
# Add extra fee for the bda membership
fee += bda_fee if join_bda else 0
clubs_fee = dict()
for club, join_club in join_clubs:
club_fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
# Add extra fee for the club membership
clubs_fee[club] = club_fee
fee += club_fee if join_club else 0
# # If the bank pays, then we don't credit now. Treasurers will validate the transaction
# # and credit the note later.
@ -387,12 +394,13 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
membership.save()
if bda_exists and join_bda:
for club, join_club in join_clubs:
if join_club:
# Create membership for the user to the BDA starting today
membership = Membership(
club=bda,
club=club,
user=user,
fee=bda_fee,
fee=clubs_fee[club],
)
membership.save()
membership.refresh_from_db()

83
docs/apps/food.rst Normal file
View File

@ -0,0 +1,83 @@
Application Food
================
L'application ``food`` s'occupe de la traçabilité et permet notamment l'obtention de la liste des allergènes.
Modèles
-------
L'application comporte 5 modèles : Allergen, QRCode, Food, BasicFood, TransformedFood.
Food
~~~~
Ce modèle est un PolymorphicModel et ne sert uniquement à créer BasicFood et TransformedFood.
Le modèle regroupe :
* Nom du produit
* Propriétaire (doit-être un Club)
* Allergènes (ManyToManyField)
* date d'expiration
* a été mangé (booléen)
* est prêt (booléen)
BasicFood
~~~~~~~~~
Les BasicFood correspondent aux produits non modifiés à la Kfet. Ils peuvent correspondre à la fois à des produits achetés en magasin ou à des produits Terre à Terre. Ces produits seront les ingrédients de tous les plats préparés et en conséquent sont les seuls produits à nécessité une saisie manuelle des allergènes.
Le modèle regroupe :
* Type de date (DLC = date limite de consommation, DDM = date de durabilité minimale)
* Date d'arrivée
* Champs de Food
TransformedFood
~~~~~~~~~~~~~~~
Les TransformedFood correspondent aux produits préparés à la Kfet. Ils peuvent être composés de BasicFood et/ou de TransformedFood. La date d'expiration et les allergènes sont automatiquement mis à jour par update (qui doit être exécuté après modification des ingrédients dans les forms par exemple).
Le modèle regroupe :
* Durée de consommation (par défaut 3 jours)
* Ingrédients (ManyToManyField vers Food)
* Date de création
* Champs de Food
Allergen
~~~~~~~~
Le modèle regroupe :
* Nom
QRCode
~~~~~~
Le modèle regroupe :
* nombre (unique, entier positif)
* food (OneToOneField vers Food)
Création de BasicFood
~~~~~~~~~~~~~~~~~~~~~
Un BasicFood a toujours besoin d'un QRCode (depuis l'interface web). Il convient donc de coller le QRCode puis de le scanner et de compléter le formulaire.
Création de TransformedFood
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Pour créer un TransformedFood, il suffit d'aller dans l'onglet ``traçabilité`` et de cliquer sur l'onglet.
Ajouter un ingrédient
~~~~~~~~~~~~~~~~~~~~~
Un ingrédient a forcément un QRCode. Il convient donc de scanner le QRCode de l'ingrédient et de sélectionner le produit auquel il doit être ajouté.
Remarque : Un produit fini doit avoir un QRCode et inversement.
Terminer un plat
~~~~~~~~~~~~~~~~
Il suffit de coller le QRCode sur le plat, de le scanner et de sélectionner le produit.

View File

@ -114,7 +114,7 @@ msgstr "Lieu où l'activité est organisée, par exemple la Kfet."
msgid "type"
msgstr "type"
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:313
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:318
#: apps/note/models/notes.py:148 apps/treasury/models.py:293
#: apps/wei/models.py:171 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15
@ -247,6 +247,7 @@ msgid "The validation of the activity is pending."
msgstr "La validation de cette activité est en attente."
#: apps/activity/tables.py:43 apps/treasury/tables.py:107
#: apps/member/templates/member/picture_update.html:18
msgid "Remove"
msgstr "Supprimer"
@ -262,13 +263,13 @@ msgstr "supprimer"
msgid "Type"
msgstr "Type"
#: apps/activity/tables.py:84 apps/member/forms.py:193
#: apps/activity/tables.py:84 apps/member/forms.py:196
#: apps/registration/forms.py:92 apps/treasury/forms.py:131
#: apps/wei/forms/registration.py:104
msgid "Last name"
msgstr "Nom de famille"
#: apps/activity/tables.py:86 apps/member/forms.py:198
#: apps/activity/tables.py:86 apps/member/forms.py:201
#: apps/note/templates/note/transaction_form.html:138
#: apps/registration/forms.py:97 apps/treasury/forms.py:133
#: apps/wei/forms/registration.py:109
@ -400,37 +401,37 @@ msgstr "Inviter"
msgid "Create new activity"
msgstr "Créer une nouvelle activité"
#: apps/activity/views.py:67 note_kfet/templates/base.html:90
#: apps/activity/views.py:68 note_kfet/templates/base.html:90
msgid "Activities"
msgstr "Activités"
#: apps/activity/views.py:93
#: apps/activity/views.py:108
msgid "Activity detail"
msgstr "Détails de l'activité"
#: apps/activity/views.py:113
#: apps/activity/views.py:128
msgid "Update activity"
msgstr "Modifier l'activité"
#: apps/activity/views.py:140
#: apps/activity/views.py:155
msgid "Invite guest to the activity \"{}\""
msgstr "Invitation pour l'activité « {} »"
#: apps/activity/views.py:178
#: apps/activity/views.py:193
msgid "You are not allowed to display the entry interface for this activity."
msgstr ""
"Vous n'êtes pas autorisé·e à afficher l'interface des entrées pour cette "
"activité."
#: apps/activity/views.py:181
#: apps/activity/views.py:196
msgid "This activity does not support activity entries."
msgstr "Cette activité ne requiert pas d'entrées."
#: apps/activity/views.py:184
#: apps/activity/views.py:199
msgid "This activity is closed."
msgstr "Cette activité est fermée."
#: apps/activity/views.py:280
#: apps/activity/views.py:295
msgid "Entry for activity \"{}\""
msgstr "Entrées pour l'activité « {} »"
@ -507,11 +508,11 @@ msgstr "cotisation pour adhérer (normalien·ne élève)"
msgid "membership fee (unpaid students)"
msgstr "cotisation pour adhérer (normalien·ne étudiant·e)"
#: apps/member/admin.py:65 apps/member/models.py:325
#: apps/member/admin.py:65 apps/member/models.py:330
msgid "roles"
msgstr "rôles"
#: apps/member/admin.py:66 apps/member/models.py:339
#: apps/member/admin.py:66 apps/member/models.py:344
msgid "fee"
msgstr "cotisation"
@ -563,29 +564,29 @@ msgid "This image cannot be loaded."
msgstr "Cette image ne peut pas être chargée."
#: apps/member/forms.py:148 apps/member/views.py:102
#: apps/registration/forms.py:34 apps/registration/views.py:266
#: apps/registration/forms.py:34 apps/registration/views.py:276
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
#: apps/member/forms.py:172
#: apps/member/forms.py:175
msgid "Inscription paid by Société Générale"
msgstr "Inscription payée par la Société générale"
#: apps/member/forms.py:174
#: apps/member/forms.py:177
msgid "Check this case if the Société Générale paid the inscription."
msgstr "Cochez cette case si la Société Générale a payé l'inscription."
#: apps/member/forms.py:179 apps/registration/forms.py:79
#: apps/member/forms.py:182 apps/registration/forms.py:79
#: apps/wei/forms/registration.py:91
msgid "Credit type"
msgstr "Type de rechargement"
#: apps/member/forms.py:180 apps/registration/forms.py:80
#: apps/member/forms.py:183 apps/registration/forms.py:80
#: apps/wei/forms/registration.py:92
msgid "No credit"
msgstr "Pas de rechargement"
#: apps/member/forms.py:182
#: apps/member/forms.py:185
msgid "You can credit the note of the user."
msgstr "Vous pouvez créditer la note de l'utilisateur·ice avant l'adhésion."
@ -594,17 +595,17 @@ msgstr "Vous pouvez créditer la note de l'utilisateur·ice avant l'adhésion."
msgid "Credit amount"
msgstr "Montant à créditer"
#: apps/member/forms.py:203 apps/note/templates/note/transaction_form.html:144
#: apps/member/forms.py:206 apps/note/templates/note/transaction_form.html:144
#: apps/registration/forms.py:102 apps/treasury/forms.py:135
#: apps/wei/forms/registration.py:114
msgid "Bank"
msgstr "Banque"
#: apps/member/forms.py:230
#: apps/member/forms.py:233
msgid "User"
msgstr "Utilisateur·ice"
#: apps/member/forms.py:244
#: apps/member/forms.py:247
msgid "Roles"
msgstr "Rôles"
@ -852,46 +853,50 @@ msgstr ""
"Date maximale d'une fin d'adhésion, après laquelle les adhérent·e·s doivent la "
"renouveler."
#: apps/member/models.py:263 apps/member/models.py:319
#: apps/member/models.py:263
msgid "add to registration form"
msgstr "ajouter au formulaire d'inscription"
#: apps/member/models.py:268 apps/member/models.py:324
#: apps/note/models/notes.py:176
msgid "club"
msgstr "club"
#: apps/member/models.py:264
#: apps/member/models.py:269
msgid "clubs"
msgstr "clubs"
#: apps/member/models.py:330
#: apps/member/models.py:335
msgid "membership starts on"
msgstr "l'adhésion commence le"
#: apps/member/models.py:334
#: apps/member/models.py:339
msgid "membership ends on"
msgstr "l'adhésion finit le"
#: apps/member/models.py:343 apps/note/models/transactions.py:385
#: apps/member/models.py:348 apps/note/models/transactions.py:385
msgid "membership"
msgstr "adhésion"
#: apps/member/models.py:344
#: apps/member/models.py:349
msgid "memberships"
msgstr "adhésions"
#: apps/member/models.py:348
#: apps/member/models.py:353
#, python-brace-format
msgid "Membership of {user} for the club {club}"
msgstr "Adhésion de {user} pour le club {club}"
#: apps/member/models.py:367
#: apps/member/models.py:372
#, python-brace-format
msgid "The role {role} does not apply to the club {club}."
msgstr "Le rôle {role} ne s'applique pas au club {club}."
#: apps/member/models.py:376 apps/member/views.py:712
#: apps/member/models.py:381 apps/member/views.py:712
msgid "User is already a member of the club"
msgstr "L'utilisateur·ice est déjà membre du club"
#: apps/member/models.py:388 apps/member/views.py:721
#: apps/member/models.py:393 apps/member/views.py:721
msgid "User is not a member of the parent club"
msgstr "L'utilisateur·ice n'est pas membre du club parent"
@ -1153,11 +1158,11 @@ msgstr "Introspection :"
msgid "Show my applications"
msgstr "Voir mes applications"
#: apps/member/templates/member/picture_update.html:35
#: apps/member/templates/member/picture_update.html:38
msgid "Nevermind"
msgstr "Annuler"
#: apps/member/templates/member/picture_update.html:36
#: apps/member/templates/member/picture_update.html:39
msgid "Crop and upload"
msgstr "Recadrer et envoyer"
@ -2183,18 +2188,23 @@ msgstr "Utilisateur·ice·s en attente d'inscription"
msgid "Registration detail"
msgstr "Détails de l'inscription"
#: apps/registration/views.py:293
#: apps/registration/views.py:256
#, python-format
msgid "Join %(club)s Club"
msgstr "Adhérer au club %(club)s"
#: apps/registration/views.py:299
msgid "You must join the BDE."
msgstr "Vous devez adhérer au BDE."
#: apps/registration/views.py:323
#: apps/registration/views.py:330
msgid ""
"The entered amount is not enough for the memberships, should be at least {}"
msgstr ""
"Le montant crédité est trop faible pour adhérer, il doit être au minimum de "
"{}"
#: apps/registration/views.py:417
#: apps/registration/views.py:425
msgid "Invalidate pre-registration"
msgstr "Invalider l'inscription"
@ -3388,6 +3398,10 @@ msgstr "Support technique"
msgid "FAQ (FR)"
msgstr "FAQ (FR)"
#: note_kfet/templates/base.html:200
msgid "Charte Info (FR)"
msgstr "Charte Info (FR)"
#: note_kfet/templates/base_search.html:15
msgid "Search by attribute such as name…"
msgstr "Chercher par un attribut tel que le nom …"
@ -3616,9 +3630,6 @@ msgstr ""
"d'adhésion. Vous devez également valider votre adresse email en suivant le "
"lien que vous avez reçu."
#~ msgid "Join BDA Club"
#~ msgstr "Adhérer au club BDA"
#, fuzzy
#~| msgid "People having you as a friend"
#~ msgid "You already have that person as a friend"

View File

@ -4,7 +4,7 @@
"pk": 1,
"fields": {
"domain": "note.crans.org",
"name": "La Note Kfet \ud83c\udf7b"
"name": "La Note Kfet 🍪"
}
}
]

View File

@ -69,6 +69,7 @@ INSTALLED_APPS = [
# Note apps
'api',
'activity',
'food',
'logs',
'member',
'note',

View File

@ -66,6 +66,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a>
</li>
{% endif %}
<li class="nav-item">
{% url 'food:food_list' as url %}
<a data-turbolinks="false" class="nav-link" href="{{ url }}">Food</a>
</li>
{% if user.is_authenticated and user|is_member:"Kfet" %}
<li class="nav-item">
{% url 'note:transfer' as url %}
@ -194,6 +198,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
class="text-muted">{% trans "Contact us" %}</a> &mdash;
<a href="mailto:{{ "SUPPORT_EMAIL" | getenv }}"
class="text-muted">{% trans "Technical Support" %}</a> &mdash;
<a href="https://perso.crans.org/club-bde/charte_informatique.pdf"
class="text-muted">{% trans "Charte Info (FR)" %}</a> &mdash;
<a href="https://note.crans.org/doc/faq/"
class="text-muted">{% trans "FAQ (FR)" %}</a> &mdash;
</span>

View File

@ -21,6 +21,7 @@ urlpatterns = [
path('activity/', include('activity.urls')),
path('treasury/', include('treasury.urls')),
path('wei/', include('wei.urls')),
path('food/',include('food.urls')),
# Include Django Contrib and Core routers
path('i18n/', include('django.conf.urls.i18n')),