mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 18:08:21 +02:00
Compare commits
1 Commits
e27864d4aa
...
svg_icons
Author | SHA1 | Date | |
---|---|---|---|
466cbd9878 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,7 +42,6 @@ map.json
|
||||
backups/
|
||||
/static/
|
||||
/media/
|
||||
/tmp/
|
||||
|
||||
# Virtualenv
|
||||
env/
|
||||
|
@ -7,10 +7,25 @@ stages:
|
||||
variables:
|
||||
GIT_SUBMODULE_STRATEGY: recursive
|
||||
|
||||
# Ubuntu 22.04
|
||||
py310-django42:
|
||||
# Debian Buster
|
||||
py37-django22:
|
||||
stage: test
|
||||
image: ubuntu:22.04
|
||||
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: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:buster-backports
|
||||
before_script:
|
||||
- apt-get update && apt-get install -y tox
|
||||
script: tox -e linters
|
||||
|
2
.gitmodules
vendored
2
.gitmodules
vendored
@ -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
|
||||
|
@ -12,7 +12,7 @@ RUN apt-get update && \
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
|
||||
python3-bs4 python3-setuptools \
|
||||
uwsgi uwsgi-plugin-python3 \
|
||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \
|
||||
texlive-xetex gettext libjs-bootstrap4 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Instal PyPI requirements
|
||||
|
10
README.md
10
README.md
@ -1,8 +1,8 @@
|
||||
# NoteKfet 2020
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
[](https://gitlab.crans.org/bde/nk20/commits/main)
|
||||
[](https://gitlab.crans.org/bde/nk20/commits/main)
|
||||
[](https://gitlab.crans.org/bde/nk20/commits/master)
|
||||
[](https://gitlab.crans.org/bde/nk20/commits/master)
|
||||
|
||||
## Table des matières
|
||||
|
||||
@ -23,7 +23,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
|
||||
$ sudo apt update
|
||||
$ sudo apt install --no-install-recommends -y \
|
||||
ipython3 python3-setuptools python3-venv python3-dev \
|
||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git
|
||||
texlive-xetex gettext libjs-bootstrap4 git
|
||||
```
|
||||
|
||||
2. **Clonage du dépot** là où vous voulez :
|
||||
@ -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 :
|
||||
@ -115,7 +115,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
|
||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
|
||||
python3-bs4 python3-setuptools python3-docutils \
|
||||
memcached uwsgi uwsgi-plugin-python3 \
|
||||
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
|
||||
texlive-xetex gettext libjs-bootstrap4 \
|
||||
nginx python3-venv git acl
|
||||
```
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
prompt: "Password of the database (leave it blank to skip database init)"
|
||||
private: yes
|
||||
vars:
|
||||
mirror: eclats.crans.org
|
||||
mirror: mirror.crans.org
|
||||
roles:
|
||||
- 1-apt-basic
|
||||
- 2-nk20
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
note:
|
||||
server_name: note.crans.org
|
||||
git_branch: main
|
||||
git_branch: master
|
||||
serve_static: true
|
||||
cron_enabled: true
|
||||
email: notekfet2020@lists.crans.org
|
||||
|
@ -1,15 +1,14 @@
|
||||
---
|
||||
- name: Add buster-backports to apt sources if needed
|
||||
- name: Add buster-backports to apt sources
|
||||
apt_repository:
|
||||
repo: deb http://{{ mirror }}/debian buster-backports main
|
||||
state: present
|
||||
when:
|
||||
- ansible_distribution == "Debian"
|
||||
- ansible_distribution_major_version | int == 10
|
||||
when: ansible_facts['distribution'] == "Debian"
|
||||
|
||||
- name: Install note_kfet APT dependencies
|
||||
apt:
|
||||
update_cache: true
|
||||
default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
|
||||
install_recommends: false
|
||||
name:
|
||||
# Common tools
|
||||
@ -18,7 +17,6 @@
|
||||
- ipython3
|
||||
|
||||
# Front-end dependencies
|
||||
- fonts-font-awesome
|
||||
- libjs-bootstrap4
|
||||
|
||||
# Python dependencies
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'activity.apps.ActivityConfig'
|
||||
|
@ -1,11 +1,11 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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')
|
||||
|
@ -1,11 +1,9 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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"))]
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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)
|
||||
|
@ -1,15 +1,12 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
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 rest_framework.filters import SearchFilter
|
||||
|
||||
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):
|
||||
@ -32,7 +29,7 @@ class ActivityViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Activity.objects.order_by('id')
|
||||
serializer_class = ActivitySerializer
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
||||
'date_start', 'date_end', 'valid', 'open', ]
|
||||
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
||||
@ -50,7 +47,7 @@ class GuestViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Guest.objects.order_by('id')
|
||||
serializer_class = GuestSerializer
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
||||
'inviter__alias__normalized_name', ]
|
||||
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name',
|
||||
@ -65,36 +62,7 @@ class EntryViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Entry.objects.order_by('id')
|
||||
serializer_class = EntrySerializer
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
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)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -6,7 +6,7 @@
|
||||
"name": "Pot",
|
||||
"manage_entries": true,
|
||||
"can_invite": true,
|
||||
"guest_entry_fee": 1000
|
||||
"guest_entry_fee": 500
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -28,25 +28,5 @@
|
||||
"can_invite": false,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "activity.activitytype",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Soir\u00e9e avec entrées",
|
||||
"manage_entries": true,
|
||||
"can_invite": false,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "activity.activitytype",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Soir\u00e9e avec invitations",
|
||||
"manage_entries": true,
|
||||
"can_invite": true,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -1,17 +1,16 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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,
|
||||
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.28 on 2024-03-23 13:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('activity', '0002_auto_20200904_2341'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='activity',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, default='', verbose_name='description'),
|
||||
),
|
||||
]
|
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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
|
||||
|
||||
|
||||
@ -66,8 +66,6 @@ class Activity(models.Model):
|
||||
|
||||
description = models.TextField(
|
||||
verbose_name=_('description'),
|
||||
blank=True,
|
||||
default="",
|
||||
)
|
||||
|
||||
location = models.CharField(
|
||||
@ -125,14 +123,6 @@ class Activity(models.Model):
|
||||
verbose_name=_('open'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("activity")
|
||||
verbose_name_plural = _("activities")
|
||||
unique_together = ("name", "date_start", "date_end",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
@ -154,6 +144,14 @@ class Activity(models.Model):
|
||||
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
|
||||
return ret
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("activity")
|
||||
verbose_name_plural = _("activities")
|
||||
unique_together = ("name", "date_start", "date_end",)
|
||||
|
||||
|
||||
class Entry(models.Model):
|
||||
"""
|
||||
@ -254,13 +252,14 @@ class Guest(models.Model):
|
||||
verbose_name=_("inviter"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("guest")
|
||||
verbose_name_plural = _("guests")
|
||||
unique_together = ("activity", "last_name", "first_name", )
|
||||
|
||||
def __str__(self):
|
||||
return self.first_name + " " + self.last_name
|
||||
@property
|
||||
def has_entry(self):
|
||||
try:
|
||||
if self.entry:
|
||||
return True
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
@ -291,14 +290,13 @@ class Guest(models.Model):
|
||||
|
||||
return super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
@property
|
||||
def has_entry(self):
|
||||
try:
|
||||
if self.entry:
|
||||
return True
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
def __str__(self):
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("guest")
|
||||
verbose_name_plural = _("guests")
|
||||
unique_together = ("activity", "last_name", "first_name", )
|
||||
|
||||
|
||||
class GuestTransaction(Transaction):
|
||||
@ -310,31 +308,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))
|
||||
|
@ -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)
|
||||
})
|
@ -1,17 +1,15 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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"),)
|
||||
|
@ -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({
|
||||
|
@ -18,26 +18,3 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
var date_end = document.getElementById("id_date_end");
|
||||
var date_start = document.getElementById("id_date_start");
|
||||
|
||||
function update_date_end (){
|
||||
if(date_end.value=="" || date_end.value<date_start.value){
|
||||
date_end.value = date_start.value;
|
||||
};
|
||||
};
|
||||
|
||||
function update_date_start (){
|
||||
if(date_start.value=="" || date_end.value<date_start.value){
|
||||
date_start.value = date_end.value;
|
||||
};
|
||||
};
|
||||
|
||||
date_start.addEventListener('focusout', update_date_end);
|
||||
date_end.addEventListener('focusout', update_date_start);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -34,7 +34,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endif %}
|
||||
<div class="card-footer">
|
||||
<a class="btn btn-sm btn-success" href="{% url 'activity:activity_create' %}" data-turbolinks="false">
|
||||
<i class="fa fa-calendar-plus-o" aria-hidden="true"></i>
|
||||
<svg class="bi bi-calendar-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4 .5a.5.5 0 0 0-1 0V1H2a2 2 0 0 0-2 2v1h16V3a2 2 0 0 0-2-2h-1V.5a.5.5 0 0 0-1 0V1H4V.5zM16 14V5H0v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2zM8.5 8.5V10H10a.5.5 0 0 1 0 1H8.5v1.5a.5.5 0 0 1-1 0V11H6a.5.5 0 0 1 0-1h1.5V8.5a.5.5 0 0 1 1 0z"/>
|
||||
</svg>
|
||||
{% trans 'New activity' %}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import timedelta
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from hashlib import md5
|
||||
@ -17,16 +17,14 @@ 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 api.viewsets import is_regex
|
||||
from django_tables2.views import SingleTableView
|
||||
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 +57,26 @@ 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-',
|
||||
)
|
||||
|
||||
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||
context["started_activities"] = started_activities
|
||||
@ -96,7 +84,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 +92,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,22 +157,17 @@ 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),
|
||||
it is closed or doesn't manage entries.
|
||||
"""
|
||||
if not self.request.user.is_authenticated:
|
||||
return self.handle_no_permission()
|
||||
|
||||
activity = Activity.objects.get(pk=self.kwargs["pk"])
|
||||
|
||||
sample_entry = Entry(activity=activity, note=self.request.user.note)
|
||||
@ -236,16 +194,13 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
|
||||
if pattern[0] != "^":
|
||||
pattern = "^" + pattern
|
||||
guest_qs = guest_qs.filter(
|
||||
Q(**{f"first_name{suffix}": pattern})
|
||||
| Q(**{f"last_name{suffix}": pattern})
|
||||
| Q(**{f"inviter__alias__name{suffix}": pattern})
|
||||
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
|
||||
Q(first_name__iregex=pattern)
|
||||
| Q(last_name__iregex=pattern)
|
||||
| Q(inviter__alias__name__iregex=pattern)
|
||||
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||
)
|
||||
else:
|
||||
guest_qs = guest_qs.none()
|
||||
@ -265,26 +220,23 @@ 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"))
|
||||
|
||||
if "search" in self.request.GET and self.request.GET["search"]:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__icontains"
|
||||
note_qs = note_qs.filter(
|
||||
Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
|
||||
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
|
||||
| Q(**{f"name{suffix}": pattern})
|
||||
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
|
||||
Q(note__noteuser__user__first_name__iregex=pattern)
|
||||
| Q(note__noteuser__user__last_name__iregex=pattern)
|
||||
| Q(name__iregex=pattern)
|
||||
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
||||
)
|
||||
else:
|
||||
note_qs = note_qs.none()
|
||||
@ -296,9 +248,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 +269,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 +278,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 +312,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 +335,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
|
||||
"""
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'api.apps.APIConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,42 +0,0 @@
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
from rest_framework.filters import SearchFilter
|
||||
|
||||
|
||||
class RegexSafeSearchFilter(SearchFilter):
|
||||
@lru_cache
|
||||
def validate_regex(self, search_term) -> bool:
|
||||
try:
|
||||
re.compile(search_term)
|
||||
return True
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
def get_search_fields(self, view, request):
|
||||
"""
|
||||
Ensure that given regex are valid.
|
||||
If not, we consider that the user is trying to search by substring.
|
||||
"""
|
||||
search_fields = super().get_search_fields(view, request)
|
||||
search_terms = self.get_search_terms(request)
|
||||
|
||||
for search_term in search_terms:
|
||||
if not self.validate_regex(search_term):
|
||||
# Invalid regex. We assume we don't query by regex but by substring.
|
||||
search_fields = [f.replace('$', '') for f in search_fields]
|
||||
break
|
||||
|
||||
return search_fields
|
||||
|
||||
def get_search_terms(self, request):
|
||||
"""
|
||||
Ensure that search field is a valid regex query. If not, we remove extra characters.
|
||||
"""
|
||||
terms = super().get_search_terms(request)
|
||||
if not all(self.validate_regex(term) for term in terms):
|
||||
# Invalid regex. If a ^ is prefixed to the search term, we remove it.
|
||||
terms = [term[1:] if term[0] == '^' else term for term in terms]
|
||||
# Same for dollars.
|
||||
terms = [term[:-1] if term[-1] == '$' else term for term in terms]
|
||||
return terms
|
@ -1,5 +0,0 @@
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
|
||||
class CustomPagination(PageNumberPagination):
|
||||
page_size_query_param = 'page_size'
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
@ -7,11 +7,8 @@ from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
from member.api.serializers import ProfileSerializer, MembershipSerializer
|
||||
from member.models import Membership
|
||||
from note.api.serializers import NoteSerializer
|
||||
from note.models import Alias
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
@ -48,30 +45,18 @@ class OAuthSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
normalized_name = serializers.SerializerMethodField()
|
||||
|
||||
profile = serializers.SerializerMethodField()
|
||||
profile = ProfileSerializer()
|
||||
|
||||
note = serializers.SerializerMethodField()
|
||||
note = NoteSerializer()
|
||||
|
||||
memberships = serializers.SerializerMethodField()
|
||||
|
||||
def get_normalized_name(self, obj):
|
||||
return Alias.normalize(obj.username)
|
||||
|
||||
def get_profile(self, obj):
|
||||
# Display the profile of the user only if we have rights to see it.
|
||||
return ProfileSerializer().to_representation(obj.profile) \
|
||||
if PermissionBackend.check_perm(get_current_request(), 'member.view_profile', obj.profile) else None
|
||||
|
||||
def get_note(self, obj):
|
||||
# Display the note of the user only if we have rights to see it.
|
||||
return NoteSerializer().to_representation(obj.note) \
|
||||
if PermissionBackend.check_perm(get_current_request(), 'note.view_note', obj.note) else None
|
||||
|
||||
def get_memberships(self, obj):
|
||||
# Display only memberships that we are allowed to see.
|
||||
return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
|
||||
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now())
|
||||
.filter(PermissionBackend.filter_queryset(get_current_request(), Membership, 'view')))
|
||||
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import json
|
||||
@ -12,12 +12,11 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.fields.files import ImageFieldFile
|
||||
from django.test import TestCase
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from phonenumbers import PhoneNumber
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from member.models import Membership, Club
|
||||
from note.models import NoteClub, NoteUser, Alias, Note
|
||||
from permission.models import PermissionMask, Permission, Role
|
||||
from phonenumbers import PhoneNumber
|
||||
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||
|
||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||
|
||||
@ -88,7 +87,7 @@ class TestAPI(TestCase):
|
||||
resp = self.client.get(url + f"?ordering=-{field}")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
if RegexSafeSearchFilter in backends:
|
||||
if SearchFilter in backends:
|
||||
# Basic search
|
||||
for field in viewset.search_fields:
|
||||
obj = self.fix_note_object(obj, field)
|
||||
|
@ -1,9 +1,8 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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')),
|
||||
]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
@ -1,29 +1,19 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django.db.models import Q
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
||||
from permission.backends import PermissionBackend
|
||||
from note.models import Alias
|
||||
|
||||
from .filters import RegexSafeSearchFilter
|
||||
from .serializers import UserSerializer, ContentTypeSerializer
|
||||
|
||||
|
||||
def is_regex(pattern):
|
||||
try:
|
||||
re.compile(pattern)
|
||||
return True
|
||||
except (re.error, TypeError):
|
||||
return False
|
||||
|
||||
|
||||
class ReadProtectedModelViewSet(ModelViewSet):
|
||||
"""
|
||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
||||
@ -70,38 +60,34 @@ class UserViewSet(ReadProtectedModelViewSet):
|
||||
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
# Check if this is a valid regex. If not, we won't check regex
|
||||
valid_regex = is_regex(pattern)
|
||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||
prefix = "^" if valid_regex else ""
|
||||
|
||||
# Filter with different rules
|
||||
# We use union-all to keep each filter rule sorted in result
|
||||
queryset = queryset.filter(
|
||||
# Match without normalization
|
||||
Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
note__alias__name__iregex="^" + pattern
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match with normalization
|
||||
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
),
|
||||
all=True,
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match on lower pattern
|
||||
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
),
|
||||
all=True,
|
||||
).union(
|
||||
queryset.filter(
|
||||
# Match on firstname or lastname
|
||||
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||
),
|
||||
all=True,
|
||||
)
|
||||
@ -121,6 +107,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||
"""
|
||||
queryset = ContentType.objects.order_by('id')
|
||||
serializer_class = ContentTypeSerializer
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['id', 'app_label', 'model', ]
|
||||
search_fields = ['$app_label', '$model', ]
|
||||
|
@ -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
|
@ -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__'
|
@ -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)
|
@ -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', ]
|
@ -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')
|
@ -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(),
|
||||
}
|
@ -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',),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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),
|
||||
]
|
||||
|
||||
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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')
|
@ -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")
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -1,3 +0,0 @@
|
||||
# from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -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'),
|
||||
]
|
@ -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
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'logs.apps.LogsConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ChangelogViewSet
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
@ -76,6 +76,9 @@ class Changelog(models.Model):
|
||||
verbose_name=_('timestamp'),
|
||||
)
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
raise ValidationError(_("Logs cannot be destroyed."))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("changelog")
|
||||
verbose_name_plural = _("changelogs")
|
||||
@ -83,6 +86,3 @@ class Changelog(models.Model):
|
||||
def __str__(self):
|
||||
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
|
||||
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
raise ValidationError(_("Logs cannot be destroyed."))
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'member.apps.MemberConfig'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
||||
|
@ -1,9 +1,8 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from api.filters import RegexSafeSearchFilter
|
||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||
@ -18,7 +17,7 @@ class ProfileViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Profile.objects.order_by('id')
|
||||
serializer_class = ProfileSerializer
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
|
||||
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
|
||||
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
||||
@ -35,7 +34,7 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Club.objects.order_by('id')
|
||||
serializer_class = ClubSerializer
|
||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
||||
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
||||
'membership_duration', 'membership_start', 'membership_end', ]
|
||||
@ -50,7 +49,7 @@ class MembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
queryset = Membership.objects.order_by('id')
|
||||
serializer_class = MembershipSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name', 'club__note__alias__normalized_name',
|
||||
'user__username', 'user__last_name', 'user__first_name', 'user__email',
|
||||
'user__note__alias__name', 'user__note__alias__normalized_name',
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
||||
|
@ -1,9 +1,9 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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,18 +43,10 @@ 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"))
|
||||
|
||||
VSS_charter_read = forms.BooleanField(
|
||||
required=True,
|
||||
label=_("Anti-VSS (<em>Violences Sexistes et Sexuelles</em>) charter read and approved"),
|
||||
help_text=_("Tick after having read and accepted the anti-VSS charter \
|
||||
<a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available here in pdf</a>")
|
||||
)
|
||||
|
||||
def clean_promotion(self):
|
||||
promotion = self.cleaned_data["promotion"]
|
||||
if promotion > timezone.now().year:
|
||||
@ -77,8 +68,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):
|
||||
@ -124,7 +114,7 @@ class ImageForm(forms.Form):
|
||||
frame = frame.crop((x, y, x + w, y + h))
|
||||
frame = frame.resize(
|
||||
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
|
||||
Image.LANCZOS,
|
||||
Image.ANTIALIAS,
|
||||
)
|
||||
frames.append(frame)
|
||||
|
||||
@ -141,9 +131,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 +144,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 +200,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(
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import hashlib
|
||||
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.24 on 2021-10-05 13:44
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0007_auto_20210313_1235'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='department',
|
||||
field=models.CharField(choices=[('A0', 'Informatics (A0)'), ('A1', 'Mathematics (A1)'), ('A2', 'Physics (A2)'), ("A'2", "Applied physics (A'2)"), ("A''2", "Chemistry (A''2)"), ('A3', 'Biology (A3)'), ('B1234', 'SAPHIRE (B1234)'), ('B1', 'Mechanics (B1)'), ('B2', 'Civil engineering (B2)'), ('B3', 'Mechanical engineering (B3)'), ('B4', 'EEA (B4)'), ('C', 'Design (C)'), ('D2', 'Economy-management (D2)'), ('D3', 'Social sciences (D3)'), ('E', 'English (E)'), ('EXT', 'External (EXT)')], max_length=8, verbose_name='department'),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.26 on 2022-09-04 21:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0008_auto_20211005_1544'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='promotion',
|
||||
field=models.PositiveSmallIntegerField(default=2022, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.28 on 2023-08-23 21:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0009_auto_20220904_2325'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='promotion',
|
||||
field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.28 on 2023-08-31 09:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0010_new_default_year'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='VSS_charter_read',
|
||||
field=models.BooleanField(default=False, verbose_name='VSS charter read'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import datetime
|
||||
@ -28,6 +28,7 @@ class Profile(models.Model):
|
||||
We do not want to patch the Django Contrib :model:`auth.User`model;
|
||||
so this model add an user profile with additional information.
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
@ -133,22 +134,6 @@ class Profile(models.Model):
|
||||
default=False,
|
||||
)
|
||||
|
||||
VSS_charter_read = models.BooleanField(
|
||||
verbose_name=_("VSS charter read"),
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user profile')
|
||||
verbose_name_plural = _('user profile')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('member:user_detail', args=(self.user_id,))
|
||||
|
||||
@property
|
||||
def ens_year(self):
|
||||
"""
|
||||
@ -173,6 +158,17 @@ class Profile(models.Model):
|
||||
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user profile')
|
||||
verbose_name_plural = _('user profile')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('member:user_detail', args=(self.user_id,))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
def send_email_validation_link(self):
|
||||
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
|
||||
token = email_validation_token.make_token(self.user)
|
||||
@ -204,11 +200,9 @@ class Club(models.Model):
|
||||
max_length=255,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
email = models.EmailField(
|
||||
verbose_name=_('email'),
|
||||
)
|
||||
|
||||
parent_club = models.ForeignKey(
|
||||
'self',
|
||||
null=True,
|
||||
@ -259,17 +253,23 @@ 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,
|
||||
)
|
||||
def update_membership_dates(self):
|
||||
"""
|
||||
This function is called each time the club detail view is displayed.
|
||||
Update the year of the membership dates.
|
||||
"""
|
||||
if not self.membership_start:
|
||||
return
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("club")
|
||||
verbose_name_plural = _("clubs")
|
||||
today = datetime.date.today()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
if (today - self.membership_start).days >= 365:
|
||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||
self.membership_start.month, self.membership_start.day)
|
||||
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||
self.membership_end.month, self.membership_end.day)
|
||||
self._force_save = True
|
||||
self.save(force_update=True)
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
@ -282,36 +282,16 @@ class Club(models.Model):
|
||||
self.membership_end = None
|
||||
super().save(force_insert, force_update, update_fields)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("club")
|
||||
verbose_name_plural = _("clubs")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('member:club_detail', args=(self.pk,))
|
||||
|
||||
def update_membership_dates(self):
|
||||
"""
|
||||
This function is called each time the club detail view is displayed.
|
||||
Update the year of the membership dates.
|
||||
"""
|
||||
if not self.membership_start or not self.membership_end:
|
||||
return
|
||||
|
||||
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):
|
||||
if self.membership_start:
|
||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||
self.membership_start.month, self.membership_start.day)
|
||||
if self.membership_end:
|
||||
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||
self.membership_end.month, self.membership_end.day)
|
||||
self._force_save = True
|
||||
self.save(force_update=True)
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
"""
|
||||
@ -351,66 +331,6 @@ class Membership(models.Model):
|
||||
verbose_name=_('fee'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('membership')
|
||||
verbose_name_plural = _('memberships')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
||||
def __str__(self):
|
||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||
"""
|
||||
# Ensure that club membership dates are valid
|
||||
old_membership_start = self.club.membership_start
|
||||
self.club.update_membership_dates()
|
||||
if self.club.membership_start != old_membership_start:
|
||||
self.club.save()
|
||||
|
||||
created = not self.pk
|
||||
if not created:
|
||||
for role in self.roles.all():
|
||||
club = role.for_club
|
||||
if club is not None:
|
||||
if club.pk != self.club_id:
|
||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||
.format(role=role.name, club=club.name))
|
||||
else:
|
||||
if Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club,
|
||||
date_start__lte=self.date_start,
|
||||
date_end__gte=self.date_start,
|
||||
).exists():
|
||||
raise ValidationError(_('User is already a member of the club'))
|
||||
|
||||
if self.club.parent_club is not None:
|
||||
# Check that the user is already a member of the parent club if the membership is created
|
||||
if not Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club.parent_club,
|
||||
date_start__gte=self.club.parent_club.membership_start,
|
||||
).exists():
|
||||
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||
self.renew_parent()
|
||||
else:
|
||||
raise ValidationError(_('User is not a member of the parent club')
|
||||
+ ' ' + self.club.parent_club.name)
|
||||
|
||||
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
||||
|
||||
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
||||
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
||||
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
||||
self.date_end = self.club.membership_end
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
self.make_transaction()
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
"""
|
||||
@ -480,14 +400,66 @@ 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()
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||
"""
|
||||
# Ensure that club membership dates are valid
|
||||
old_membership_start = self.club.membership_start
|
||||
self.club.update_membership_dates()
|
||||
if self.club.membership_start != old_membership_start:
|
||||
self.club.save()
|
||||
|
||||
created = not self.pk
|
||||
if not created:
|
||||
for role in self.roles.all():
|
||||
club = role.for_club
|
||||
if club is not None:
|
||||
if club.pk != self.club_id:
|
||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||
.format(role=role.name, club=club.name))
|
||||
else:
|
||||
if Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club,
|
||||
date_start__lte=self.date_start,
|
||||
date_end__gte=self.date_start,
|
||||
).exists():
|
||||
raise ValidationError(_('User is already a member of the club'))
|
||||
|
||||
if self.club.parent_club is not None:
|
||||
# Check that the user is already a member of the parent club if the membership is created
|
||||
if not Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club.parent_club,
|
||||
date_start__gte=self.club.parent_club.membership_start,
|
||||
).exists():
|
||||
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||
self.renew_parent()
|
||||
else:
|
||||
raise ValidationError(_('User is not a member of the parent club')
|
||||
+ ' ' + self.club.parent_club.name)
|
||||
|
||||
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
||||
|
||||
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
||||
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
||||
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
||||
self.date_end = self.club.membership_end
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
self.make_transaction()
|
||||
|
||||
def make_transaction(self):
|
||||
"""
|
||||
Create Membership transaction associated to this membership.
|
||||
@ -525,3 +497,11 @@ class Membership(models.Model):
|
||||
soge_credit.save()
|
||||
else:
|
||||
transaction.save(force_insert=True)
|
||||
|
||||
def __str__(self):
|
||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('membership')
|
||||
verbose_name_plural = _('memberships')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
@ -1,64 +0,0 @@
|
||||
/**
|
||||
* On form submit, create a new friendship
|
||||
*/
|
||||
function form_create_trust (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('trusted') + '/',
|
||||
function (trusted_alias) {
|
||||
if ((trusted_alias.note == formData.get('trusting')))
|
||||
{
|
||||
addMsg(gettext("You can't add yourself as a friend"), "danger")
|
||||
return
|
||||
}
|
||||
create_trust(formData.get('trusting'), trusted_alias.note)
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a trust between users
|
||||
* @param trusting:Integer trusting note id
|
||||
* @param trusted:Integer trusted note id
|
||||
*/
|
||||
function create_trust(trusting, trusted) {
|
||||
$.post('/api/note/trust/', {
|
||||
trusting: trusting,
|
||||
trusted: trusted,
|
||||
csrfmiddlewaretoken: CSRF_TOKEN
|
||||
}).done(function () {
|
||||
// Reload tables
|
||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||
$('#trusted_table').load(location.pathname + ' #trusted_table')
|
||||
addMsg(gettext('Friendship successfully added'), 'success')
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On click of "delete", delete the trust
|
||||
* @param button_id:Integer Trust id to remove
|
||||
*/
|
||||
function delete_button (button_id) {
|
||||
$.ajax({
|
||||
url: '/api/note/trust/' + button_id + '/',
|
||||
method: 'DELETE',
|
||||
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
||||
}).done(function () {
|
||||
addMsg(gettext('Friendship successfully deleted'), 'success')
|
||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||
$('#trusted_table').load(location.pathname + ' #trusted_table')
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
// Attach event
|
||||
document.getElementById('form_trust').addEventListener('submit', form_create_trust)
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 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
|
||||
@ -120,7 +120,7 @@ class MembershipTable(tables.Table):
|
||||
club=record.club,
|
||||
user=record.user,
|
||||
date_start__gte=record.club.membership_start,
|
||||
date_end__lte=record.club.membership_end or date(9999, 12, 31),
|
||||
date_end__lte=record.club.membership_end,
|
||||
).exists(): # If the renew is not yet performed
|
||||
empty_membership = Membership(
|
||||
club=record.club,
|
||||
|
@ -45,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card-footer">
|
||||
{% if user_object %}
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'member:user_update_profile' user_object.pk %}">
|
||||
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans 'Update Profile' %}
|
||||
</a>
|
||||
{% url 'member:user_detail' user_object.pk as user_profile_url %}
|
||||
{% if request.path_info != user_profile_url %}
|
||||
@ -59,7 +62,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% if ".change_"|has_perm:club %}
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'member:club_update' pk=club.pk %}"
|
||||
data-turbolinks="false">
|
||||
<i class="fa fa-edit"></i> {% trans 'Update Profile' %}
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans 'Update Profile' %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% url 'member:club_detail' club.pk as club_detail_url %}
|
||||
|
@ -10,7 +10,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="font-weight-bold">
|
||||
<i class="fa fa-users"></i> {% trans "Club managers" %}
|
||||
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
</svg>
|
||||
{% trans "Club managers" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table managers %}
|
||||
@ -23,7 +26,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
|
||||
<i class="fa fa-users"></i> {% trans "Club members" %}
|
||||
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
|
||||
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
|
||||
</svg>
|
||||
{% trans "Club members" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table member_list %}
|
||||
@ -37,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card-header position-relative" id="historyListHeading">
|
||||
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %}
|
||||
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
|
||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
||||
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
|
||||
</svg>
|
||||
{% trans "Transaction history" %}
|
||||
</a>
|
||||
</div>
|
||||
<div id="history_list">
|
||||
|
@ -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"
|
||||
|
@ -47,7 +47,9 @@
|
||||
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
|
||||
</a>
|
||||
</dd>
|
||||
|
@ -11,7 +11,9 @@
|
||||
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
<a class="badge badge-secondary" href="{% url 'password_change' %}">
|
||||
<i class="fa fa-lock"></i>
|
||||
<svg class="bi bi-lock" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
|
||||
</svg>
|
||||
{% trans 'Change password' %}
|
||||
</a>
|
||||
</dd>
|
||||
@ -20,19 +22,13 @@
|
||||
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
<svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
|
||||
</svg>
|
||||
{% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-xl-6">{% trans 'friendships'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">
|
||||
<a class="badge badge-secondary" href="{% url 'member:user_trust' user_object.pk %}">
|
||||
<i class="fa fa-edit"></i>
|
||||
{% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
|
||||
</a>
|
||||
</dd>
|
||||
|
||||
{% if "member.view_profile"|has_perm:user_object.profile %}
|
||||
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
||||
@ -60,7 +56,10 @@
|
||||
{% if user_object.pk == user.pk %}
|
||||
<div class="text-center">
|
||||
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
||||
<i class="fa fa-cogs"></i>{% trans 'API token' %}
|
||||
<svg class="bi bi-cogs" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
|
||||
</svg>
|
||||
{% trans 'API token' %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -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 -->
|
||||
|
@ -18,7 +18,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-header position-relative" id="clubListHeading">
|
||||
<a class="font-weight-bold">
|
||||
<i class="fa fa-users"></i> {% trans "View my memberships" %}
|
||||
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
</svg>
|
||||
{% trans "View my memberships" %}
|
||||
</a>
|
||||
</div>
|
||||
{% render_table club_list %}
|
||||
@ -29,7 +32,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<a class="stretched-link font-weight-bold text-decoration-none"
|
||||
{% if "note.view_note"|has_perm:user_object.note %}
|
||||
href="{% url 'note:transactions' pk=user_object.note.pk %}" {% endif %}>
|
||||
<i class="fa fa-euro"></i> {% trans "Transaction history" %}
|
||||
<svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
|
||||
</svg>
|
||||
{% trans "Transaction history" %}
|
||||
</a>
|
||||
</div>
|
||||
<div id="history_list">
|
||||
|
@ -1,48 +0,0 @@
|
||||
{% extends "member/base.html" %}
|
||||
{% comment %}
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endcomment %}
|
||||
{% load static django_tables2 i18n %}
|
||||
|
||||
{% block profile_content %}
|
||||
<div class="card bg-light mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "Add friends" %}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
{% if can_create %}
|
||||
<form class="input-group" method="POST" id="form_trust">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="trusting" value="{{ object.note.pk }}">
|
||||
{%include "autocomplete_model.html" %}
|
||||
<div class="input-group-append">
|
||||
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% render_table trusting %}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning card mb-3">
|
||||
{% blocktrans trimmed %}
|
||||
Adding someone as a friend enables them to initiate transactions coming
|
||||
from your account (while keeping your balance positive). This is
|
||||
designed to simplify using note kfet transfers to transfer money between
|
||||
users. The intent is that one person can make all transfers for a group of
|
||||
friends without needing additional rights among them.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "People having you as a friend" %}
|
||||
</h3>
|
||||
{% render_table trusted_by %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script src="{% static "member/js/trust.js" %}"></script>
|
||||
<script src="{% static "js/autocomplete_model.js" %}"></script>
|
||||
{% endblock%}
|
@ -7,7 +7,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block content %}
|
||||
{% if can_manage_registrations %}
|
||||
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
|
||||
<i class="fa fa-user-plus"></i> {% trans "Registrations" %}
|
||||
<svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
|
||||
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
|
||||
</svg>
|
||||
{% trans "Registrations" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user