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

Compare commits

..

3 Commits

Author SHA1 Message Date
74f0923feb Merge branch 'no-api-error' into 'main'
fix #113

See merge request bde/nk20!253
2024-07-18 16:28:30 +02:00
de7b91db2a Fix #113. Fix regex in views. 2024-07-18 14:16:37 +02:00
799c43f688 Parse input of search filters to prevent errors based on invalid regex, fixes #113
Signed-off-by: Yohann D'ANELLO <ynerant@crans.org>
2024-07-18 14:16:07 +02:00
266 changed files with 5551 additions and 13451 deletions

View File

@ -7,10 +7,25 @@ stages:
variables:
GIT_SUBMODULE_STRATEGY: recursive
# Ubuntu 22.04
py310-django42:
# 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
# Ubuntu 20.04
py38-django22:
stage: test
image: ubuntu:22.04
image: ubuntu:20.04
before_script:
# Fix tzdata prompt
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
@ -22,12 +37,12 @@ py310-django42:
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 py310-django42
script: tox -e py38-django22
# Debian Bookworm
py311-django42:
# Debian Bullseye
py39-django22:
stage: test
image: debian:bookworm
image: debian:bullseye
before_script:
- >
apt-get update &&
@ -37,11 +52,11 @@ py311-django42:
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 py311-django42
script: tox -e py39-django22
linters:
stage: quality-assurance
image: debian:bookworm
image: debian:bullseye
before_script:
- apt-get update && apt-get install -y tox
script: tox -e linters

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "apps/scripts"]
path = apps/scripts
url = https://gitlab.crans.org/bde/nk20-scripts
url = https://gitlab.crans.org/bde/nk20-scripts.git

View File

@ -55,7 +55,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
(env)$ ./manage.py makemigrations
(env)$ ./manage.py migrate
(env)$ ./manage.py loaddata initial
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
(env)$ ./manage.py createsuperuser # Création d'un utilisateur initial
```
6. Enjoy :

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
default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,11 +1,11 @@
# 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
from django.contrib import admin
from note_kfet.admin import admin_site
from .forms import GuestForm
from .models import Activity, ActivityType, Entry, Guest, Opener
from .models import Activity, ActivityType, Entry, Guest
@admin.register(Activity, site=admin_site)
@ -45,11 +45,3 @@ class EntryAdmin(admin.ModelAdmin):
Admin customisation for Entry
"""
list_display = ('note', 'activity', 'time', 'guest')
@admin.register(Opener, site=admin_site)
class OpenerAdmin(admin.ModelAdmin):
"""
Admin customisation for Opener
"""
list_display = ('activity', 'opener')

View File

@ -1,11 +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
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction, Opener
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction
class ActivityTypeSerializer(serializers.ModelSerializer):
@ -61,17 +59,3 @@ class GuestTransactionSerializer(serializers.ModelSerializer):
class Meta:
model = GuestTransaction
fields = '__all__'
class OpenerSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Openers.
The djangorestframework plugin will analyse the model `Opener` and parse all fields in the API.
"""
class Meta:
model = Opener
fields = '__all__'
validators = [UniqueTogetherValidator(
queryset=Opener.objects.all(), fields=("opener", "activity"),
message=_("This opener already exists"))]

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
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
def register_activity_urls(router, path):
@ -12,4 +12,3 @@ def register_activity_urls(router, path):
router.register(path + '/type', ActivityTypeViewSet)
router.register(path + '/guest', GuestViewSet)
router.register(path + '/entry', EntryViewSet)
router.register(path + '/opener', OpenerViewSet)

View File

@ -1,15 +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
from django_filters.rest_framework import DjangoFilterBackend
from api.filters import RegexSafeSearchFilter
from api.viewsets import ReadProtectedModelViewSet
from django.core.exceptions import ValidationError
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.response import Response
from rest_framework import status
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer, OpenerSerializer
from ..models import Activity, ActivityType, Entry, Guest, Opener
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
from ..models import Activity, ActivityType, Entry, Guest
class ActivityTypeViewSet(ReadProtectedModelViewSet):
@ -69,32 +66,3 @@ class EntryViewSet(ReadProtectedModelViewSet):
filterset_fields = ['activity', 'time', 'note', 'guest', ]
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
'$guest__last_name', '$guest__first_name', ]
class OpenerViewSet(ReadProtectedModelViewSet):
"""
REST Opener View set.
The djangorestframework plugin will get all `Opener` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/opener/
"""
queryset = Opener.objects
serializer_class = OpenerSerializer
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend]
search_fields = ['$opener__alias__name', '$opener__alias__normalized_name',
'$activity__name']
filterset_fields = ['opener', 'opener__noteuser__user', 'activity']
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# opener-activity can't change
serializer_class.Meta.read_only_fields = ('opener', 'acitivity',)
return serializer_class
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
try:
self.perform_destroy(instance)
except ValidationError as e:
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)

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
from django.apps import AppConfig

View File

@ -1,17 +1,16 @@
# 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
from datetime import timedelta
from random import shuffle
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import Club
from note.models import Note, NoteUser
from note_kfet.inputs import Autocomplete
from note_kfet.inputs import Autocomplete, DateTimePickerInput
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
@ -44,7 +43,7 @@ class ActivityForm(forms.ModelForm):
class Meta:
model = Activity
exclude = ('creater', 'valid', 'open', 'opener', )
exclude = ('creater', 'valid', 'open', )
widgets = {
"organizer": Autocomplete(
model=Club,

View File

@ -1,28 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-01 12:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0006_trust'),
('activity', '0003_auto_20240323_1422'),
]
operations = [
migrations.CreateModel(
name='Opener',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('activity', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='opener', to='activity.Activity', verbose_name='activity')),
('opener', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='activity_responsible', to='note.Note', verbose_name='opener')),
],
options={
'verbose_name': 'opener',
'verbose_name_plural': 'openers',
'unique_together': {('opener', 'activity')},
},
),
]

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,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
import os
@ -11,7 +11,7 @@ from django.db import models, transaction
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note.models import NoteUser, Transaction, Note
from note.models import NoteUser, Transaction
from rest_framework.exceptions import ValidationError
@ -310,31 +310,3 @@ class GuestTransaction(Transaction):
@property
def type(self):
return _('Invitation')
class Opener(models.Model):
"""
Allow the user to make activity entries without more rights
"""
activity = models.ForeignKey(
Activity,
on_delete=models.CASCADE,
related_name='opener',
verbose_name=_('activity')
)
opener = models.ForeignKey(
Note,
on_delete=models.CASCADE,
related_name='activity_responsible',
verbose_name=_('Opener')
)
class Meta:
verbose_name = _("Opener")
verbose_name_plural = _("Openers")
unique_together = ("opener", "activity")
def __str__(self):
return _("{opener} is opener of activity {acivity}").format(
opener=str(self.opener), acivity=str(self.activity))

View File

@ -1,57 +0,0 @@
/**
* On form submit, add a new opener
*/
function form_create_opener (e) {
// Do not submit HTML form
e.preventDefault()
// Get data and send to API
const formData = new FormData(e.target)
$.getJSON('/api/note/alias/'+formData.get('opener') + '/',
function (opener_alias) {
create_opener(formData.get('activity'), opener_alias.note)
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* Add an opener between an activity and a user
* @param activity:Integer activity id
* @param opener:Integer user note id
*/
function create_opener(activity, opener) {
$.post('/api/activity/opener/', {
activity: activity,
opener: opener,
csrfmiddlewaretoken: CSRF_TOKEN
}).done(function () {
// Reload tables
$('#opener_table').load(location.pathname + ' #opener_table')
addMsg(gettext('Opener successfully added'), 'success')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* On click of "delete", delete the opener
* @param button_id:Integer Opener id to remove
*/
function delete_button (button_id) {
$.ajax({
url: '/api/activity/opener/' + button_id + '/',
method: 'DELETE',
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () {
addMsg(gettext('Opener successfully deleted'), 'success')
$('#opener_table').load(location.pathname + ' #opener_table')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
$(document).ready(function () {
// Attach event
document.getElementById('form_opener').addEventListener('submit', form_create_opener)
})

View File

@ -1,17 +1,15 @@
# 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
from django.utils import timezone
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
import django_tables2 as tables
from django_tables2 import A
from permission.backends import PermissionBackend
from note.templatetags.pretty_money import pretty_money
from .models import Activity, Entry, Guest, Opener
from .models import Activity, Entry, Guest
class ActivityTable(tables.Table):
@ -115,34 +113,3 @@ class EntryTable(tables.Table):
'data-last-name': lambda record: record.last_name,
'data-first-name': lambda record: record.first_name,
}
# function delete_button(id) provided in template file
DELETE_TEMPLATE = """
<button id="{{ record.pk }}" class="btn btn-danger btn-sm" onclick="delete_button(this.id)"> {{ delete_trans }}</button>
"""
class OpenerTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped',
'id': "opener_table"
}
model = Opener
fields = ("opener",)
template_name = 'django_tables2/bootstrap4.html'
show_header = False
opener = tables.Column(attrs={'td': {'class': 'text-center'}})
delete_col = tables.TemplateColumn(
template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('Delete')},
attrs={
'td': {
'class': lambda record: 'col-sm-1'
+ (' d-none' if not PermissionBackend.check_perm(
get_current_request(), "activity.delete_opener", record)
else '')}},
verbose_name=_("Delete"),)

View File

@ -4,31 +4,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n perms %}
{% load render_table from django_tables2 %}
{% load static django_tables2 i18n %}
{% block content %}
<h1 class="text-white">{{ title }}</h1>
{% include "activity/includes/activity_info.html" %}
{% if activity.activity_type.manage_entries and ".change__opener"|has_perm:activity %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
{% trans "Openers" %}
</h3>
<div class="card-body">
<form class="input-group" method="POST" id="form_opener">
{% csrf_token %}
<input type="hidden" name="activity" value="{{ object.pk }}">
{%include "autocomplete_model.html" %}
<div class="input-group-append">
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
</div>
</form>
</div>
{% render_table opener %}
</div>
{% endif %}
{% if guests.data %}
<div class="card bg-white mb-3">
<h3 class="card-header text-center">
@ -42,8 +22,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %}
{% block extrajavascript %}
<script src="{% static "activity/js/opener.js" %}"></script>
<script src="{% static "js/autocomplete_model.js" %}"></script>
<script>
function remove_guest(guest_id) {
$.ajax({

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
from datetime import timedelta

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
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
from hashlib import md5
@ -17,16 +17,15 @@ 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.views.generic.list import ListView
from django_tables2.views import MultiTableMixin, SingleTableMixin
from django_tables2.views import SingleTableView
from api.viewsets import is_regex
from note.models import Alias, NoteSpecial, NoteUser
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from .forms import ActivityForm, GuestForm
from .models import Activity, Entry, Guest, Opener
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
from .models import Activity, Entry, Guest
from .tables import ActivityTable, EntryTable, GuestTable
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
@ -59,36 +58,27 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Displays all Activities, and classify if they are on-going or upcoming ones.
"""
model = Activity
tables = [
lambda data: ActivityTable(data, prefix="all-"),
lambda data: ActivityTable(data, prefix="upcoming-"),
]
table_class = ActivityTable
ordering = ('-date_start',)
extra_context = {"title": _("Activities")}
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).distinct()
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)
tables = context["tables"]
for name, table in zip(["table", "upcoming"], tables):
context[name] = table
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',
)
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
context["started_activities"] = started_activities
@ -96,7 +86,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
return context
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Shows details about one activity. Add guest to context
"""
@ -104,40 +94,15 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
context_object_name = "activity"
extra_context = {"title": _("Activity detail")}
tables = [
lambda data: GuestTable(data, prefix="guests-"),
lambda data: OpenerTable(data, prefix="opener-"),
]
def get_tables_data(self):
return [
Guest.objects.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")),
self.object.opener.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Opener, "view")),
]
def get_context_data(self, **kwargs):
context = super().get_context_data()
tables = context["tables"]
for name, table in zip(["guests", "opener"], tables):
context[name] = table
table = GuestTable(data=Guest.objects.filter(activity=self.object)
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
context["guests"] = table
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
context["widget"] = {
"name": "opener",
"resetable": True,
"attrs": {
"class": "autocomplete form-control",
"id": "opener",
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
"name_field": "name",
"placeholder": ""
}
}
return context
@ -194,14 +159,12 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
class ActivityEntryView(LoginRequiredMixin, TemplateView):
"""
Manages entry to an activity
"""
template_name = "activity/activity_entry.html"
table_class = EntryTable
def dispatch(self, request, *args, **kwargs):
"""
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
@ -265,11 +228,12 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
# Keep only users that have a note
note_qs = note_qs.filter(note__noteuser__isnull=False)
# Keep only valid members
# Keep only members
note_qs = note_qs.filter(
note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(),
note__noteuser__user__memberships__date_end__gte=timezone.now()).exclude(note__inactivity_reason='forced')
note__noteuser__user__memberships__date_end__gte=timezone.now(),
)
# Filter with permission backend
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
@ -296,9 +260,15 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
return note_qs
def get_table_data(self):
def get_context_data(self, **kwargs):
"""
Query the list of Guest and Note to the activity and add information to makes entry with JS.
"""
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
matched = []
@ -311,17 +281,8 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
note.activity = activity
matched.append(note)
return matched
def get_context_data(self, **kwargs):
"""
Query the list of Guest and Note to the activity and add information to makes entry with JS.
"""
context = super().get_context_data(**kwargs)
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
.distinct().get(pk=self.kwargs["pk"])
context["activity"] = activity
table = EntryTable(data=matched)
context["table"] = table
context["entries"] = Entry.objects.filter(activity=activity)
@ -329,7 +290,7 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).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()
context["activities_open"] = [a for a in activities_open
if PermissionBackend.check_perm(self.request,
@ -363,8 +324,8 @@ X-WR-CALNAME:Kfet Calendar
NAME:Kfet Calendar
CALSCALE:GREGORIAN
BEGIN:VTIMEZONE
TZID:Europe/Paris
X-LIC-LOCATION:Europe/Paris
TZID:Europe/Berlin
X-LIC-LOCATION:Europe/Berlin
BEGIN:DAYLIGHT
TZOFFSETFROM:+0100
TZOFFSETTO:+0200
@ -386,10 +347,10 @@ END:VTIMEZONE
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
UID:{md5((activity.name + "$" + str(activity.id) + str(activity.date_start)).encode("UTF-8")).hexdigest()}
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)}
DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
LOCATION:{self.multilines(activity.location, 75, 9) if activity.location else "Kfet"}
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f"""
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + """
-- {activity.organizer.name}
END:VEVENT
"""

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
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
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

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
import json

View File

@ -1,9 +1,8 @@
# 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
from django.conf import settings
from django.conf.urls import include
from django.urls import re_path
from django.conf.urls import url, include
from rest_framework import routers
from .views import UserInformationView
@ -15,48 +14,40 @@ router = routers.DefaultRouter()
router.register('models', ContentTypeViewSet)
router.register('user', UserViewSet)
if "activity" in settings.INSTALLED_APPS:
from activity.api.urls import register_activity_urls
register_activity_urls(router, 'activity')
if "food" in settings.INSTALLED_APPS:
from food.api.urls import register_food_urls
register_food_urls(router, 'food')
if "logs" in settings.INSTALLED_APPS:
from logs.api.urls import register_logs_urls
register_logs_urls(router, 'logs')
if "member" in settings.INSTALLED_APPS:
from member.api.urls import register_members_urls
register_members_urls(router, 'members')
if "member" in settings.INSTALLED_APPS:
from activity.api.urls import register_activity_urls
register_activity_urls(router, 'activity')
if "note" in settings.INSTALLED_APPS:
from note.api.urls import register_note_urls
register_note_urls(router, 'note')
if "permission" in settings.INSTALLED_APPS:
from permission.api.urls import register_permission_urls
register_permission_urls(router, 'permission')
if "treasury" in settings.INSTALLED_APPS:
from treasury.api.urls import register_treasury_urls
register_treasury_urls(router, 'treasury')
if "permission" in settings.INSTALLED_APPS:
from permission.api.urls import register_permission_urls
register_permission_urls(router, 'permission')
if "logs" in settings.INSTALLED_APPS:
from logs.api.urls import register_logs_urls
register_logs_urls(router, 'logs')
if "wei" in settings.INSTALLED_APPS:
from wei.api.urls import register_wei_urls
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'
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
re_path('^', include(router.urls)),
re_path('^me/', UserInformationView.as_view()),
re_path('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url('^', include(router.urls)),
url('^me/', UserInformationView.as_view()),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

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
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
import re

View File

View File

@ -1,37 +0,0 @@
# Copyright (C) 2018-2025 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

View File

@ -1,50 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import Allergen, BasicFood, QRCode, TransformedFood
class AllergenSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Allergen.
The djangorestframework plugin will analyse the model `Allergen` and parse all fields in the API.
"""
class Meta:
model = Allergen
fields = '__all__'
class BasicFoodSerializer(serializers.ModelSerializer):
"""
REST API Serializer for BasicFood.
The djangorestframework plugin will analyse the model `BasicFood` and parse all fields in the API.
"""
class Meta:
model = BasicFood
fields = '__all__'
class QRCodeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for QRCode.
The djangorestframework plugin will analyse the model `QRCode` and parse all fields in the API.
"""
class Meta:
model = QRCode
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,14 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import AllergenViewSet, BasicFoodViewSet, QRCodeViewSet, TransformedFoodViewSet
def register_food_urls(router, path):
"""
Configure router for Food REST API.
"""
router.register(path + '/allergen', AllergenViewSet)
router.register(path + '/basic_food', BasicFoodViewSet)
router.register(path + '/qrcode', QRCodeViewSet)
router.register(path + '/transformed_food', TransformedFoodViewSet)

View File

@ -1,61 +0,0 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from api.viewsets import ReadProtectedModelViewSet
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from .serializers import AllergenSerializer, BasicFoodSerializer, QRCodeSerializer, TransformedFoodSerializer
from ..models import Allergen, BasicFood, QRCode, TransformedFood
class AllergenViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Allergen` objects, serialize it to JSON with the given serializer,
then render it on /api/food/allergen/
"""
queryset = Allergen.objects.order_by('id')
serializer_class = AllergenSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class BasicFoodViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `BasicFood` objects, serialize it to JSON with the given serializer,
then render it on /api/food/basic_food/
"""
queryset = BasicFood.objects.order_by('id')
serializer_class = BasicFoodSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_fields = ['name', ]
search_fields = ['$name', ]
class QRCodeViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `QRCode` objects, serialize it to JSON with the given serializer,
then render it on /api/food/qrcode/
"""
queryset = QRCode.objects.order_by('id')
serializer_class = QRCodeSerializer
filter_backends = [DjangoFilterBackend, SearchFilter]
filterset_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,11 +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 _
from django.apps import AppConfig
class FoodkfetConfig(AppConfig):
name = 'food'
verbose_name = _('food')

View File

@ -1,114 +0,0 @@
# Copyright (C) 2018-2025 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 bootstrap_datepicker_plus.widgets import DateTimePickerInput
from note_kfet.inputs import Autocomplete
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(
polymorphic_ctype__model='transformedfood',
is_ready=False,
is_active=True,
was_eaten=False,
)
# 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:
model = TransformedFood
fields = ('ingredient', 'is_active')
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 METRO 5kg")})
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', 'is_active', 'was_eaten', '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_active=True,
was_eaten=False,
polymorphic_ctype__model='transformedfood',
)
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
self.fields['is_ready'].initial = False
self.fields['was_eaten'].initial = False
# 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', 'is_ready', 'was_eaten', 'shelf_life')
widgets = {
"owner": Autocomplete(
model=Club,
attrs={"api_url": "/api/members/club/"},
),
'creation_date': DateTimePickerInput(),
}

View File

@ -1,84 +0,0 @@
# 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

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

@ -1,62 +0,0 @@
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

@ -1,28 +0,0 @@
# 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,20 +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'),
('food', '0004_auto_20240813_2358'),
]
operations = [
migrations.AlterField(
model_name='food',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
]

View File

@ -1,226 +0,0 @@
# Copyright (C) 2018-2025 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.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):
"""
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 != 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(
default=False,
verbose_name=_('is ready'),
)
is_active = models.BooleanField(
default=True,
verbose_name=_('is active'),
)
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'),
)
# Without microbiological analyzes, the storage time is 3 days
shelf_life = models.DurationField(
verbose_name=_("shelf life"),
default=timedelta(days=3),
)
@transaction.atomic
def archive(self):
# When a meal are archived, if it was eaten, update ingredient fully used for this meal
raise NotImplementedError
@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')

View File

@ -1,19 +0,0 @@
# Copyright (C) 2018-2025 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', "owner", "allergens", "expiry_date")

View File

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

@ -1,37 +0,0 @@
{% 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,20 +0,0 @@
{% 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

@ -1,55 +0,0 @@
{% 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,39 +0,0 @@
{% 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

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

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

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

View File

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

View File

@ -1,21 +0,0 @@
# Copyright (C) 2018-2025 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.TransformedListView.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('<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'),
]

View File

@ -1,421 +0,0 @@
# Copyright (C) 2018-2025 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 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.list import ListView
from django.forms import HiddenInput
from permission.backends import PermissionBackend
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 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)
# We flip logic ""fully used = not is_active""
food.is_active = not food.is_active
# 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()
food.save()
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 update a basic food
"""
model = BasicFood
form_class = BasicFoodForms
template_name = 'food/basicfood_form.html'
extra_context = {"title": _("Update an 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})
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):
context = super().get_context_data(**kwargs)
context["can_update"] = PermissionBackend.check_perm(self.request, "food.change_food")
context["can_add_ingredient"] = PermissionBackend.check_perm(self.request, "food.change_transformedfood")
return context
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()
return context
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"]
context["last_basic"] = BasicFood.objects.order_by('-pk').all()[:10]
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.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"],
food_container_id=1
)
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))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
qr_code_number = self.kwargs['slug']
qrcode = self.model.objects.get(qr_code_number=qr_code_number)
model = qrcode.food_container.polymorphic_ctype.model
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
class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
"""
A view to add a tranformed food
"""
model = TransformedFood
template_name = 'food/transformedfood_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.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)
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})
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 TransformedFoodUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
A view to update transformed product
"""
model = TransformedFood
template_name = 'food/transformedfood_form.html'
form_class = TransformedFoodForms
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):
context = super().get_context_data(**kwargs)
return context
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")
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# We choose a club which should work
for membership in self.request.user.memberships.all():
club_id = membership.club.id
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

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
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
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
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
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
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
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
from django.contrib.contenttypes.models import ContentType
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
# noinspection PyProtectedMember
previous = instance._previous
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
request = get_current_request()
if request is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
ip = "127.0.0.1"
username = Alias.normalize(getpass.getuser())
note = NoteUser.objects.filter(alias__normalized_name=username)
@ -134,13 +134,13 @@ def delete_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
return
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
request = get_current_request()
if request is None:
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
ip = "127.0.0.1"
username = Alias.normalize(getpass.getuser())
note = NoteUser.objects.filter(alias__normalized_name=username)

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
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
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
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
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
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
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
from cas_server.auth import DjangoAuthUser # pragma: no cover

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
import io
from bootstrap_datepicker_plus.widgets import DatePickerInput
from PIL import Image, ImageSequence
from django import forms
from django.conf import settings
from django.contrib.auth.forms import AuthenticationForm
@ -13,9 +13,8 @@ from django.forms import CheckboxSelectMultiple
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, Alias
from note_kfet.inputs import Autocomplete, AmountInput
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask, Role
from PIL import Image, ImageSequence
from .models import Profile, Club, Membership
@ -33,7 +32,7 @@ class UserForm(forms.ModelForm):
# Django usernames can only contain letters, numbers, @, ., +, - and _.
# We want to allow users to have uncommon and unpractical usernames:
# That is their problem, and we have normalized aliases for us.
return super()._get_validation_exclusions() | {"username"}
return super()._get_validation_exclusions() + ["username"]
class Meta:
model = User
@ -44,7 +43,6 @@ class ProfileForm(forms.ModelForm):
"""
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"))
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
@ -77,8 +75,7 @@ class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
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', 'ml_sport_registration', )
exclude = ('user', 'email_confirmed', 'registration_valid', )
class ImageForm(forms.Form):
@ -141,9 +138,6 @@ 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):
@ -157,7 +151,7 @@ class ClubForm(forms.ModelForm):
class Meta:
model = Club
exclude = ("add_registration_form",)
fields = '__all__'
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),
@ -213,9 +207,9 @@ class MembershipForm(forms.ModelForm):
class Meta:
model = Membership
fields = ('user', 'date_start')
# Le champ d'utilisateur⋅rice est remplacé par un champ d'auto-complétion.
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les noms d'utilisateur⋅rices valides
# et récupère les noms d'utilisateur valides
widgets = {
'user':
Autocomplete(

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
import hashlib

View File

@ -1,18 +0,0 @@
# 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

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-01 12:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0012_club_add_registration_form'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2024, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

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
import datetime
@ -259,11 +259,6 @@ 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")
@ -295,14 +290,7 @@ class Club(models.Model):
today = datetime.date.today()
# Avoid any problems on February 29
if self.membership_start.month == 2 and self.membership_start.day == 29:
self.membership_start -= datetime.timedelta(days=1)
if self.membership_end.month == 2 and self.membership_end.day == 29:
self.membership_end += datetime.timedelta(days=1)
while today >= datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day):
while (today - self.membership_start).days >= 365:
if self.membership_start:
self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)
@ -480,10 +468,10 @@ class Membership(models.Model):
if self.club.parent_club.name == "BDE":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all())
Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
elif self.club.parent_club.name == "Kfet":
parent_membership.roles.set(
Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
else:
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
parent_membership.save()

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

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
from datetime import date
@ -42,12 +42,12 @@ class UserTable(tables.Table):
"""
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
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):
# 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 }}
</h3>
<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">
<label class="form-check-label" for="only_active">
<input type="checkbox" class="checkboxinput form-check-input" id="only_active"

View File

@ -14,9 +14,6 @@ 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

@ -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
from datetime import date

View File

@ -291,7 +291,7 @@ class TestMemberships(TestCase):
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
roles=[role.id for role in Role.objects.filter(
Q(name="Membre de club") | Q(name="Trésorière de club") | Q(name="Bureau de club")).all()],
Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()],
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.membership.refresh_from_db()

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
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
from datetime import timedelta, date
@ -16,7 +16,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from api.viewsets import is_regex
from note.models import Alias, NoteClub, NoteUser, Trust
@ -26,7 +26,6 @@ from note_kfet.middlewares import _set_current_request
from permission.backends import PermissionBackend
from permission.models import Role
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
from django import forms
from .forms import UserForm, ProfileForm, ImageForm, ClubForm, MembershipForm, \
CustomAuthenticationForm, MembershipRolesForm
@ -73,24 +72,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True
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):
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:
del 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
del context['profile_form'].fields["last_report"]
return context
@ -262,7 +248,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return context
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user trust relationships
"""
@ -271,25 +257,13 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
context_object_name = 'user_object'
extra_context = {"title": _("Note friendships")}
tables = [
lambda data: TrustTable(data, prefix="trust-"),
lambda data: TrustedTable(data, prefix="trusted-"),
]
def get_tables_data(self):
note = self.object.note
return [
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tables = context["tables"]
for name, table in zip(["trusting", "trusted_by"], tables):
context[name] = table
note = context['object'].note
context["trusting"] = TrustTable(
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["trusted_by"] = TrustedTable(
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
trusting=context["object"].note,
trusted=context["object"].note
@ -308,7 +282,7 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user aliases.
"""
@ -317,15 +291,12 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixi
context_object_name = 'user_object'
extra_context = {"title": _("Note aliases")}
table_class = AliasTable
context_table_name = "aliases"
def get_table_data(self):
return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
.order_by('normalized_name')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
.order_by('normalized_name').all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note,
name="",
@ -360,9 +331,6 @@ 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":
@ -549,7 +517,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
return context
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Manage aliases of a club.
"""
@ -558,16 +526,11 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin,
context_object_name = 'club'
extra_context = {"title": _("Note aliases")}
table_class = AliasTable
context_table_name = "aliases"
def get_table_data(self):
return self.object.note.alias.filter(
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["aliases"] = AliasTable(note.alias.filter(
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note,
name="",
@ -871,8 +834,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
ret = super().form_valid(form)
member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
# Set the same roles as before
if old_membership:
@ -908,7 +871,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
membership.refresh_from_db()
if old_membership.exists():
membership.roles.set(old_membership.get().roles.all())
membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
membership.save()
return ret

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
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
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
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
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
from django.conf import settings
@ -183,10 +183,19 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
# We match first an alias if it is matched without normalization,
# then if the normalized pattern matches a normalized alias.
queryset = queryset.filter(
Q(**{f'name{suffix}': alias_prefix + alias})
| Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
| Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
)
**{f'name{suffix}': alias_prefix + alias}
).union(
queryset.filter(
Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
& ~Q(**{f'name{suffix}': alias_prefix + alias})
),
all=True).union(
queryset.filter(
Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
& ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
& ~Q(**{f'name{suffix}': alias_prefix + alias})
),
all=True)
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
else queryset.order_by("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
from django.apps import AppConfig

View File

@ -1,14 +1,13 @@
# 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
from datetime import datetime
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.forms import CheckboxSelectMultiple
from django.utils.timezone import make_aware
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import Autocomplete, AmountInput
from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput
from .models import TransactionTemplate, NoteClub, Alias

View File

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

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
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
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
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
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
// 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',
polymorphic_ctype: type,
resourcetype: 'RecurrentTransaction',
source: source.id,
source: source,
source_alias: source_alias,
destination: dest,
template: template
@ -294,10 +294,3 @@ searchbar.addEventListener("keyup", function (e) {
if (firstMatch && e.key === "Enter")
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
import html
@ -260,13 +260,11 @@ class ButtonTable(tables.Table):
text=_('edit'),
accessor='pk',
verbose_name=_("Edit"),
orderable=False,
)
hideshow = tables.Column(
verbose_name=_("Hide/Show"),
accessor="pk",
orderable=False,
attrs={
'td': {
'class': 'col-sm-1',
@ -278,8 +276,7 @@ class ButtonTable(tables.Table):
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}},
verbose_name=_("Delete"),
orderable=False, )
verbose_name=_("Delete"), )
def render_amount(self, value):
return pretty_money(value)

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
name="{{ widget.name }}"
{# Other attributes are loaded #}
{% for name, value in widget.attrs.items %}
{% if value is not False %}{{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
<div class="input-group-append">
<span class="input-group-text"></span>

View File

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

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
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
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
from api.tests import TestAPI

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