mirror of https://gitlab.crans.org/bde/nk20
Compare commits
1 Commits
85d38487fa
...
caa7923290
Author | SHA1 | Date |
---|---|---|
hugoooo | caa7923290 |
|
@ -7,25 +7,25 @@ stages:
|
||||||
variables:
|
variables:
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
GIT_SUBMODULE_STRATEGY: recursive
|
||||||
|
|
||||||
# Debian Bullseye
|
# Debian Buster
|
||||||
py39-django42:
|
# py37-django22:
|
||||||
stage: test
|
# stage: test
|
||||||
image: debian:bullseye
|
# image: debian:buster-backports
|
||||||
before_script:
|
# before_script:
|
||||||
- >
|
# - >
|
||||||
apt-get update &&
|
# apt-get update &&
|
||||||
apt-get install --no-install-recommends -y
|
# apt-get install --no-install-recommends -t buster-backports -y
|
||||||
python3-django python3-django-crispy-forms
|
# python3-django python3-django-crispy-forms
|
||||||
python3-django-extensions python3-django-filters python3-django-polymorphic
|
# python3-django-extensions python3-django-filters python3-django-polymorphic
|
||||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
# python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
# python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||||
python3-bs4 python3-setuptools tox texlive-xetex
|
# python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
script: tox -e py39-django42
|
# script: tox -e py37-django22
|
||||||
|
|
||||||
# Ubuntu 22.04
|
# Ubuntu 20.04
|
||||||
py310-django42:
|
py38-django22:
|
||||||
stage: test
|
stage: test
|
||||||
image: ubuntu:22.04
|
image: ubuntu:20.04
|
||||||
before_script:
|
before_script:
|
||||||
# Fix tzdata prompt
|
# Fix tzdata prompt
|
||||||
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
|
- ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime && echo Europe/Paris > /etc/timezone
|
||||||
|
@ -37,12 +37,12 @@ py310-django42:
|
||||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||||
python3-bs4 python3-setuptools tox texlive-xetex
|
python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
script: tox -e py310-django42
|
script: tox -e py38-django22
|
||||||
|
|
||||||
# Debian Bookworm
|
# Debian Bullseye
|
||||||
py311-django42:
|
py39-django22:
|
||||||
stage: test
|
stage: test
|
||||||
image: debian:bookworm
|
image: debian:bullseye
|
||||||
before_script:
|
before_script:
|
||||||
- >
|
- >
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
|
@ -52,13 +52,11 @@ py311-django42:
|
||||||
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
|
||||||
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
|
||||||
python3-bs4 python3-setuptools tox texlive-xetex
|
python3-bs4 python3-setuptools tox texlive-xetex
|
||||||
script: tox -e py311-django42
|
script: tox -e py39-django22
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
stage: quality-assurance
|
stage: quality-assurance
|
||||||
image: debian:bookworm
|
image: debian:bullseye
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get install -y tox
|
- apt-get update && apt-get install -y tox
|
||||||
script: tox -e linters
|
script: tox -e linters
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
[submodule "apps/scripts"]
|
[submodule "apps/scripts"]
|
||||||
path = apps/scripts
|
path = apps/scripts
|
||||||
url = https://gitlab.crans.org/bde/nk20-scripts
|
url = https://gitlab.crans.org/bde/nk20-scripts.git
|
||||||
|
|
|
@ -55,7 +55,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
|
||||||
(env)$ ./manage.py makemigrations
|
(env)$ ./manage.py makemigrations
|
||||||
(env)$ ./manage.py migrate
|
(env)$ ./manage.py migrate
|
||||||
(env)$ ./manage.py loaddata initial
|
(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 :
|
6. Enjoy :
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.contrib import admin
|
||||||
from note_kfet.admin import admin_site
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .forms import GuestForm
|
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)
|
@admin.register(Activity, site=admin_site)
|
||||||
|
@ -45,11 +45,3 @@ class EntryAdmin(admin.ModelAdmin):
|
||||||
Admin customisation for Entry
|
Admin customisation for Entry
|
||||||
"""
|
"""
|
||||||
list_display = ('note', 'activity', 'time', 'guest')
|
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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from rest_framework import serializers
|
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):
|
class ActivityTypeSerializer(serializers.ModelSerializer):
|
||||||
|
@ -61,17 +59,3 @@ class GuestTransactionSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = GuestTransaction
|
model = GuestTransaction
|
||||||
fields = '__all__'
|
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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet
|
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_activity_urls(router, path):
|
def register_activity_urls(router, path):
|
||||||
|
@ -12,4 +12,3 @@ def register_activity_urls(router, path):
|
||||||
router.register(path + '/type', ActivityTypeViewSet)
|
router.register(path + '/type', ActivityTypeViewSet)
|
||||||
router.register(path + '/guest', GuestViewSet)
|
router.register(path + '/guest', GuestViewSet)
|
||||||
router.register(path + '/entry', EntryViewSet)
|
router.register(path + '/entry', EntryViewSet)
|
||||||
router.register(path + '/opener', OpenerViewSet)
|
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from api.filters import RegexSafeSearchFilter
|
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.response import Response
|
from rest_framework.filters import SearchFilter
|
||||||
from rest_framework import status
|
|
||||||
|
|
||||||
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer, OpenerSerializer
|
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
|
||||||
from ..models import Activity, ActivityType, Entry, Guest, Opener
|
from ..models import Activity, ActivityType, Entry, Guest
|
||||||
|
|
||||||
|
|
||||||
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
||||||
|
@ -32,7 +29,7 @@ class ActivityViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Activity.objects.order_by('id')
|
queryset = Activity.objects.order_by('id')
|
||||||
serializer_class = ActivitySerializer
|
serializer_class = ActivitySerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
filterset_fields = ['name', 'description', 'activity_type', 'location', 'creater', 'organizer', 'attendees_club',
|
||||||
'date_start', 'date_end', 'valid', 'open', ]
|
'date_start', 'date_end', 'valid', 'open', ]
|
||||||
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
search_fields = ['$name', '$description', '$location', '$creater__last_name', '$creater__first_name',
|
||||||
|
@ -50,7 +47,7 @@ class GuestViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Guest.objects.order_by('id')
|
queryset = Guest.objects.order_by('id')
|
||||||
serializer_class = GuestSerializer
|
serializer_class = GuestSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name',
|
||||||
'inviter__alias__normalized_name', ]
|
'inviter__alias__normalized_name', ]
|
||||||
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__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')
|
queryset = Entry.objects.order_by('id')
|
||||||
serializer_class = EntrySerializer
|
serializer_class = EntrySerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['activity', 'time', 'note', 'guest', ]
|
filterset_fields = ['activity', 'time', 'note', 'guest', ]
|
||||||
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
|
search_fields = ['$activity__name', '$note__user__email', '$note__alias__name', '$note__alias__normalized_name',
|
||||||
'$guest__last_name', '$guest__first_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)
|
|
||||||
|
|
|
@ -4,14 +4,13 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
|
|
||||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from member.models import Club
|
from member.models import Club
|
||||||
from note.models import Note, NoteUser
|
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 note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
|
@ -44,7 +43,7 @@ class ActivityForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Activity
|
model = Activity
|
||||||
exclude = ('creater', 'valid', 'open', 'opener', )
|
exclude = ('creater', 'valid', 'open', )
|
||||||
widgets = {
|
widgets = {
|
||||||
"organizer": Autocomplete(
|
"organizer": Autocomplete(
|
||||||
model=Club,
|
model=Club,
|
||||||
|
|
|
@ -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')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -11,7 +11,7 @@ from django.db import models, transaction
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note.models import NoteUser, Transaction, Note
|
from note.models import NoteUser, Transaction
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
@ -310,31 +310,3 @@ class GuestTransaction(Transaction):
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _('Invitation')
|
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)
|
|
||||||
})
|
|
|
@ -5,13 +5,11 @@ from django.utils import timezone
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.middlewares import get_current_request
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2 import A
|
from django_tables2 import A
|
||||||
from permission.backends import PermissionBackend
|
|
||||||
from note.templatetags.pretty_money import pretty_money
|
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):
|
class ActivityTable(tables.Table):
|
||||||
|
@ -115,34 +113,3 @@ class EntryTable(tables.Table):
|
||||||
'data-last-name': lambda record: record.last_name,
|
'data-last-name': lambda record: record.last_name,
|
||||||
'data-first-name': lambda record: record.first_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 %}
|
{% endcomment %}
|
||||||
{% load i18n perms %}
|
{% load i18n perms %}
|
||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
{% load static django_tables2 i18n %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="text-white">{{ title }}</h1>
|
<h1 class="text-white">{{ title }}</h1>
|
||||||
{% include "activity/includes/activity_info.html" %}
|
{% 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 %}
|
{% if guests.data %}
|
||||||
<div class="card bg-white mb-3">
|
<div class="card bg-white mb-3">
|
||||||
<h3 class="card-header text-center">
|
<h3 class="card-header text-center">
|
||||||
|
@ -42,8 +22,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script src="{% static "activity/js/opener.js" %}"></script>
|
|
||||||
<script src="{% static "js/autocomplete_model.js" %}"></script>
|
|
||||||
<script>
|
<script>
|
||||||
function remove_guest(guest_id) {
|
function remove_guest(guest_id) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
|
|
|
@ -18,15 +18,14 @@ from django.views import View
|
||||||
from django.views.decorators.cache import cache_page
|
from django.views.decorators.cache import cache_page
|
||||||
from django.views.generic import DetailView, TemplateView, UpdateView
|
from django.views.generic import DetailView, TemplateView, UpdateView
|
||||||
from django.views.generic.list import ListView
|
from django.views.generic.list import ListView
|
||||||
from django_tables2.views import MultiTableMixin, SingleTableMixin
|
from django_tables2.views import MultiTableMixin
|
||||||
from api.viewsets import is_regex
|
|
||||||
from note.models import Alias, NoteSpecial, NoteUser
|
from note.models import Alias, NoteSpecial, NoteUser
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||||
|
|
||||||
from .forms import ActivityForm, GuestForm
|
from .forms import ActivityForm, GuestForm
|
||||||
from .models import Activity, Entry, Guest, Opener
|
from .models import Activity, Entry, Guest
|
||||||
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
|
from .tables import ActivityTable, EntryTable, GuestTable
|
||||||
|
|
||||||
|
|
||||||
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
|
@ -64,15 +63,19 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
|
||||||
Displays all Activities, and classify if they are on-going or upcoming ones.
|
Displays all Activities, and classify if they are on-going or upcoming ones.
|
||||||
"""
|
"""
|
||||||
model = Activity
|
model = Activity
|
||||||
tables = [
|
tables = [ActivityTable, ActivityTable]
|
||||||
lambda data: ActivityTable(data, prefix="all-"),
|
|
||||||
lambda data: ActivityTable(data, prefix="upcoming-"),
|
|
||||||
]
|
|
||||||
extra_context = {"title": _("Activities")}
|
extra_context = {"title": _("Activities")}
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
return super().get_queryset(**kwargs).distinct()
|
return super().get_queryset(**kwargs).distinct()
|
||||||
|
|
||||||
|
def get_tables(self):
|
||||||
|
tables = super().get_tables()
|
||||||
|
|
||||||
|
tables[0].prefix = "all-"
|
||||||
|
tables[1].prefix = "upcoming-"
|
||||||
|
return tables
|
||||||
|
|
||||||
def get_tables_data(self):
|
def get_tables_data(self):
|
||||||
# first table = all activities, second table = upcoming
|
# first table = all activities, second table = upcoming
|
||||||
return [
|
return [
|
||||||
|
@ -96,7 +99,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Shows details about one activity. Add guest to context
|
Shows details about one activity. Add guest to context
|
||||||
"""
|
"""
|
||||||
|
@ -104,40 +107,15 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMix
|
||||||
context_object_name = "activity"
|
context_object_name = "activity"
|
||||||
extra_context = {"title": _("Activity detail")}
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
|
|
||||||
tables = context["tables"]
|
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
||||||
for name, table in zip(["guests", "opener"], tables):
|
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
|
||||||
context[name] = table
|
context["guests"] = table
|
||||||
|
|
||||||
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
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
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -194,14 +172,12 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
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
|
Manages entry to an activity
|
||||||
"""
|
"""
|
||||||
template_name = "activity/activity_entry.html"
|
template_name = "activity/activity_entry.html"
|
||||||
|
|
||||||
table_class = EntryTable
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
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),
|
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
|
||||||
|
@ -236,16 +212,13 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||||
|
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
|
if pattern[0] != "^":
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
pattern = "^" + pattern
|
||||||
valid_regex = is_regex(pattern)
|
|
||||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
|
||||||
pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
|
|
||||||
guest_qs = guest_qs.filter(
|
guest_qs = guest_qs.filter(
|
||||||
Q(**{f"first_name{suffix}": pattern})
|
Q(first_name__iregex=pattern)
|
||||||
| Q(**{f"last_name{suffix}": pattern})
|
| Q(last_name__iregex=pattern)
|
||||||
| Q(**{f"inviter__alias__name{suffix}": pattern})
|
| Q(inviter__alias__name__iregex=pattern)
|
||||||
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
|
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
guest_qs = guest_qs.none()
|
guest_qs = guest_qs.none()
|
||||||
|
@ -277,15 +250,11 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||||
|
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = 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(
|
note_qs = note_qs.filter(
|
||||||
Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
|
Q(note__noteuser__user__first_name__iregex=pattern)
|
||||||
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
|
| Q(note__noteuser__user__last_name__iregex=pattern)
|
||||||
| Q(**{f"name{suffix}": pattern})
|
| Q(name__iregex=pattern)
|
||||||
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
|
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
note_qs = note_qs.none()
|
note_qs = note_qs.none()
|
||||||
|
@ -297,9 +266,15 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||||
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql' else note_qs.distinct()[:20]
|
||||||
return note_qs
|
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"))\
|
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
.distinct().get(pk=self.kwargs["pk"])
|
.distinct().get(pk=self.kwargs["pk"])
|
||||||
|
context["activity"] = activity
|
||||||
|
|
||||||
matched = []
|
matched = []
|
||||||
|
|
||||||
|
@ -312,17 +287,8 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
|
||||||
note.activity = activity
|
note.activity = activity
|
||||||
matched.append(note)
|
matched.append(note)
|
||||||
|
|
||||||
return matched
|
table = EntryTable(data=matched)
|
||||||
|
context["table"] = table
|
||||||
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
|
|
||||||
|
|
||||||
context["entries"] = Entry.objects.filter(activity=activity)
|
context["entries"] = Entry.objects.filter(activity=activity)
|
||||||
|
|
||||||
|
@ -364,8 +330,8 @@ X-WR-CALNAME:Kfet Calendar
|
||||||
NAME:Kfet Calendar
|
NAME:Kfet Calendar
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
BEGIN:VTIMEZONE
|
BEGIN:VTIMEZONE
|
||||||
TZID:Europe/Paris
|
TZID:Europe/Berlin
|
||||||
X-LIC-LOCATION:Europe/Paris
|
X-LIC-LOCATION:Europe/Berlin
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:+0100
|
||||||
TZOFFSETTO:+0200
|
TZOFFSETTO:+0200
|
||||||
|
@ -387,10 +353,10 @@ END:VTIMEZONE
|
||||||
DTSTAMP:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}Z
|
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()}
|
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)}
|
SUMMARY;CHARSET=UTF-8:{self.multilines(activity.name, 75, 22)}
|
||||||
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
|
DTSTART;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
|
||||||
DTEND:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_end)}
|
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"}
|
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}
|
-- {activity.organizer.name}
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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
|
|
|
@ -12,12 +12,11 @@ from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models.fields.files import ImageFieldFile
|
from django.db.models.fields.files import ImageFieldFile
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
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 member.models import Membership, Club
|
||||||
from note.models import NoteClub, NoteUser, Alias, Note
|
from note.models import NoteClub, NoteUser, Alias, Note
|
||||||
from permission.models import PermissionMask, Permission, Role
|
from permission.models import PermissionMask, Permission, Role
|
||||||
|
from phonenumbers import PhoneNumber
|
||||||
|
from rest_framework.filters import SearchFilter, OrderingFilter
|
||||||
|
|
||||||
from .viewsets import ContentTypeViewSet, UserViewSet
|
from .viewsets import ContentTypeViewSet, UserViewSet
|
||||||
|
|
||||||
|
@ -88,7 +87,7 @@ class TestAPI(TestCase):
|
||||||
resp = self.client.get(url + f"?ordering=-{field}")
|
resp = self.client.get(url + f"?ordering=-{field}")
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
if RegexSafeSearchFilter in backends:
|
if SearchFilter in backends:
|
||||||
# Basic search
|
# Basic search
|
||||||
for field in viewset.search_fields:
|
for field in viewset.search_fields:
|
||||||
obj = self.fix_note_object(obj, field)
|
obj = self.fix_note_object(obj, field)
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include
|
from django.conf.urls import url, include
|
||||||
from django.urls import re_path
|
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
|
|
||||||
from .views import UserInformationView
|
from .views import UserInformationView
|
||||||
|
@ -15,33 +14,29 @@ router = routers.DefaultRouter()
|
||||||
router.register('models', ContentTypeViewSet)
|
router.register('models', ContentTypeViewSet)
|
||||||
router.register('user', UserViewSet)
|
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:
|
if "member" in settings.INSTALLED_APPS:
|
||||||
from member.api.urls import register_members_urls
|
from member.api.urls import register_members_urls
|
||||||
register_members_urls(router, 'members')
|
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:
|
if "note" in settings.INSTALLED_APPS:
|
||||||
from note.api.urls import register_note_urls
|
from note.api.urls import register_note_urls
|
||||||
register_note_urls(router, 'note')
|
register_note_urls(router, 'note')
|
||||||
|
|
||||||
|
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:
|
if "permission" in settings.INSTALLED_APPS:
|
||||||
from permission.api.urls import register_permission_urls
|
from permission.api.urls import register_permission_urls
|
||||||
register_permission_urls(router, 'permission')
|
register_permission_urls(router, 'permission')
|
||||||
|
|
||||||
if "treasury" in settings.INSTALLED_APPS:
|
if "logs" in settings.INSTALLED_APPS:
|
||||||
from treasury.api.urls import register_treasury_urls
|
from logs.api.urls import register_logs_urls
|
||||||
register_treasury_urls(router, 'treasury')
|
register_logs_urls(router, 'logs')
|
||||||
|
|
||||||
if "wei" in settings.INSTALLED_APPS:
|
if "wei" in settings.INSTALLED_APPS:
|
||||||
from wei.api.urls import register_wei_urls
|
from wei.api.urls import register_wei_urls
|
||||||
|
@ -52,7 +47,7 @@ app_name = 'api'
|
||||||
# Wire up our API using automatic URL routing.
|
# Wire up our API using automatic URL routing.
|
||||||
# Additionally, we include login URLs for the browsable API.
|
# Additionally, we include login URLs for the browsable API.
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
re_path('^', include(router.urls)),
|
url('^', include(router.urls)),
|
||||||
re_path('^me/', UserInformationView.as_view()),
|
url('^me/', UserInformationView.as_view()),
|
||||||
re_path('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,29 +1,19 @@
|
||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from rest_framework.filters import SearchFilter
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from note.models import Alias
|
from note.models import Alias
|
||||||
|
|
||||||
from .filters import RegexSafeSearchFilter
|
|
||||||
from .serializers import UserSerializer, ContentTypeSerializer
|
from .serializers import UserSerializer, ContentTypeSerializer
|
||||||
|
|
||||||
|
|
||||||
def is_regex(pattern):
|
|
||||||
try:
|
|
||||||
re.compile(pattern)
|
|
||||||
return True
|
|
||||||
except (re.error, TypeError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class ReadProtectedModelViewSet(ModelViewSet):
|
class ReadProtectedModelViewSet(ModelViewSet):
|
||||||
"""
|
"""
|
||||||
Protect a ModelViewSet by filtering the objects that the user cannot see.
|
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:
|
if "search" in self.request.GET:
|
||||||
pattern = 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"
|
|
||||||
prefix = "^" if valid_regex else ""
|
|
||||||
|
|
||||||
# Filter with different rules
|
# Filter with different rules
|
||||||
# We use union-all to keep each filter rule sorted in result
|
# We use union-all to keep each filter rule sorted in result
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
# Match without normalization
|
# Match without normalization
|
||||||
Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
note__alias__name__iregex="^" + pattern
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match with normalization
|
# Match with normalization
|
||||||
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match on lower pattern
|
# Match on lower pattern
|
||||||
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match on firstname or lastname
|
# Match on firstname or lastname
|
||||||
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
|
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
||||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
||||||
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
& ~Q(note__alias__name__iregex="^" + pattern)
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
)
|
)
|
||||||
|
@ -121,6 +107,6 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = ContentType.objects.order_by('id')
|
queryset = ContentType.objects.order_by('id')
|
||||||
serializer_class = ContentTypeSerializer
|
serializer_class = ContentTypeSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['id', 'app_label', 'model', ]
|
filterset_fields = ['id', 'app_label', 'model', ]
|
||||||
search_fields = ['$app_label', '$model', ]
|
search_fields = ['$app_label', '$model', ]
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from django.db import transaction
|
|
||||||
from note_kfet.admin import admin_site
|
|
||||||
|
|
||||||
from .models import Allergen, BasicFood, QRCode, TransformedFood
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(QRCode, site=admin_site)
|
|
||||||
class QRCodeAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(BasicFood, site=admin_site)
|
|
||||||
class BasicFoodAdmin(admin.ModelAdmin):
|
|
||||||
@transaction.atomic
|
|
||||||
def save_related(self, *args, **kwargs):
|
|
||||||
ans = super().save_related(*args, **kwargs)
|
|
||||||
args[1].instance.update()
|
|
||||||
return ans
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TransformedFood, site=admin_site)
|
|
||||||
class TransformedFoodAdmin(admin.ModelAdmin):
|
|
||||||
exclude = ["allergens", "expiry_date"]
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save_related(self, request, form, *args, **kwargs):
|
|
||||||
super().save_related(request, form, *args, **kwargs)
|
|
||||||
form.instance.update()
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Allergen, site=admin_site)
|
|
||||||
class AllergenAdmin(admin.ModelAdmin):
|
|
||||||
pass
|
|
|
@ -1,50 +0,0 @@
|
||||||
# Copyright (C) 2018-2024 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-2024 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-2024 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-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class FoodkfetConfig(AppConfig):
|
|
||||||
name = 'food'
|
|
||||||
verbose_name = _('food')
|
|
|
@ -1,114 +0,0 @@
|
||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from random import shuffle
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.utils import timezone
|
|
||||||
from member.models import Club
|
|
||||||
from 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,226 +0,0 @@
|
||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from django.db import models, transaction
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from member.models import Club
|
|
||||||
from polymorphic.models import PolymorphicModel
|
|
||||||
|
|
||||||
|
|
||||||
class QRCode(models.Model):
|
|
||||||
"""
|
|
||||||
An QRCode model
|
|
||||||
"""
|
|
||||||
qr_code_number = models.PositiveIntegerField(
|
|
||||||
verbose_name=_("QR-code number"),
|
|
||||||
unique=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
food_container = models.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-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
import django_tables2 as tables
|
|
||||||
from django_tables2 import A
|
|
||||||
|
|
||||||
from .models import TransformedFood
|
|
||||||
|
|
||||||
|
|
||||||
class TransformedFoodTable(tables.Table):
|
|
||||||
name = tables.LinkColumn(
|
|
||||||
'food:food_view',
|
|
||||||
args=[A('pk'), ],
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = TransformedFood
|
|
||||||
template_name = 'django_tables2/bootstrap4.html'
|
|
||||||
fields = ('name', "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-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
app_name = 'food'
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('', views.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-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.db import transaction
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django_tables2.views import 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
|
|
|
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
previous = instance._previous
|
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()
|
request = get_current_request()
|
||||||
|
|
||||||
if request is None:
|
if request is None:
|
||||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
# 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
|
# 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"
|
ip = "127.0.0.1"
|
||||||
username = Alias.normalize(getpass.getuser())
|
username = Alias.normalize(getpass.getuser())
|
||||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
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"):
|
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||||
return
|
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()
|
request = get_current_request()
|
||||||
|
|
||||||
if request is None:
|
if request is None:
|
||||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
# 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
|
# 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"
|
ip = "127.0.0.1"
|
||||||
username = Alias.normalize(getpass.getuser())
|
username = Alias.normalize(getpass.getuser())
|
||||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from api.filters import RegexSafeSearchFilter
|
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||||
|
@ -18,7 +17,7 @@ class ProfileViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Profile.objects.order_by('id')
|
queryset = Profile.objects.order_by('id')
|
||||||
serializer_class = ProfileSerializer
|
serializer_class = ProfileSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['user', 'user__first_name', 'user__last_name', 'user__username', 'user__email',
|
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",
|
'user__note__alias__name', 'user__note__alias__normalized_name', 'phone_number', "section",
|
||||||
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
'department', 'promotion', 'address', 'paid', 'ml_events_registration', 'ml_sport_registration',
|
||||||
|
@ -35,7 +34,7 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Club.objects.order_by('id')
|
queryset = Club.objects.order_by('id')
|
||||||
serializer_class = ClubSerializer
|
serializer_class = ClubSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
filterset_fields = ['name', 'email', 'note__alias__name', 'note__alias__normalized_name', 'parent_club',
|
||||||
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
'parent_club__name', 'require_memberships', 'membership_fee_paid', 'membership_fee_unpaid',
|
||||||
'membership_duration', 'membership_start', 'membership_end', ]
|
'membership_duration', 'membership_start', 'membership_end', ]
|
||||||
|
@ -50,7 +49,7 @@ class MembershipViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Membership.objects.order_by('id')
|
queryset = Membership.objects.order_by('id')
|
||||||
serializer_class = MembershipSerializer
|
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',
|
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__username', 'user__last_name', 'user__first_name', 'user__email',
|
||||||
'user__note__alias__name', 'user__note__alias__normalized_name',
|
'user__note__alias__name', 'user__note__alias__normalized_name',
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from bootstrap_datepicker_plus.widgets import DatePickerInput
|
from PIL import Image, ImageSequence
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.forms import AuthenticationForm
|
from django.contrib.auth.forms import AuthenticationForm
|
||||||
|
@ -13,9 +13,8 @@ from django.forms import CheckboxSelectMultiple
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note.models import NoteSpecial, Alias
|
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 permission.models import PermissionMask, Role
|
||||||
from PIL import Image, ImageSequence
|
|
||||||
|
|
||||||
from .models import Profile, Club, Membership
|
from .models import Profile, Club, Membership
|
||||||
|
|
||||||
|
@ -33,7 +32,7 @@ class UserForm(forms.ModelForm):
|
||||||
# Django usernames can only contain letters, numbers, @, ., +, - and _.
|
# Django usernames can only contain letters, numbers, @, ., +, - and _.
|
||||||
# We want to allow users to have uncommon and unpractical usernames:
|
# We want to allow users to have uncommon and unpractical usernames:
|
||||||
# That is their problem, and we have normalized aliases for us.
|
# 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:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
|
@ -211,9 +210,9 @@ class MembershipForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Membership
|
model = Membership
|
||||||
fields = ('user', 'date_start')
|
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
|
# 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 = {
|
widgets = {
|
||||||
'user':
|
'user':
|
||||||
Autocomplete(
|
Autocomplete(
|
||||||
|
|
|
@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -295,14 +295,7 @@ class Club(models.Model):
|
||||||
|
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
|
|
||||||
# Avoid any problems on February 29
|
while (today - self.membership_start).days >= 365:
|
||||||
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:
|
if self.membership_start:
|
||||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||||
self.membership_start.month, self.membership_start.day)
|
self.membership_start.month, self.membership_start.day)
|
||||||
|
@ -480,10 +473,10 @@ class Membership(models.Model):
|
||||||
|
|
||||||
if self.club.parent_club.name == "BDE":
|
if self.club.parent_club.name == "BDE":
|
||||||
parent_membership.roles.set(
|
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":
|
elif self.club.parent_club.name == "Kfet":
|
||||||
parent_membership.roles.set(
|
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:
|
else:
|
||||||
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
||||||
parent_membership.save()
|
parent_membership.save()
|
||||||
|
|
|
@ -291,7 +291,7 @@ class TestMemberships(TestCase):
|
||||||
|
|
||||||
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
||||||
roles=[role.id for role in Role.objects.filter(
|
roles=[role.id for role in Role.objects.filter(
|
||||||
Q(name="Membre de club") | Q(name="Trésorièr⋅e de club") | Q(name="Bureau de club")).all()],
|
Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()],
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
self.membership.refresh_from_db()
|
self.membership.refresh_from_db()
|
||||||
|
|
|
@ -16,9 +16,8 @@ from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import DetailView, UpdateView, TemplateView
|
from django.views.generic import DetailView, UpdateView, TemplateView
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django_tables2.views import MultiTableMixin, SingleTableMixin, SingleTableView
|
from django_tables2.views import SingleTableView
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from api.viewsets import is_regex
|
|
||||||
from note.models import Alias, NoteClub, NoteUser, Trust
|
from note.models import Alias, NoteClub, NoteUser, Trust
|
||||||
from note.models.transactions import Transaction, SpecialTransaction
|
from note.models.transactions import Transaction, SpecialTransaction
|
||||||
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
|
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
|
||||||
|
@ -220,20 +219,16 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = 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"
|
|
||||||
prefix = "^" if valid_regex else ""
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(**{f"username{suffix}": prefix + pattern})
|
username__iregex="^" + pattern
|
||||||
).union(
|
).union(
|
||||||
qs.filter(
|
qs.filter(
|
||||||
(Q(**{f"alias{suffix}": prefix + pattern})
|
(Q(alias__iregex="^" + pattern)
|
||||||
| Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
|
| Q(normalized_alias__iregex="^" + Alias.normalize(pattern))
|
||||||
| Q(**{f"last_name{suffix}": prefix + pattern})
|
| Q(last_name__iregex="^" + pattern)
|
||||||
| Q(**{f"first_name{suffix}": prefix + pattern})
|
| Q(first_name__iregex="^" + pattern)
|
||||||
| Q(email__istartswith=pattern))
|
| Q(email__istartswith=pattern))
|
||||||
& ~Q(**{f"username{suffix}": prefix + pattern})
|
& ~Q(username__iregex="^" + pattern)
|
||||||
), all=True)
|
), all=True)
|
||||||
else:
|
else:
|
||||||
qs = qs.none()
|
qs = qs.none()
|
||||||
|
@ -248,7 +243,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View and manage user trust relationships
|
View and manage user trust relationships
|
||||||
"""
|
"""
|
||||||
|
@ -257,25 +252,13 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
|
||||||
context_object_name = 'user_object'
|
context_object_name = 'user_object'
|
||||||
extra_context = {"title": _("Note friendships")}
|
extra_context = {"title": _("Note friendships")}
|
||||||
|
|
||||||
tables = [
|
|
||||||
lambda data: TrustTable(data, prefix="trust-"),
|
|
||||||
lambda data: TrustedTable(data, prefix="trusted-"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_tables_data(self):
|
|
||||||
note = self.object.note
|
|
||||||
return [
|
|
||||||
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
|
|
||||||
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct(),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
note = context['object'].note
|
||||||
tables = context["tables"]
|
context["trusting"] = TrustTable(
|
||||||
for name, table in zip(["trusting", "trusted_by"], tables):
|
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||||
context[name] = table
|
context["trusted_by"] = TrustedTable(
|
||||||
|
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
|
||||||
trusting=context["object"].note,
|
trusting=context["object"].note,
|
||||||
trusted=context["object"].note
|
trusted=context["object"].note
|
||||||
|
@ -294,7 +277,7 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View and manage user aliases.
|
View and manage user aliases.
|
||||||
"""
|
"""
|
||||||
|
@ -303,15 +286,12 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixi
|
||||||
context_object_name = 'user_object'
|
context_object_name = 'user_object'
|
||||||
extra_context = {"title": _("Note aliases")}
|
extra_context = {"title": _("Note aliases")}
|
||||||
|
|
||||||
table_class = AliasTable
|
|
||||||
context_table_name = "aliases"
|
|
||||||
|
|
||||||
def get_table_data(self):
|
|
||||||
return self.object.note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() \
|
|
||||||
.order_by('normalized_name')
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
note = context['object'].note
|
||||||
|
context["aliases"] = AliasTable(
|
||||||
|
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
|
||||||
|
.order_by('normalized_name').all())
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
|
@ -430,15 +410,10 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = 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"
|
|
||||||
prefix = "^" if valid_regex else ""
|
|
||||||
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(**{f"name{suffix}": prefix + pattern})
|
Q(name__iregex=pattern)
|
||||||
| Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
| Q(note__alias__name__iregex=pattern)
|
||||||
| Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
| Q(note__alias__normalized_name__iregex=Alias.normalize(pattern))
|
||||||
)
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
@ -535,7 +510,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Manage aliases of a club.
|
Manage aliases of a club.
|
||||||
"""
|
"""
|
||||||
|
@ -544,16 +519,11 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin,
|
||||||
context_object_name = 'club'
|
context_object_name = 'club'
|
||||||
extra_context = {"title": _("Note aliases")}
|
extra_context = {"title": _("Note aliases")}
|
||||||
|
|
||||||
table_class = AliasTable
|
|
||||||
context_table_name = "aliases"
|
|
||||||
|
|
||||||
def get_table_data(self):
|
|
||||||
return self.object.note.alias.filter(
|
|
||||||
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct()
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
note = context['object'].note
|
||||||
|
context["aliases"] = AliasTable(note.alias.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
|
@ -857,8 +827,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
|
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
|
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
|
||||||
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \
|
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
|
||||||
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
||||||
# Set the same roles as before
|
# Set the same roles as before
|
||||||
if old_membership:
|
if old_membership:
|
||||||
|
@ -894,7 +864,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
if old_membership.exists():
|
if old_membership.exists():
|
||||||
membership.roles.set(old_membership.get().roles.all())
|
membership.roles.set(old_membership.get().roles.all())
|
||||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
|
membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
@ -942,15 +912,10 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
||||||
|
|
||||||
if 'search' in self.request.GET:
|
if 'search' in self.request.GET:
|
||||||
pattern = 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"
|
|
||||||
prefix = "^" if valid_regex else ""
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(**{f"user__first_name{suffix}": prefix + pattern})
|
Q(user__first_name__iregex='^' + pattern)
|
||||||
| Q(**{f"user__last_name{suffix}": prefix + pattern})
|
| Q(user__last_name__iregex='^' + pattern)
|
||||||
| Q(**{f"user__note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
|
||||||
)
|
)
|
||||||
|
|
||||||
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from rest_framework import status, viewsets
|
from rest_framework import viewsets
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from api.filters import RegexSafeSearchFilter
|
from rest_framework import status
|
||||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet, \
|
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
||||||
is_regex
|
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
|
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer, \
|
||||||
|
@ -29,7 +29,7 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Note.objects.order_by('id')
|
queryset = Note.objects.order_by('id')
|
||||||
serializer_class = NotePolymorphicSerializer
|
serializer_class = NotePolymorphicSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
|
filterset_fields = ['alias__name', 'polymorphic_ctype', 'is_active', 'balance', 'last_negative', 'created_at', ]
|
||||||
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
|
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model',
|
||||||
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
|
'$noteuser__user__last_name', '$noteuser__user__first_name', '$noteuser__user__email',
|
||||||
|
@ -48,14 +48,10 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", ".*")
|
alias = self.request.query_params.get("alias", ".*")
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
|
||||||
valid_regex = is_regex(alias)
|
|
||||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
|
||||||
alias_prefix = '^' if valid_regex else ''
|
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(**{f"alias__name{suffix}": alias_prefix + alias})
|
Q(alias__name__iregex="^" + alias)
|
||||||
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
| Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
|
||||||
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()})
|
| Q(alias__normalized_name__iregex="^" + alias.lower())
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset.order_by("id")
|
return queryset.order_by("id")
|
||||||
|
@ -69,7 +65,7 @@ class TrustViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Trust.objects
|
queryset = Trust.objects
|
||||||
serializer_class = TrustSerializer
|
serializer_class = TrustSerializer
|
||||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
|
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
|
||||||
'$trusted__alias__name', '$trusted__alias__normalized_name']
|
'$trusted__alias__name', '$trusted__alias__normalized_name']
|
||||||
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
|
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
|
||||||
|
@ -95,11 +91,11 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/note/alias/
|
then render it on /api/note/aliases/
|
||||||
"""
|
"""
|
||||||
queryset = Alias.objects
|
queryset = Alias.objects
|
||||||
serializer_class = AliasSerializer
|
serializer_class = AliasSerializer
|
||||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
|
@ -130,22 +126,18 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", None)
|
alias = self.request.query_params.get("alias", None)
|
||||||
if alias:
|
if alias:
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
|
||||||
valid_regex = is_regex(alias)
|
|
||||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
|
||||||
alias_prefix = '^' if valid_regex else ''
|
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
**{f"name{suffix}": alias_prefix + alias}
|
name__iregex="^" + alias
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
||||||
& ~Q(**{f"name{suffix}": alias_prefix + alias})
|
& ~Q(name__iregex="^" + alias)
|
||||||
),
|
),
|
||||||
all=True).union(
|
all=True).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(**{f"normalized_name{suffix}": "^" + alias.lower()})
|
Q(normalized_name__iregex="^" + alias.lower())
|
||||||
& ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)})
|
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
||||||
& ~Q(**{f"name{suffix}": "^" + alias})
|
& ~Q(name__iregex="^" + alias)
|
||||||
),
|
),
|
||||||
all=True)
|
all=True)
|
||||||
|
|
||||||
|
@ -155,7 +147,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||||
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
queryset = Alias.objects
|
queryset = Alias.objects
|
||||||
serializer_class = ConsumerSerializer
|
serializer_class = ConsumerSerializer
|
||||||
filter_backends = [RegexSafeSearchFilter, OrderingFilter, DjangoFilterBackend]
|
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
|
@ -174,7 +166,11 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", None)
|
alias = self.request.query_params.get("alias", None)
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
valid_regex = is_regex(alias)
|
try:
|
||||||
|
re.compile(alias)
|
||||||
|
valid_regex = True
|
||||||
|
except (re.error, TypeError):
|
||||||
|
valid_regex = False
|
||||||
suffix = '__iregex' if valid_regex else '__istartswith'
|
suffix = '__iregex' if valid_regex else '__istartswith'
|
||||||
alias_prefix = '^' if valid_regex else ''
|
alias_prefix = '^' if valid_regex else ''
|
||||||
queryset = queryset.prefetch_related('note')
|
queryset = queryset.prefetch_related('note')
|
||||||
|
@ -183,10 +179,19 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
# We match first an alias if it is matched without normalization,
|
# We match first an alias if it is matched without normalization,
|
||||||
# then if the normalized pattern matches a normalized alias.
|
# then if the normalized pattern matches a normalized alias.
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(**{f'name{suffix}': alias_prefix + alias})
|
**{f'name{suffix}': alias_prefix + alias}
|
||||||
| Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
).union(
|
||||||
| Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
|
queryset.filter(
|
||||||
)
|
Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||||
|
& ~Q(**{f'name{suffix}': alias_prefix + alias})
|
||||||
|
),
|
||||||
|
all=True).union(
|
||||||
|
queryset.filter(
|
||||||
|
Q(**{f'normalized_name{suffix}': alias_prefix + alias.lower()})
|
||||||
|
& ~Q(**{f'normalized_name{suffix}': alias_prefix + Alias.normalize(alias)})
|
||||||
|
& ~Q(**{f'name{suffix}': alias_prefix + alias})
|
||||||
|
),
|
||||||
|
all=True)
|
||||||
|
|
||||||
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
|
queryset = queryset if settings.DATABASES[queryset.db]["ENGINE"] == 'django.db.backends.postgresql' \
|
||||||
else queryset.order_by("name")
|
else queryset.order_by("name")
|
||||||
|
@ -202,7 +207,7 @@ class TemplateCategoryViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = TemplateCategory.objects.order_by('name')
|
queryset = TemplateCategory.objects.order_by('name')
|
||||||
serializer_class = TemplateCategorySerializer
|
serializer_class = TemplateCategorySerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['name', 'templates', 'templates__name']
|
filterset_fields = ['name', 'templates', 'templates__name']
|
||||||
search_fields = ['$name', '$templates__name', ]
|
search_fields = ['$name', '$templates__name', ]
|
||||||
|
|
||||||
|
@ -215,7 +220,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = TransactionTemplate.objects.order_by('name')
|
queryset = TransactionTemplate.objects.order_by('name')
|
||||||
serializer_class = TransactionTemplateSerializer
|
serializer_class = TransactionTemplateSerializer
|
||||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
|
filterset_fields = ['name', 'amount', 'display', 'category', 'category__name', ]
|
||||||
search_fields = ['$name', '$category__name', ]
|
search_fields = ['$name', '$category__name', ]
|
||||||
ordering_fields = ['amount', ]
|
ordering_fields = ['amount', ]
|
||||||
|
@ -229,7 +234,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Transaction.objects.order_by('-created_at')
|
queryset = Transaction.objects.order_by('-created_at')
|
||||||
serializer_class = TransactionPolymorphicSerializer
|
serializer_class = TransactionPolymorphicSerializer
|
||||||
filter_backends = [RegexSafeSearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
|
filterset_fields = ['source', 'source_alias', 'source__alias__name', 'source__alias__normalized_name',
|
||||||
'destination', 'destination_alias', 'destination__alias__name',
|
'destination', 'destination_alias', 'destination__alias__name',
|
||||||
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
|
'destination__alias__normalized_name', 'quantity', 'polymorphic_ctype', 'amount',
|
||||||
|
|
|
@ -2,13 +2,12 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from bootstrap_datepicker_plus.widgets import DateTimePickerInput
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
from django.utils.timezone import make_aware
|
from django.utils.timezone import make_aware
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.inputs import Autocomplete, AmountInput
|
from note_kfet.inputs import Autocomplete, AmountInput, DateTimePickerInput
|
||||||
|
|
||||||
from .models import TransactionTemplate, NoteClub, Alias
|
from .models import TransactionTemplate, NoteClub, Alias
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ def create_special_notes(apps, schema_editor):
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('note', '0001_initial'),
|
('note', '0001_initial'),
|
||||||
('logs', '0001_initial'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
name="{{ widget.name }}"
|
name="{{ widget.name }}"
|
||||||
{# Other attributes are loaded #}
|
{# Other attributes are loaded #}
|
||||||
{% for name, value in widget.attrs.items %}
|
{% for name, value in widget.attrs.items %}
|
||||||
{% if value is not False %}{{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}
|
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
|
||||||
{% endfor %}>
|
{% endfor %}>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<span class="input-group-text">€</span>
|
<span class="input-group-text">€</span>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérent⋅es dont le solde
|
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
||||||
est inférieur à 0 €.
|
est inférieur à 0 €.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,6 @@ from django.views.generic import CreateView, UpdateView, DetailView
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from activity.models import Entry
|
from activity.models import Entry
|
||||||
from api.viewsets import is_regex
|
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.views import ProtectQuerysetMixin
|
from permission.views import ProtectQuerysetMixin
|
||||||
from note_kfet.inputs import AmountInput
|
from note_kfet.inputs import AmountInput
|
||||||
|
@ -90,15 +89,11 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
|
||||||
qs = super().get_queryset().distinct()
|
qs = super().get_queryset().distinct()
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = 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"
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(**{f"name{suffix}": pattern})
|
Q(name__iregex=pattern)
|
||||||
| Q(**{f"destination__club__name{suffix}": pattern})
|
| Q(destination__club__name__iregex=pattern)
|
||||||
| Q(**{f"category__name{suffix}": pattern})
|
| Q(category__name__iregex=pattern)
|
||||||
| Q(**{f"description{suffix}": pattern})
|
| Q(description__iregex=pattern)
|
||||||
)
|
)
|
||||||
|
|
||||||
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
||||||
|
@ -228,10 +223,7 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
|
||||||
if "type" in data and data["type"]:
|
if "type" in data and data["type"]:
|
||||||
transactions = transactions.filter(polymorphic_ctype__in=data["type"])
|
transactions = transactions.filter(polymorphic_ctype__in=data["type"])
|
||||||
if "reason" in data and data["reason"]:
|
if "reason" in data and data["reason"]:
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
transactions = transactions.filter(reason__iregex=data["reason"])
|
||||||
valid_regex = is_regex(data["reason"])
|
|
||||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
|
||||||
transactions = transactions.filter(Q(**{f"reason{suffix}": data["reason"]}))
|
|
||||||
if "valid" in data and data["valid"]:
|
if "valid" in data and data["valid"]:
|
||||||
transactions = transactions.filter(valid=data["valid"])
|
transactions = transactions.filter(valid=data["valid"])
|
||||||
if "amount_gte" in data and data["amount_gte"]:
|
if "amount_gte" in data and data["amount_gte"]:
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
|
||||||
from api.filters import RegexSafeSearchFilter
|
|
||||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||||
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
from rest_framework.filters import SearchFilter
|
||||||
|
|
||||||
from .serializers import PermissionSerializer, RoleSerializer
|
from .serializers import PermissionSerializer, RoleSerializer
|
||||||
from ..models import Permission, Role
|
from ..models import Permission, Role
|
||||||
|
@ -17,9 +17,9 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Permission.objects.order_by('id')
|
queryset = Permission.objects.order_by('id')
|
||||||
serializer_class = PermissionSerializer
|
serializer_class = PermissionSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
|
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
|
||||||
search_fields = ['$model__model', '$query', '$description', ]
|
search_fields = ['$model__name', '$query', '$description', ]
|
||||||
|
|
||||||
|
|
||||||
class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
|
@ -30,6 +30,6 @@ class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Role.objects.order_by('id')
|
queryset = Role.objects.order_by('id')
|
||||||
serializer_class = RoleSerializer
|
serializer_class = RoleSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
|
filterset_fields = ['name', 'permissions', 'for_club', 'memberships__user', ]
|
||||||
search_fields = ['$name', '$for_club__name', ]
|
search_fields = ['$name', '$for_club__name', ]
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -35,8 +35,6 @@ class PermissionScopes(BaseScopes):
|
||||||
|
|
||||||
|
|
||||||
class PermissionOAuth2Validator(OAuth2Validator):
|
class PermissionOAuth2Validator(OAuth2Validator):
|
||||||
oidc_claim_scope = None # fix breaking change of django-oauth-toolkit 2.0.0
|
|
||||||
|
|
||||||
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
|
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
User can request as many scope as he wants, including invalid scopes,
|
User can request as many scope as he wants, including invalid scopes,
|
||||||
|
|
|
@ -36,8 +36,8 @@ class RightsTable(tables.Table):
|
||||||
|
|
||||||
def render_roles(self, record):
|
def render_roles(self, record):
|
||||||
# If the user has the right to manage the roles, display the link to manage them
|
# If the user has the right to manage the roles, display the link to manage them
|
||||||
roles = record.roles.filter((~(Q(name="Adhérent⋅e BDE")
|
roles = record.roles.filter((~(Q(name="Adhérent BDE")
|
||||||
| Q(name="Adhérent⋅e Kfet")
|
| Q(name="Adhérent Kfet")
|
||||||
| Q(name="Membre de club")
|
| Q(name="Membre de club")
|
||||||
| Q(name="Bureau de club"))
|
| Q(name="Bureau de club"))
|
||||||
& Q(weirole__isnull=True))).all()
|
& Q(weirole__isnull=True))).all()
|
||||||
|
|
|
@ -58,7 +58,7 @@ class OAuth2TestCase(TestCase):
|
||||||
# Create membership to validate permissions
|
# Create membership to validate permissions
|
||||||
NoteUser.objects.create(user=self.user)
|
NoteUser.objects.create(user=self.user)
|
||||||
membership = Membership.objects.create(user=self.user, club_id=bde.pk)
|
membership = Membership.objects.create(user=self.user, club_id=bde.pk)
|
||||||
membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE"))
|
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
# User is now a member and can now see its own user detail
|
# User is now a member and can now see its own user detail
|
||||||
|
@ -85,7 +85,7 @@ class OAuth2TestCase(TestCase):
|
||||||
bde = Club.objects.get(name="BDE")
|
bde = Club.objects.get(name="BDE")
|
||||||
NoteUser.objects.create(user=self.user)
|
NoteUser.objects.create(user=self.user)
|
||||||
membership = Membership.objects.create(user=self.user, club_id=bde.pk)
|
membership = Membership.objects.create(user=self.user, club_id=bde.pk)
|
||||||
membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE"))
|
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
resp = self.client.get(reverse('permission:scopes'))
|
resp = self.client.get(reverse('permission:scopes'))
|
||||||
|
|
|
@ -12,7 +12,6 @@ from django.forms import HiddenInput
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import UpdateView, TemplateView, CreateView
|
from django.views.generic import UpdateView, TemplateView, CreateView
|
||||||
from django_tables2 import MultiTableMixin
|
|
||||||
from member.models import Membership
|
from member.models import Membership
|
||||||
|
|
||||||
from .backends import PermissionBackend
|
from .backends import PermissionBackend
|
||||||
|
@ -36,9 +35,11 @@ class ProtectQuerysetMixin:
|
||||||
try:
|
try:
|
||||||
return super().get_object(queryset)
|
return super().get_object(queryset)
|
||||||
except Http404 as e:
|
except Http404 as e:
|
||||||
if self.get_queryset(filter_permissions=False).count() == self.get_queryset().count():
|
try:
|
||||||
|
super().get_object(self.get_queryset(filter_permissions=False))
|
||||||
|
raise PermissionDenied()
|
||||||
|
except Http404:
|
||||||
raise e
|
raise e
|
||||||
raise PermissionDenied()
|
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
|
@ -106,31 +107,10 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RightsView(MultiTableMixin, TemplateView):
|
class RightsView(TemplateView):
|
||||||
template_name = "permission/all_rights.html"
|
template_name = "permission/all_rights.html"
|
||||||
extra_context = {"title": _("Rights")}
|
extra_context = {"title": _("Rights")}
|
||||||
|
|
||||||
tables = [
|
|
||||||
lambda data: RightsTable(data, prefix="clubs-"),
|
|
||||||
lambda data: SuperuserTable(data, prefix="superusers-"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_tables_data(self):
|
|
||||||
special_memberships = Membership.objects.filter(
|
|
||||||
date_start__lte=date.today(),
|
|
||||||
date_end__gte=date.today(),
|
|
||||||
).filter(roles__in=Role.objects.filter((~(Q(name="Adhérent⋅e BDE")
|
|
||||||
| Q(name="Adhérent⋅e Kfet")
|
|
||||||
| Q(name="Membre de club")
|
|
||||||
| Q(name="Bureau de club"))
|
|
||||||
& Q(weirole__isnull=True))))\
|
|
||||||
.order_by("club__name", "user__last_name")\
|
|
||||||
.distinct().all()
|
|
||||||
return [
|
|
||||||
special_memberships,
|
|
||||||
User.objects.filter(is_superuser=True).order_by("last_name"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
@ -148,9 +128,19 @@ class RightsView(MultiTableMixin, TemplateView):
|
||||||
role.clubs = [membership.club for membership in active_memberships if role in membership.roles.all()]
|
role.clubs = [membership.club for membership in active_memberships if role in membership.roles.all()]
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
tables = context["tables"]
|
special_memberships = Membership.objects.filter(
|
||||||
for name, table in zip(["special_memberships_table", "superusers"], tables):
|
date_start__lte=date.today(),
|
||||||
context[name] = table
|
date_end__gte=date.today(),
|
||||||
|
).filter(roles__in=Role.objects.filter((~(Q(name="Adhérent BDE")
|
||||||
|
| Q(name="Adhérent Kfet")
|
||||||
|
| Q(name="Membre de club")
|
||||||
|
| Q(name="Bureau de club"))
|
||||||
|
& Q(weirole__isnull=True))))\
|
||||||
|
.order_by("club__name", "user__last_name")\
|
||||||
|
.distinct().all()
|
||||||
|
context["special_memberships_table"] = RightsTable(special_memberships, prefix="clubs-")
|
||||||
|
context["superusers"] = SuperuserTable(User.objects.filter(is_superuser=True).order_by("last_name").all(),
|
||||||
|
prefix="superusers-")
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ from django.views import View
|
||||||
from django.views.generic import CreateView, TemplateView, DetailView
|
from django.views.generic import CreateView, TemplateView, DetailView
|
||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from api.viewsets import is_regex
|
|
||||||
from member.forms import ProfileForm
|
from member.forms import ProfileForm
|
||||||
from member.models import Membership, Club
|
from member.models import Membership, Club
|
||||||
from note.models import SpecialTransaction, Alias
|
from note.models import SpecialTransaction, Alias
|
||||||
|
@ -193,16 +192,11 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = 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_username = "__iregex" if valid_regex else "__icontains"
|
|
||||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
|
||||||
prefix = "^" if valid_regex else ""
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(**{f"first_name{suffix}": pattern})
|
Q(first_name__iregex=pattern)
|
||||||
| Q(**{f"last_name{suffix}": pattern})
|
| Q(last_name__iregex=pattern)
|
||||||
| Q(**{f"profile__section{suffix}": pattern})
|
| Q(profile__section__iregex=pattern)
|
||||||
| Q(**{f"username{suffix_username}": prefix + pattern})
|
| Q(username__iregex="^" + pattern)
|
||||||
)
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
@ -383,7 +377,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
# membership._soge = True
|
# membership._soge = True
|
||||||
membership.save()
|
membership.save()
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE"))
|
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
if join_kfet:
|
if join_kfet:
|
||||||
|
@ -397,7 +391,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
# membership._soge = True
|
# membership._soge = True
|
||||||
membership.save()
|
membership.save()
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
membership.roles.add(Role.objects.get(name="Adhérent⋅e Kfet"))
|
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
for club, join_club in join_clubs:
|
for club, join_club in join_clubs:
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from api.filters import RegexSafeSearchFilter
|
from rest_framework.filters import SearchFilter
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer, \
|
from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer, \
|
||||||
|
@ -18,7 +18,7 @@ class InvoiceViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Invoice.objects.order_by('id')
|
queryset = Invoice.objects.order_by('id')
|
||||||
serializer_class = InvoiceSerializer
|
serializer_class = InvoiceSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
|
filterset_fields = ['bde', 'object', 'description', 'name', 'address', 'date', 'acquitted', 'locked', ]
|
||||||
search_fields = ['$object', '$description', '$name', '$address', ]
|
search_fields = ['$object', '$description', '$name', '$address', ]
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ class ProductViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Product.objects.order_by('invoice_id', 'id')
|
queryset = Product.objects.order_by('invoice_id', 'id')
|
||||||
serializer_class = ProductSerializer
|
serializer_class = ProductSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
|
filterset_fields = ['invoice', 'designation', 'quantity', 'amount', ]
|
||||||
search_fields = ['$designation', '$invoice__object', ]
|
search_fields = ['$designation', '$invoice__object', ]
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = RemittanceType.objects.order_by('id')
|
queryset = RemittanceType.objects.order_by('id')
|
||||||
serializer_class = RemittanceTypeSerializer
|
serializer_class = RemittanceTypeSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['note', ]
|
filterset_fields = ['note', ]
|
||||||
search_fields = ['$note__special_type', ]
|
search_fields = ['$note__special_type', ]
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class RemittanceViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Remittance.objects.order_by('id')
|
queryset = Remittance.objects.order_by('id')
|
||||||
serializer_class = RemittanceSerializer
|
serializer_class = RemittanceSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ]
|
filterset_fields = ['date', 'remittance_type', 'comment', 'closed', 'transaction_proxies__transaction', ]
|
||||||
search_fields = ['$remittance_type__note__special_type', '$comment', ]
|
search_fields = ['$remittance_type__note__special_type', '$comment', ]
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class SogeCreditViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = SogeCredit.objects.order_by('id')
|
queryset = SogeCredit.objects.order_by('id')
|
||||||
serializer_class = SogeCreditSerializer
|
serializer_class = SogeCreditSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name',
|
filterset_fields = ['user', 'user__last_name', 'user__first_name', 'user__email', 'user__note__alias__name',
|
||||||
'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ]
|
'user__note__alias__normalized_name', 'transactions', 'credit_transaction', ]
|
||||||
search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name',
|
search_fields = ['$user__last_name', '$user__first_name', '$user__email', '$user__note__alias__name',
|
||||||
|
|
|
@ -19,8 +19,7 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import UpdateView, DetailView
|
from django.views.generic import UpdateView, DetailView
|
||||||
from django.views.generic.base import View, TemplateView
|
from django.views.generic.base import View, TemplateView
|
||||||
from django.views.generic.edit import BaseFormView, DeleteView
|
from django.views.generic.edit import BaseFormView, DeleteView
|
||||||
from django_tables2 import MultiTableMixin, SingleTableMixin, SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from api.viewsets import is_regex
|
|
||||||
from note.models import SpecialTransaction, NoteSpecial, Alias
|
from note.models import SpecialTransaction, NoteSpecial, Alias
|
||||||
from note_kfet.settings.base import BASE_DIR
|
from note_kfet.settings.base import BASE_DIR
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
@ -252,26 +251,21 @@ class RemittanceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context["table"] = RemittanceTable(
|
||||||
|
data=Remittance.objects.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all())
|
||||||
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
|
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class RemittanceListView(LoginRequiredMixin, MultiTableMixin, TemplateView):
|
class RemittanceListView(LoginRequiredMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
List existing Remittances
|
List existing Remittances
|
||||||
"""
|
"""
|
||||||
template_name = "treasury/remittance_list.html"
|
template_name = "treasury/remittance_list.html"
|
||||||
extra_context = {"title": _("Remittances list")}
|
extra_context = {"title": _("Remittances list")}
|
||||||
|
|
||||||
tables = [
|
|
||||||
lambda data: RemittanceTable(data, prefix="opened-remittances-"),
|
|
||||||
lambda data: RemittanceTable(data, prefix="closed-remittances-"),
|
|
||||||
lambda data: SpecialTransactionTable(data, prefix="no-remittance-", exclude=('remittance_remove', )),
|
|
||||||
lambda data: SpecialTransactionTable(data, prefix="with-remittance-", exclude=('remittance_add', )),
|
|
||||||
]
|
|
||||||
paginate_by = 10 # number of rows in tables
|
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
# Check that the user is authenticated
|
# Check that the user is authenticated
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
|
@ -281,37 +275,49 @@ class RemittanceListView(LoginRequiredMixin, MultiTableMixin, TemplateView):
|
||||||
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_tables_data(self):
|
|
||||||
return [
|
|
||||||
Remittance.objects.filter(closed=False).filter(
|
|
||||||
PermissionBackend.filter_queryset(self.request, Remittance, "view")),
|
|
||||||
Remittance.objects.filter(closed=True).filter(
|
|
||||||
PermissionBackend.filter_queryset(self.request, Remittance, "view")),
|
|
||||||
SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
|
||||||
specialtransactionproxy__remittance=None).filter(
|
|
||||||
PermissionBackend.filter_queryset(self.request, Remittance, "view")),
|
|
||||||
SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
|
||||||
specialtransactionproxy__remittance__closed=False).filter(
|
|
||||||
PermissionBackend.filter_queryset(self.request, Remittance, "view")),
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
tables = context["tables"]
|
opened_remittances = RemittanceTable(
|
||||||
names = [
|
data=Remittance.objects.filter(closed=False).filter(
|
||||||
"opened_remittances",
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
|
||||||
"closed_remittances",
|
prefix="opened-remittances-",
|
||||||
"special_transactions_no_remittance",
|
)
|
||||||
"special_transactions_with_remittance",
|
opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
|
||||||
]
|
context["opened_remittances"] = opened_remittances
|
||||||
for name, table in zip(names, tables):
|
|
||||||
context[name] = table
|
closed_remittances = RemittanceTable(
|
||||||
|
data=Remittance.objects.filter(closed=True).filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
|
||||||
|
prefix="closed-remittances-",
|
||||||
|
)
|
||||||
|
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
|
||||||
|
context["closed_remittances"] = closed_remittances
|
||||||
|
|
||||||
|
no_remittance_tr = SpecialTransactionTable(
|
||||||
|
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||||
|
specialtransactionproxy__remittance=None).filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
|
||||||
|
exclude=('remittance_remove', ),
|
||||||
|
prefix="no-remittance-",
|
||||||
|
)
|
||||||
|
no_remittance_tr.paginate(page=self.request.GET.get("no-remittance-page", 1), per_page=10)
|
||||||
|
context["special_transactions_no_remittance"] = no_remittance_tr
|
||||||
|
|
||||||
|
with_remittance_tr = SpecialTransactionTable(
|
||||||
|
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||||
|
specialtransactionproxy__remittance__closed=False).filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
|
||||||
|
exclude=('remittance_add', ),
|
||||||
|
prefix="with-remittance-",
|
||||||
|
)
|
||||||
|
with_remittance_tr.paginate(page=self.request.GET.get("with-remittance-page", 1), per_page=10)
|
||||||
|
context["special_transactions_with_remittance"] = with_remittance_tr
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, UpdateView):
|
class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Update Remittance
|
Update Remittance
|
||||||
"""
|
"""
|
||||||
|
@ -319,18 +325,19 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
|
||||||
form_class = RemittanceForm
|
form_class = RemittanceForm
|
||||||
extra_context = {"title": _("Update a remittance")}
|
extra_context = {"title": _("Update a remittance")}
|
||||||
|
|
||||||
table_class = SpecialTransactionTable
|
|
||||||
context_table_name = "special_transactions"
|
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return reverse_lazy('treasury:remittance_list')
|
return reverse_lazy('treasury:remittance_list')
|
||||||
|
|
||||||
def get_table_data(self):
|
def get_context_data(self, **kwargs):
|
||||||
return SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
|
context = super().get_context_data(**kwargs)
|
||||||
PermissionBackend.filter_queryset(self.request, Remittance, "view"))
|
|
||||||
|
|
||||||
def get_table_kwargs(self):
|
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
|
||||||
return {"exclude": ('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', )}
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all()
|
||||||
|
context["special_transactions"] = SpecialTransactionTable(
|
||||||
|
data=data,
|
||||||
|
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
|
@ -404,16 +411,11 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
||||||
if "search" in self.request.GET:
|
if "search" in self.request.GET:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
if pattern:
|
if pattern:
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
|
||||||
valid_regex = is_regex(pattern)
|
|
||||||
suffix_alias = "__iregex" if valid_regex else "__icontains"
|
|
||||||
suffix = "__iregex" if valid_regex else "__istartswith"
|
|
||||||
prefix = "^" if valid_regex else ""
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(**{f"user__first_name{suffix}": pattern})
|
Q(user__first_name__iregex=pattern)
|
||||||
| Q(**{f"user__last_name{suffix}": pattern})
|
| Q(user__last_name__iregex=pattern)
|
||||||
| Q(**{f"user__note__alias__name{suffix_alias}": prefix + pattern})
|
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||||
| Q(**{f"user__note__alias__normalized_name{suffix_alias}": prefix + Alias.normalize(pattern)})
|
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
)
|
)
|
||||||
|
|
||||||
if "valid" not in self.request.GET or not self.request.GET["valid"]:
|
if "valid" not in self.request.GET or not self.request.GET["valid"]:
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter, SearchFilter
|
||||||
from api.filters import RegexSafeSearchFilter
|
|
||||||
from api.viewsets import ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
|
from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \
|
||||||
|
@ -19,7 +18,7 @@ class WEIClubViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = WEIClub.objects.order_by('id')
|
queryset = WEIClub.objects.order_by('id')
|
||||||
serializer_class = WEIClubSerializer
|
serializer_class = WEIClubSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name',
|
filterset_fields = ['name', 'year', 'date_start', 'date_end', 'email', 'note__alias__name',
|
||||||
'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships',
|
'note__alias__normalized_name', 'parent_club', 'parent_club__name', 'require_memberships',
|
||||||
'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start',
|
'membership_fee_paid', 'membership_fee_unpaid', 'membership_duration', 'membership_start',
|
||||||
|
@ -35,7 +34,7 @@ class BusViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Bus.objects.order_by('id')
|
queryset = Bus.objects.order_by('id')
|
||||||
serializer_class = BusSerializer
|
serializer_class = BusSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['name', 'wei', 'description', ]
|
filterset_fields = ['name', 'wei', 'description', ]
|
||||||
search_fields = ['$name', '$wei__name', '$description', ]
|
search_fields = ['$name', '$wei__name', '$description', ]
|
||||||
|
|
||||||
|
@ -48,7 +47,7 @@ class BusTeamViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = BusTeam.objects.order_by('id')
|
queryset = BusTeam.objects.order_by('id')
|
||||||
serializer_class = BusTeamSerializer
|
serializer_class = BusTeamSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
|
filterset_fields = ['name', 'bus', 'color', 'description', 'bus__wei', ]
|
||||||
search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
|
search_fields = ['$name', '$bus__name', '$bus__wei__name', '$description', ]
|
||||||
|
|
||||||
|
@ -61,7 +60,7 @@ class WEIRoleViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = WEIRole.objects.order_by('id')
|
queryset = WEIRole.objects.order_by('id')
|
||||||
serializer_class = WEIRoleSerializer
|
serializer_class = WEIRoleSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['name', 'permissions', 'memberships', ]
|
filterset_fields = ['name', 'permissions', 'memberships', ]
|
||||||
search_fields = ['$name', ]
|
search_fields = ['$name', ]
|
||||||
|
|
||||||
|
@ -74,7 +73,7 @@ class WEIRegistrationViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = WEIRegistration.objects.order_by('id')
|
queryset = WEIRegistration.objects.order_by('id')
|
||||||
serializer_class = WEIRegistrationSerializer
|
serializer_class = WEIRegistrationSerializer
|
||||||
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||||
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
|
filterset_fields = ['user', 'user__username', 'user__first_name', 'user__last_name', 'user__email',
|
||||||
'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
|
'user__note__alias__name', 'user__note__alias__normalized_name', 'wei', 'wei__name',
|
||||||
'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
|
'wei__email', 'wei__year', 'soge_credit', 'caution_check', 'birth_date', 'gender',
|
||||||
|
@ -93,7 +92,7 @@ class WEIMembershipViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = WEIMembership.objects.order_by('id')
|
queryset = WEIMembership.objects.order_by('id')
|
||||||
serializer_class = WEIMembershipSerializer
|
serializer_class = WEIMembershipSerializer
|
||||||
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
|
filter_backends = [DjangoFilterBackend, OrderingFilter, SearchFilter]
|
||||||
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name',
|
filterset_fields = ['club__name', 'club__email', 'club__note__alias__name',
|
||||||
'club__note__alias__normalized_name', 'user__username', 'user__last_name',
|
'club__note__alias__normalized_name', 'user__username', 'user__last_name',
|
||||||
'user__first_name', 'user__email', 'user__note__alias__name',
|
'user__first_name', 'user__email', 'user__note__alias__name',
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from bootstrap_datepicker_plus.widgets import DatePickerInput
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.forms import CheckboxSelectMultiple
|
from django.forms import CheckboxSelectMultiple
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note.models import NoteSpecial, NoteUser
|
from note.models import NoteSpecial, NoteUser
|
||||||
from note_kfet.inputs import AmountInput, Autocomplete, ColorWidget
|
from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget
|
||||||
|
|
||||||
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
|
from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole
|
||||||
|
|
||||||
|
@ -75,7 +74,7 @@ class WEIChooseBusForm(forms.Form):
|
||||||
queryset=WEIRole.objects.filter(~Q(name="1A")),
|
queryset=WEIRole.objects.filter(~Q(name="1A")),
|
||||||
label=_("WEI Roles"),
|
label=_("WEI Roles"),
|
||||||
help_text=_("Select the roles that you are interested in."),
|
help_text=_("Select the roles that you are interested in."),
|
||||||
initial=WEIRole.objects.filter(name="Adhérent⋅e WEI").all(),
|
initial=WEIRole.objects.filter(name="Adhérent WEI").all(),
|
||||||
widget=CheckboxSelectMultiple(),
|
widget=CheckboxSelectMultiple(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -84,5 +84,5 @@ class Command(BaseCommand):
|
||||||
s += sep + user.profile.section_generated
|
s += sep + user.profile.section_generated
|
||||||
s += sep + bus.name
|
s += sep + bus.name
|
||||||
s += sep + (team.name if team else "--")
|
s += sep + (team.name if team else "--")
|
||||||
s += sep + ", ".join(role.name for role in membership.roles.filter(~Q(name="Adhérent⋅e WEI")).all())
|
s += sep + ", ".join(role.name for role in membership.roles.filter(~Q(name="Adhérent WEI")).all())
|
||||||
self.stdout.write(s)
|
self.stdout.write(s)
|
||||||
|
|
|
@ -439,7 +439,7 @@ class TestWEIRegistration(TestCase):
|
||||||
emergency_contact_phone='+33123456789',
|
emergency_contact_phone='+33123456789',
|
||||||
))
|
))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertTrue("This user can't be in her/his first year since he/she has already participated to a WEI."
|
self.assertTrue("This user can't be in her/his first year since he/she has already participated to a WEI."
|
||||||
in str(response.context["form"].errors))
|
in str(response.context["form"].errors))
|
||||||
|
|
||||||
# Check that if the WEI is started, we can't register anyone
|
# Check that if the WEI is started, we can't register anyone
|
||||||
|
@ -504,7 +504,7 @@ class TestWEIRegistration(TestCase):
|
||||||
emergency_contact_phone='+33600000000',
|
emergency_contact_phone='+33600000000',
|
||||||
bus=[self.bus.id],
|
bus=[self.bus.id],
|
||||||
team=[self.team.id],
|
team=[self.team.id],
|
||||||
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
|
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent WEI").all()],
|
||||||
information_json=self.registration.information_json,
|
information_json=self.registration.information_json,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -558,7 +558,7 @@ class TestWEIRegistration(TestCase):
|
||||||
emergency_contact_phone='+33600000000',
|
emergency_contact_phone='+33600000000',
|
||||||
bus=[self.bus.id],
|
bus=[self.bus.id],
|
||||||
team=[self.team.id],
|
team=[self.team.id],
|
||||||
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent⋅e WEI").all()],
|
roles=[role.id for role in WEIRole.objects.filter(name="Adhérent WEI").all()],
|
||||||
information_json=self.registration.information_json,
|
information_json=self.registration.information_json,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -635,7 +635,7 @@ class TestWEIRegistration(TestCase):
|
||||||
))
|
))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertFalse(response.context["form"].is_valid())
|
self.assertFalse(response.context["form"].is_valid())
|
||||||
self.assertTrue("This team doesn't belong to the given bus." in str(response.context["form"].errors))
|
self.assertTrue("This team doesn't belong to the given bus." in str(response.context["form"].errors))
|
||||||
|
|
||||||
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
|
response = self.client.post(reverse("wei:validate_registration", kwargs=dict(pk=self.registration.pk)), dict(
|
||||||
roles=[WEIRole.objects.get(name="GC WEI").id],
|
roles=[WEIRole.objects.get(name="GC WEI").id],
|
||||||
|
|
|
@ -22,8 +22,7 @@ from django.views import View
|
||||||
from django.views.generic import DetailView, UpdateView, RedirectView, TemplateView
|
from django.views.generic import DetailView, UpdateView, RedirectView, TemplateView
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic.edit import BaseFormView, DeleteView
|
from django.views.generic.edit import BaseFormView, DeleteView
|
||||||
from django_tables2 import SingleTableView, MultiTableMixin
|
from django_tables2 import SingleTableView
|
||||||
from api.viewsets import is_regex
|
|
||||||
from member.models import Membership, Club
|
from member.models import Membership, Club
|
||||||
from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial
|
from note.models import Transaction, NoteClub, Alias, SpecialTransaction, NoteSpecial
|
||||||
from note.tables import HistoryTable
|
from note.tables import HistoryTable
|
||||||
|
@ -101,7 +100,7 @@ class WEICreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk})
|
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View WEI information
|
View WEI information
|
||||||
"""
|
"""
|
||||||
|
@ -109,40 +108,34 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
|
||||||
context_object_name = "club"
|
context_object_name = "club"
|
||||||
extra_context = {"title": _("WEI Detail")}
|
extra_context = {"title": _("WEI Detail")}
|
||||||
|
|
||||||
tables = [
|
|
||||||
lambda data: HistoryTable(data, prefix="history-"),
|
|
||||||
lambda data: WEIMembershipTable(data, prefix="membership-"),
|
|
||||||
lambda data: WEIRegistrationTable(data, prefix="pre-registration-"),
|
|
||||||
lambda data: BusTable(data, prefix="bus-"),
|
|
||||||
]
|
|
||||||
paginate_by = 20 # number of rows in tables
|
|
||||||
|
|
||||||
def get_tables_data(self):
|
|
||||||
club = self.object
|
|
||||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
|
|
||||||
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view")) \
|
|
||||||
.order_by('-created_at', '-id')
|
|
||||||
club_member = WEIMembership.objects.filter(
|
|
||||||
club=club,
|
|
||||||
date_end__gte=date.today(),
|
|
||||||
).filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view"))
|
|
||||||
pre_registrations = WEIRegistration.objects.filter(
|
|
||||||
PermissionBackend.filter_queryset(self.request, WEIRegistration, "view")).filter(
|
|
||||||
membership=None,
|
|
||||||
wei=club
|
|
||||||
)
|
|
||||||
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
|
|
||||||
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
|
|
||||||
return [club_transactions, club_member, pre_registrations, buses, ]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
club = context["club"]
|
club = context["club"]
|
||||||
|
|
||||||
tables = context["tables"]
|
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
|
||||||
for name, table in zip(["history_list", "member_list", "pre_registrations", "buses"], tables):
|
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view")) \
|
||||||
context[name] = table
|
.order_by('-created_at', '-id')
|
||||||
|
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||||
|
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||||
|
context['history_list'] = history_table
|
||||||
|
|
||||||
|
club_member = WEIMembership.objects.filter(
|
||||||
|
club=club,
|
||||||
|
date_end__gte=date.today(),
|
||||||
|
).filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view"))
|
||||||
|
membership_table = WEIMembershipTable(data=club_member, prefix="membership-")
|
||||||
|
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
||||||
|
context['member_list'] = membership_table
|
||||||
|
|
||||||
|
pre_registrations = WEIRegistration.objects.filter(
|
||||||
|
PermissionBackend.filter_queryset(self.request, WEIRegistration, "view")).filter(
|
||||||
|
membership=None,
|
||||||
|
wei=club
|
||||||
|
)
|
||||||
|
pre_registrations_table = WEIRegistrationTable(data=pre_registrations, prefix="pre-registration-")
|
||||||
|
pre_registrations_table.paginate(per_page=20, page=self.request.GET.get('pre-registration-page', 1))
|
||||||
|
context['pre_registrations'] = pre_registrations_table
|
||||||
|
|
||||||
my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user)
|
my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user)
|
||||||
if my_registration.exists():
|
if my_registration.exists():
|
||||||
|
@ -151,6 +144,11 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, D
|
||||||
my_registration = None
|
my_registration = None
|
||||||
context["my_registration"] = my_registration
|
context["my_registration"] = my_registration
|
||||||
|
|
||||||
|
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
|
||||||
|
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
|
||||||
|
bus_table = BusTable(data=buses, prefix="bus-")
|
||||||
|
context['buses'] = bus_table
|
||||||
|
|
||||||
random_user = User.objects.filter(~Q(wei__wei__in=[club])).first()
|
random_user = User.objects.filter(~Q(wei__wei__in=[club])).first()
|
||||||
|
|
||||||
if random_user is None:
|
if random_user is None:
|
||||||
|
@ -221,18 +219,13 @@ class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||||
if not pattern:
|
if not pattern:
|
||||||
return qs.none()
|
return qs.none()
|
||||||
|
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
|
||||||
valid_regex = is_regex(pattern)
|
|
||||||
suffix_alias = "__iregex" if valid_regex else "__istartswith"
|
|
||||||
suffix = "__iregex" if valid_regex else "__icontains"
|
|
||||||
prefix = "^" if valid_regex else ""
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(**{f"user__first_name{suffix}": pattern})
|
Q(user__first_name__iregex=pattern)
|
||||||
| Q(**{f"user__last_name{suffix}": pattern})
|
| Q(user__last_name__iregex=pattern)
|
||||||
| Q(**{f"user__note__alias__name{suffix_alias}": prefix + pattern})
|
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||||
| Q(**{f"user__note__alias__normalized_name{suffix_alias}": prefix + Alias.normalize(pattern)})
|
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
| Q(**{f"bus__name{suffix}": pattern})
|
| Q(bus__name__iregex=pattern)
|
||||||
| Q(**{f"team__name{suffix}": pattern})
|
| Q(team__name__iregex=pattern)
|
||||||
)
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
@ -262,16 +255,11 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
|
||||||
pattern = self.request.GET.get("search", "")
|
pattern = self.request.GET.get("search", "")
|
||||||
|
|
||||||
if pattern:
|
if pattern:
|
||||||
# Check if this is a valid regex. If not, we won't check regex
|
|
||||||
valid_regex = is_regex(pattern)
|
|
||||||
suffix_alias = "__iregex" if valid_regex else "__istartswith"
|
|
||||||
suffix = "__iregex" if valid_regex else "__icontains"
|
|
||||||
prefix = "^" if valid_regex else ""
|
|
||||||
qs = qs.filter(
|
qs = qs.filter(
|
||||||
Q(**{f"user__first_name{suffix}": pattern})
|
Q(user__first_name__iregex=pattern)
|
||||||
| Q(**{f"user__last_name{suffix}": pattern})
|
| Q(user__last_name__iregex=pattern)
|
||||||
| Q(**{f"user__note__alias__name{suffix_alias}": prefix + pattern})
|
| Q(user__note__alias__name__iregex="^" + pattern)
|
||||||
| Q(**{f"user__note__alias__normalized_name{suffix_alias}": prefix + Alias.normalize(pattern)})
|
| Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
||||||
)
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
@ -928,7 +916,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
form["team"].initial = BusTeam.objects.get(pk=information["preferred_team_pk"][0])
|
form["team"].initial = BusTeam.objects.get(pk=information["preferred_team_pk"][0])
|
||||||
if "preferred_roles_pk" in information:
|
if "preferred_roles_pk" in information:
|
||||||
form["roles"].initial = WEIRole.objects.filter(
|
form["roles"].initial = WEIRole.objects.filter(
|
||||||
Q(pk__in=information["preferred_roles_pk"]) | Q(name="Adhérent⋅e WEI")
|
Q(pk__in=information["preferred_roles_pk"]) | Q(name="Adhérent WEI")
|
||||||
).all()
|
).all()
|
||||||
return form
|
return form
|
||||||
|
|
||||||
|
@ -1020,7 +1008,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
|
|
||||||
membership.save()
|
membership.save()
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
membership.roles.add(WEIRole.objects.get(name="Adhérent⋅e WEI"))
|
membership.roles.add(WEIRole.objects.get(name="Adhérent WEI"))
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 143 KiB |
|
@ -23,7 +23,7 @@ Pages de l'API
|
||||||
Il suffit d'ajouter le préfixe ``/api/`` pour arriver sur ces pages.
|
Il suffit d'ajouter le préfixe ``/api/`` pour arriver sur ces pages.
|
||||||
|
|
||||||
* `models <basic#type-de-contenu>`_ : liste des différents modèles enregistrés en base de données
|
* `models <basic#type-de-contenu>`_ : liste des différents modèles enregistrés en base de données
|
||||||
* `user <basic#utilisateur>`_ : liste des différent⋅es utilisateur⋅rices enregistrés
|
* `user <basic#utilisateur>`_ : liste des différents utilisateurs enregistrés
|
||||||
* `members/profile <member#profil-utilisateur>`_ : liste des différents profils associés à des utilisateurs
|
* `members/profile <member#profil-utilisateur>`_ : liste des différents profils associés à des utilisateurs
|
||||||
* `members/club <member#club>`_ : liste des différents clubs enregistrés
|
* `members/club <member#club>`_ : liste des différents clubs enregistrés
|
||||||
* `members/membership <member#adhesion>`_ : liste des adhésions enregistrées
|
* `members/membership <member#adhesion>`_ : liste des adhésions enregistrées
|
||||||
|
@ -69,7 +69,7 @@ S'authentifier
|
||||||
|
|
||||||
L'authentification peut se faire soit par session en se connectant via la page de connexion classique,
|
L'authentification peut se faire soit par session en se connectant via la page de connexion classique,
|
||||||
soit via un jeton d'authentification. Le jeton peut se récupérer via la page de son propre compte, en cliquant
|
soit via un jeton d'authentification. Le jeton peut se récupérer via la page de son propre compte, en cliquant
|
||||||
sur le bouton « `Accès API <https://note.crans.org/accounts/manage-auth-token/>`_ ». Il peut être révoqué et régénéré
|
sur le bouton « `Accès API <https://note.crans.org/accounts/manage-auth-token/>`_ ». Il peut être révoqué et regénéré
|
||||||
en un clic.
|
en un clic.
|
||||||
|
|
||||||
Pour s'authentifier via ce jeton, il faut ajouter l'en-tête ``Authorization: Token <TOKEN>`` aux paramètres HTTP.
|
Pour s'authentifier via ce jeton, il faut ajouter l'en-tête ``Authorization: Token <TOKEN>`` aux paramètres HTTP.
|
||||||
|
@ -111,7 +111,7 @@ Trois types de filtres sont implémentés :
|
||||||
|
|
||||||
Les filtres disponibles sont indiqués sur chacune des pages de documentation.
|
Les filtres disponibles sont indiqués sur chacune des pages de documentation.
|
||||||
|
|
||||||
Le résultat est déjà par défaut filtré par droits : seuls les éléments que l'utilisateur⋅rice a le droit de voir sont affichés.
|
Le résultat est déjà par défaut filtré par droits : seuls les éléments que l'utilisateur à le droit de voir sont affichés.
|
||||||
Cela est possible grâce à la structure des permissions, générant justement des filtres de requêtes de base de données.
|
Cela est possible grâce à la structure des permissions, générant justement des filtres de requêtes de base de données.
|
||||||
|
|
||||||
Une requête à l'adresse ``/api/<model>/<pk>/`` affiche directement les informations du modèle demandé au format JSON.
|
Une requête à l'adresse ``/api/<model>/<pk>/`` affiche directement les informations du modèle demandé au format JSON.
|
||||||
|
@ -126,9 +126,8 @@ Des exceptions sont faites sur certaines pages : les pages de logs et de content
|
||||||
|
|
||||||
Les formats supportés sont multiples : ``application/json``, ``application/x-www-url-encoded``, ``multipart/form-data``.
|
Les formats supportés sont multiples : ``application/json``, ``application/x-www-url-encoded``, ``multipart/form-data``.
|
||||||
Cela facilite l'envoi de requêtes. Le module construit ensuite l'instance du modèle et le sauvegarde dans la base de
|
Cela facilite l'envoi de requêtes. Le module construit ensuite l'instance du modèle et le sauvegarde dans la base de
|
||||||
données. L'application ``permission`` s'assure que l'utilisateur⋅rice a le droit de faire ce type de modification.
|
données. L'application ``permission`` s'assure que l'utilisateur à le droit de faire ce type de modification. La réponse
|
||||||
La réponse renvoyée est l'objet enregistré au format JSON si l'ajout s'est bien déroulé, sinon un message d'erreur au
|
renvoyée est l'objet enregistré au format JSON si l'ajout s'est bien déroulé, sinon un message d'erreur au format JSON.
|
||||||
format JSON.
|
|
||||||
|
|
||||||
PATCH
|
PATCH
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
|
@ -135,7 +135,7 @@ Options
|
||||||
"required": false,
|
"required": false,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"label": "Pay\u00e9",
|
"label": "Pay\u00e9",
|
||||||
"help_text": "Indique si l'utilisateur⋅rice per\u00e7oit un salaire."
|
"help_text": "Indique si l'utilisateur per\u00e7oit un salaire."
|
||||||
},
|
},
|
||||||
"ml_events_registration": {
|
"ml_events_registration": {
|
||||||
"type": "choice",
|
"type": "choice",
|
||||||
|
|
|
@ -511,7 +511,7 @@ Options
|
||||||
"required": false,
|
"required": false,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"label": "Premi\u00e8re ann\u00e9e",
|
"label": "Premi\u00e8re ann\u00e9e",
|
||||||
"help_text": "Indique si l'utilisateur⋅rice est nouvelleau dans l'\u00e9cole."
|
"help_text": "Indique si l'utilisateur est nouveau dans l'\u00e9cole."
|
||||||
},
|
},
|
||||||
"information_json": {
|
"information_json": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -524,7 +524,7 @@ Options
|
||||||
"type": "field",
|
"type": "field",
|
||||||
"required": true,
|
"required": true,
|
||||||
"read_only": false,
|
"read_only": false,
|
||||||
"label": "Utilisateur⋅rice"
|
"label": "Utilisateur"
|
||||||
},
|
},
|
||||||
"wei": {
|
"wei": {
|
||||||
"type": "field",
|
"type": "field",
|
||||||
|
|
|
@ -3,12 +3,12 @@ Application Activités
|
||||||
|
|
||||||
L'application activités gère les différentes activités liées au BDE. Elle permet entre autres de créer des activités qui
|
L'application activités gère les différentes activités liées au BDE. Elle permet entre autres de créer des activités qui
|
||||||
peuvent être diffusées via des calendriers ou la mailing list d'événements. Elle permet aussi de réguler l'accès aux
|
peuvent être diffusées via des calendriers ou la mailing list d'événements. Elle permet aussi de réguler l'accès aux
|
||||||
événements, en s'assurant que leur note est positive. Elle permet enfin de gérer les invité⋅es.
|
événements, en s'assurant que leur note est positive. Elle permet enfin de gérer les invités.
|
||||||
|
|
||||||
Modèles
|
Modèles
|
||||||
-------
|
-------
|
||||||
|
|
||||||
L'application comporte 6 modèles : activités, types d'activité, invité⋅es, entrées et transactions d'invitation et les ouvreur⋅ses.
|
L'application comporte 5 modèles : activités, types d'activité, invités, entrées et transactions d'invitation.
|
||||||
|
|
||||||
Types d'activité
|
Types d'activité
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
@ -16,7 +16,7 @@ Types d'activité
|
||||||
Les activités sont triées par type (pots, soirées de club, ...), et chaque type regroupe diverses informations :
|
Les activités sont triées par type (pots, soirées de club, ...), et chaque type regroupe diverses informations :
|
||||||
|
|
||||||
* Nom du type
|
* Nom du type
|
||||||
* Possibilité d'inviter des non-adhérent⋅es (booléen)
|
* Possibilité d'inviter des non-adhérents (booléen)
|
||||||
* Prix d'invitation (entier, centimes à débiter sur la note de l'hôte)
|
* Prix d'invitation (entier, centimes à débiter sur la note de l'hôte)
|
||||||
|
|
||||||
Activités
|
Activités
|
||||||
|
@ -26,7 +26,7 @@ Le modèle d'activité regroupe les informations liées à l'activité même :
|
||||||
|
|
||||||
* Nom de l'activité
|
* Nom de l'activité
|
||||||
* Description de l'activité
|
* Description de l'activité
|
||||||
* Créateur⋅rice, personne qui a proposé l'activité
|
* Créateur, personne qui a proposé l'activité
|
||||||
* Club ayant organisé l'activité
|
* Club ayant organisé l'activité
|
||||||
* Note sur laquelle verser les crédits d'invitation (peut être nul si non concerné)
|
* Note sur laquelle verser les crédits d'invitation (peut être nul si non concerné)
|
||||||
* Club invité (généralement le club Kfet)
|
* Club invité (généralement le club Kfet)
|
||||||
|
@ -38,19 +38,19 @@ Le modèle d'activité regroupe les informations liées à l'activité même :
|
||||||
Entrées
|
Entrées
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
Une instance de ce modèle est créé dès que quelqu'un⋅e est inscrit⋅e à l'activité. Sont stockées les informations suivantes :
|
Une instance de ce modèle est créé dès que quelqu'un est inscrit à l'activité. Sont stockées les informations suivantes :
|
||||||
|
|
||||||
* Activité concernée (clé étrangère)
|
* Activité concernée (clé étrangère)
|
||||||
* Heure d'entrée
|
* Heure d'entrée
|
||||||
* Note de la personne entrée, ou hôte s'il s'agit d'un⋅e invité⋅e (clé étrangère vers ``NoteUser``)
|
* Note de la personne entrée, ou hôte s'il s'agit d'un invité (clé étrangère vers ``NoteUser``)
|
||||||
* Invité⋅e (``OneToOneField`` vers ``Guest``, ``None`` si c'est la personne elle-même qui rentre et non saon invité⋅e)
|
* Invité (``OneToOneField`` vers ``Guest``, ``None`` si c'est la personne elle-même qui rentre et non son invité)
|
||||||
|
|
||||||
Il n'est pas possible de créer une entrée si la note est en négatif.
|
Il n'est pas possible de créer une entrée si la note est en négatif.
|
||||||
|
|
||||||
Invité⋅es
|
Invités
|
||||||
~~~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
Les adhérent⋅es ont la possibilité d'inviter des ami⋅es. Pour cela, les différentes informations sont enregistrées :
|
Les adhérents ont la possibilité d'inviter des amis. Pour cela, les différentes informations sont enregistrées :
|
||||||
|
|
||||||
* Activité concernée (clé étrangère)
|
* Activité concernée (clé étrangère)
|
||||||
* Nom de famille
|
* Nom de famille
|
||||||
|
@ -60,7 +60,7 @@ Les adhérent⋅es ont la possibilité d'inviter des ami⋅es. Pour cela, les di
|
||||||
Certaines contraintes s'appliquent :
|
Certaines contraintes s'appliquent :
|
||||||
|
|
||||||
* Une personne ne peut pas être invitée plus de 5 fois par an (coupe nom/prénom)
|
* Une personne ne peut pas être invitée plus de 5 fois par an (coupe nom/prénom)
|
||||||
* Un⋅e adhérent⋅e ne peut pas inviter plus de 3 personnes par activité.
|
* Un adhérent ne peut pas inviter plus de 3 personnes par activité.
|
||||||
|
|
||||||
Transactions d'invitation
|
Transactions d'invitation
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -71,17 +71,6 @@ comportent qu'un champ supplémentaire, de type ``OneToOneField`` vers ``Guest``
|
||||||
Ce modèle aurait pu appartenir à l'application ``note``, mais afin de rester modulaire et que l'application ``note``
|
Ce modèle aurait pu appartenir à l'application ``note``, mais afin de rester modulaire et que l'application ``note``
|
||||||
ne dépende pas de cette application, on procède de cette manière.
|
ne dépende pas de cette application, on procède de cette manière.
|
||||||
|
|
||||||
Ouvreur⋅ses
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
Depuis la page d'une activité, il est possible d'ajouter des personnes en tant qu'« ouvreur⋅se ». Cela permet à une
|
|
||||||
personne sans aucun droit note de pouvoir faire les entrées d'une ``Activity``. Ce rôle n'est valable que pendant que
|
|
||||||
l'activité est ouverte et sur aucune autre activité. Les ouvreur⋅ses ont aussi accès à l'interface des transactions.
|
|
||||||
|
|
||||||
Ce modèle regroupe :
|
|
||||||
* Activité (clé étrangère)
|
|
||||||
* Note (clé étrangère)
|
|
||||||
|
|
||||||
Graphe
|
Graphe
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
|
@ -94,15 +83,15 @@ UI
|
||||||
Création d'activités
|
Création d'activités
|
||||||
~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
N'importe quel⋅le adhérent⋅e Kfet peut suggérer l'ajout d'une activité via un formulaire.
|
N'importe quel adhérent Kfet peut suggérer l'ajout d'une activité via un formulaire.
|
||||||
|
|
||||||
Gestion des activités
|
Gestion des activités
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Les ayant-droit (Res[pot] et respos infos) peuvent valider les activités proposées. Ils peuvent également la modifier
|
Les ayant-droit (Res[pot] et respos infos) peuvent valider les activités proposées. Ils peuvent également la modifier
|
||||||
si besoin. Iels peuvent enfin la déclarer ouverte pour lancer l'accès aux entrées.
|
si besoin. Ils peuvent enfin la déclarer ouvertes pour lancer l'accès aux entrées.
|
||||||
|
|
||||||
N'importe qui peut inviter des ami⋅es non adhérent⋅es, tant que les contraintes de nombre (un⋅e adhérent⋅e n'invite pas plus de
|
N'importe qui peut inviter des amis non adhérents, tant que les contraintes de nombre (un adhérent n'invite pas plus de
|
||||||
trois personnes par activité et une personne ne peut pas être invitée plus de 5 fois par an). L'invitation est
|
trois personnes par activité et une personne ne peut pas être invitée plus de 5 fois par an). L'invitation est
|
||||||
facturée à l'entrée.
|
facturée à l'entrée.
|
||||||
|
|
||||||
|
@ -110,15 +99,12 @@ Entrées aux soirées
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
L'interface d'entrées est simple et ergonomique. Elle contient un champ de texte. À chaque fois que le champ est
|
L'interface d'entrées est simple et ergonomique. Elle contient un champ de texte. À chaque fois que le champ est
|
||||||
modifié, un tableau est affiché comprenant la liste des invité⋅es et des adhérent⋅es dont le prénom, le nom ou un alias
|
modifié, un tableau est affiché comprenant la liste des invités et des adhérents dont le prénom, le nom ou un alias
|
||||||
de la note est acceptée par le texte entré.
|
de la note est acceptée par le texte entré.
|
||||||
|
|
||||||
En cliquant sur la ligne de la personne qui souhaite rentrer, s'il s'agit d'un⋅e adhérent⋅e, alors la personne est comptée
|
En cliquant sur la ligne de la personne qui souhaite rentrée, s'il s'agit d'un adhérent, alors la personne est comptée
|
||||||
comme entrée à l'activité, sous réserve que sa note soit positive. S'il s'agit d'un⋅e invité⋅e, alors 3 boutons
|
comme entrée à l'activité, sous réserve que sa note soit positive. S'il s'agit d'un invité, alors 3 boutons
|
||||||
apparaîssent, afin de régler la taxe d'invitation : l'un prélève directement depuis la note de l'hôte, les deux autres
|
apparaîssent, afin de régler la taxe d'invitation : l'un prélève directement depuis la note de l'hôte, les deux autres
|
||||||
permettent un paiement par espèces ou par carte bancaire. En réalité, les deux derniers boutons enregistrent
|
permettent un paiement par espèces ou par carte bancaire. En réalité, les deux derniers boutons enregistrent
|
||||||
automatiquement un crédit sur la note de l'hôte, puis une transaction (de type ``GuestTransaction``) est faite depuis
|
automatiquement un crédit sur la note de l'hôte, puis une transaction (de type ``GuestTransaction``) est faite depuis
|
||||||
la note de l'hôte vers la note du club organisateur de l'événement.
|
la note de l'hôte vers la note de l'organisateur de l'événement.
|
||||||
|
|
||||||
Si une personne souhaite faire les entrées, il est possible de l'ajouter dans la liste des ouvreur⋅ses depuis la page
|
|
||||||
de l'activité.
|
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
Application Food
|
|
||||||
================
|
|
||||||
|
|
||||||
L'application ``food`` s'occupe de la traçabilité et permet notamment l'obtention de la liste des allergènes.
|
|
||||||
|
|
||||||
Modèles
|
|
||||||
-------
|
|
||||||
|
|
||||||
L'application comporte 5 modèles : Allergen, QRCode, Food, BasicFood, TransformedFood.
|
|
||||||
|
|
||||||
Food
|
|
||||||
~~~~
|
|
||||||
|
|
||||||
Ce modèle est un PolymorphicModel et ne sert uniquement à créer BasicFood et TransformedFood.
|
|
||||||
|
|
||||||
Le modèle regroupe :
|
|
||||||
|
|
||||||
* Nom du produit
|
|
||||||
* Propriétaire (doit-être un Club)
|
|
||||||
* Allergènes (ManyToManyField)
|
|
||||||
* date d'expiration
|
|
||||||
* a été mangé (booléen)
|
|
||||||
* est prêt (booléen)
|
|
||||||
|
|
||||||
BasicFood
|
|
||||||
~~~~~~~~~
|
|
||||||
|
|
||||||
Les BasicFood correspondent aux produits non modifiés à la Kfet. Ils peuvent correspondre à la fois à des produits achetés en magasin ou à des produits Terre à Terre. Ces produits seront les ingrédients de tous les plats préparés et en conséquent sont les seuls produits à nécessité une saisie manuelle des allergènes.
|
|
||||||
|
|
||||||
Le modèle regroupe :
|
|
||||||
|
|
||||||
* Type de date (DLC = date limite de consommation, DDM = date de durabilité minimale)
|
|
||||||
* Date d'arrivée
|
|
||||||
* Champs de Food
|
|
||||||
|
|
||||||
TransformedFood
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Les TransformedFood correspondent aux produits préparés à la Kfet. Ils peuvent être composés de BasicFood et/ou de TransformedFood. La date d'expiration et les allergènes sont automatiquement mis à jour par update (qui doit être exécuté après modification des ingrédients dans les forms par exemple).
|
|
||||||
|
|
||||||
Le modèle regroupe :
|
|
||||||
|
|
||||||
* Durée de consommation (par défaut 3 jours)
|
|
||||||
* Ingrédients (ManyToManyField vers Food)
|
|
||||||
* Date de création
|
|
||||||
* Champs de Food
|
|
||||||
|
|
||||||
Allergen
|
|
||||||
~~~~~~~~
|
|
||||||
|
|
||||||
Le modèle regroupe :
|
|
||||||
|
|
||||||
* Nom
|
|
||||||
|
|
||||||
QRCode
|
|
||||||
~~~~~~
|
|
||||||
|
|
||||||
Le modèle regroupe :
|
|
||||||
|
|
||||||
* nombre (unique, entier positif)
|
|
||||||
* food (OneToOneField vers Food)
|
|
||||||
|
|
||||||
Création de BasicFood
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Un BasicFood a toujours besoin d'un QRCode (depuis l'interface web). Il convient donc de coller le QRCode puis de le scanner et de compléter le formulaire.
|
|
||||||
|
|
||||||
Création de TransformedFood
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Pour créer un TransformedFood, il suffit d'aller dans l'onglet ``traçabilité`` et de cliquer sur l'onglet.
|
|
||||||
|
|
||||||
Ajouter un ingrédient
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Un ingrédient a forcément un QRCode. Il convient donc de scanner le QRCode de l'ingrédient et de sélectionner le produit auquel il doit être ajouté.
|
|
||||||
|
|
||||||
Remarque : Un produit fini doit avoir un QRCode et inversement.
|
|
||||||
|
|
||||||
Terminer un plat
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Il suffit de coller le QRCode sur le plat, de le scanner et de sélectionner le produit.
|
|
|
@ -1,5 +1,5 @@
|
||||||
Applications de la Note Kfet 2020
|
Applications de la NoteKfet2020
|
||||||
=================================
|
===============================
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
@ -15,26 +15,27 @@ Applications de la Note Kfet 2020
|
||||||
treasury
|
treasury
|
||||||
wei
|
wei
|
||||||
|
|
||||||
La Note Kfet 2020 est un projet Django, décomposé en applications.
|
La NoteKfet est un projet Django, décomposé en applications.
|
||||||
Certaines applications sont développées uniquement pour ce projet, et sont indispensables,
|
Certaines Applications sont développées uniquement pour ce projet, et sont indispensables,
|
||||||
d'autres sont packagées et sont installées comme dépendances.
|
d'autres sont packagesé et sont installées comme dépendances.
|
||||||
Enfin, des fonctionnalités annexes ont été rajoutées, mais ne sont pas essentielles au déploiement de la Note Kfet 2020. Leur usage est cependant recommandé.
|
Enfin des fonctionnalités annexes ont été rajouté, mais ne sont pas essentiel au déploiement de la NoteKfet;
|
||||||
|
leur usage est cependant recommandé.
|
||||||
|
|
||||||
L'affichage Web utilise le framework Bootstrap4 et quelques morceaux de JavaScript personnalisés.
|
Le front utilise le framework Bootstrap4 et quelques morceaux de javascript custom.
|
||||||
|
|
||||||
Applications indispensables
|
Applications indispensables
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
* ``note_kfet`` :
|
* ``note_kfet`` :
|
||||||
Application "projet" de django, c'est ici que la configuration de la note est gérée.
|
Application "projet" de django, c'est ici que la config de la note est gérée.
|
||||||
* `Member <member>`_ :
|
* `Member <member>`_ :
|
||||||
Gestion des profils d'utilisateur⋅rices, des clubs et de leur membres.
|
Gestion des profils d'utilisateurs, des clubs et de leur membres.
|
||||||
* `Note <note>`_ :
|
* `Note <note>`_ :
|
||||||
Les notes associées à des utilisateur⋅rices ou des clubs.
|
Les notes associés a des utilisateurs ou des clubs.
|
||||||
* `Activity <activity>`_ :
|
* `Activity <activity>`_ :
|
||||||
La gestion des activités (créations, gestion, entrées,…)
|
La gestion des Activités (créations, gestion, entrée...)
|
||||||
* `Permission <permission>`_ :
|
* `Permission <permission>`_ :
|
||||||
Backend de droits, limites les pouvoirs des utilisateur⋅rices
|
Backend de droits, limites les pouvoirs des utilisateurs
|
||||||
* `API <../api>`_ :
|
* `API <../api>`_ :
|
||||||
API REST de la note, est notamment utilisée pour rendre la note dynamique
|
API REST de la note, est notamment utilisée pour rendre la note dynamique
|
||||||
(notamment la page de conso)
|
(notamment la page de conso)
|
||||||
|
@ -51,9 +52,9 @@ Applications packagées
|
||||||
`<https://django-polymorphic.readthedocs.io/en/stable/>`_
|
`<https://django-polymorphic.readthedocs.io/en/stable/>`_
|
||||||
|
|
||||||
* ``crispy_forms``
|
* ``crispy_forms``
|
||||||
Utiliser pour générer des formulaires avec Bootstrap4
|
Utiliser pour générer des forms avec bootstrap4
|
||||||
* ``django_tables2``
|
* ``django_tables2``
|
||||||
utiliser pour afficher des tables de données et les formater, en Python plutôt qu'en HTML.
|
utiliser pour afficher des tables de données et les formater, en python plutôt qu'en HTML.
|
||||||
* ``restframework``
|
* ``restframework``
|
||||||
Base de l'`API <../api>`_.
|
Base de l'`API <../api>`_.
|
||||||
|
|
||||||
|
@ -62,11 +63,11 @@ Applications facultatives
|
||||||
* `Logs <logs>`_
|
* `Logs <logs>`_
|
||||||
Enregistre toute les modifications effectuées en base de donnée.
|
Enregistre toute les modifications effectuées en base de donnée.
|
||||||
* ``cas-server``
|
* ``cas-server``
|
||||||
Serveur central d'authentification, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client.
|
Serveur central d'authenfication, permet d'utiliser son compte de la NoteKfet2020 pour se connecter à d'autre application ayant intégrer un client.
|
||||||
* `Scripts <https://gitlab.crans.org/bde/nk20-scripts>`_
|
* `Script <https://gitlab.crans.org/bde/nk20-scripts>`_
|
||||||
Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc…
|
Ensemble de commande `./manage.py` pour la gestion de la note: import de données, verification d'intégrité, etc ...
|
||||||
* `Treasury <treasury>`_ :
|
* `Treasury <treasury>`_ :
|
||||||
Interface de gestion pour les trésorièr⋅es, émission de factures, remises de chèque, statistiques ...
|
Interface de gestion pour les trésoriers, émission de facture, remise de chèque, statistiques ...
|
||||||
* `WEI <wei>`_ :
|
* `WEI <wei>`_ :
|
||||||
Interface de gestion du WEI.
|
Interface de gestion du WEI.
|
||||||
|
|
||||||
|
|
|
@ -6,22 +6,22 @@ Chaque modification effectuée sur un modèle est enregistrée dans la base dans
|
||||||
Dès qu'un modèle veut être sauvegardé, deux signaux sont envoyés dans ``logs.signals`` : un avant et un après
|
Dès qu'un modèle veut être sauvegardé, deux signaux sont envoyés dans ``logs.signals`` : un avant et un après
|
||||||
la sauvegarde.
|
la sauvegarde.
|
||||||
En pré-sauvegarde, on récupère l'ancienne version du modèle, si elle existe.
|
En pré-sauvegarde, on récupère l'ancienne version du modèle, si elle existe.
|
||||||
En post-sauvegarde, on récupère l'utilisateur⋅rice et l'IP courant⋅es (voir ci-dessous), on convertit les modèles en JSON
|
En post-sauvegarde, on récupère l'utilisateur et l'IP courants (voir ci-dessous), on convertit les modèles en JSON
|
||||||
et on enregistre une entrée ``Changelog`` dans la base de données.
|
et on enregistre une entrée ``Changelog`` dans la base de données.
|
||||||
|
|
||||||
Pour récupérer l'utilisateur⋅rice et son IP, le middleware ``logs.middlewares.LogsMiddlewares`` récupère à chaque requête
|
Pour récupérer l'utilisateur et son IP, le middleware ``logs.middlewares.LogsMiddlewares`` récupère à chaque requête
|
||||||
l'utilisateur⋅rice et l'adresse IP, et les stocke dans le processus courant, afin qu'ils puissent être
|
l'utilisateur et l'adresse IP, et les stocke dans le processus courant, afin qu'ils puissent être
|
||||||
récupérés par les signaux.
|
récupérés par les signaux.
|
||||||
|
|
||||||
Si jamais la modification ne provient pas d'une requête Web, on suppose qu'elle vient d'une instruction
|
Si jamais la modification ne provient pas d'une requête Web, on suppose qu'elle vient d'une instruction
|
||||||
lancée avec ``manage.py``.
|
lancée avec ``manage.py``.
|
||||||
On récupère alors le nom de l'utilisateur⋅rice dans l'interface de commandes, et si une note est associée à cet alias,
|
On récupère alors le nom de l'utilisateur dans l'interface de commandes, et si une note est associée à cet alias,
|
||||||
alors on considère que c'est le détenteur de la note qui a effectué cette modification, sur l'adresse IP ``127.0.0.1``.
|
alors on considère que c'est le détenteur de la note qui a effectué cette modification, sur l'adresse IP ``127.0.0.1``.
|
||||||
Sinon, le champ est laissé à ``None``.
|
Sinon, le champ est laissé à ``None``.
|
||||||
|
|
||||||
Une entrée de ``Changelog`` contient les informations suivantes :
|
Une entrée de ``Changelog`` contient les informations suivantes :
|
||||||
|
|
||||||
* Utilisateur⋅rice (``ForeignKey`` vers ``User``, nullable)
|
* Utilisateur (``ForeignKey`` vers ``User``, nullable)
|
||||||
* Adresse IP (``GenericIPAddressField``)
|
* Adresse IP (``GenericIPAddressField``)
|
||||||
* Type de modèle enregistré (``ForeignKey`` vers ``Model``)
|
* Type de modèle enregistré (``ForeignKey`` vers ``Model``)
|
||||||
* Identifiant ``pk`` de l'instance enregistrée (``CharField``)
|
* Identifiant ``pk`` de l'instance enregistrée (``CharField``)
|
||||||
|
@ -54,4 +54,4 @@ Graphe
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
.. image:: ../_static/img/graphs/logs.svg
|
.. image:: ../_static/img/graphs/logs.svg
|
||||||
:alt: Logs graph
|
:alt: Logs graphe
|
||||||
|
|
|
@ -1,63 +1,63 @@
|
||||||
Application Member
|
Application Member
|
||||||
==================
|
==================
|
||||||
|
|
||||||
L'application ``member`` s'occcupe de la gestion des utilisateur⋅rices enregistré⋅es.
|
L'application ``member`` s'occcupe de la gestion des utilisateurs enregistrés.
|
||||||
|
|
||||||
Le model d'utilisateur⋅rice ``django.contrib.auth.model.User`` est complété par un ``Profile`` utilisateur⋅rice.
|
Le model d'utilisateur ``django.contrib.auth.model.User`` est complété par un ``Profile`` utilisateur.
|
||||||
|
|
||||||
Toustes les utilisateur⋅rices peuvent être membre de ``Club``. Cela se traduit par une adhésion ``Membership``, dont les
|
Tous les utilisateurs peuvent être membre de ``Club``. Cela se traduit par une adhésion ``Membership``, dont les
|
||||||
caractéristiques sont propres à chaque club.
|
caractéristiques sont propres à chaque club.
|
||||||
|
|
||||||
En pratique, la Note Kfet possède au minimum deux clubs : **Bde** et **Kfet** (instanciés
|
En pratique, la NoteKfet possède au minimum deux Club: **Bde** et **Kfet** (instanciés via les fixtures). Et tous
|
||||||
via les fixtures). Et toutes les personnes à jour de cotisation sont membre à minima de
|
les personnes à jour de cotisation sont membre à minima de Bde.
|
||||||
BDE. Être adhérent⋅e du club Kfet permet d'utiliser sa note pour consommer.
|
Être adhérent du club Kfet permet d'utiliser sa note pour consommer.
|
||||||
|
|
||||||
Modèles
|
Modèles
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Utilisateur⋅rice
|
Utilisateur
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|
||||||
Le modèle ``User`` est directement implémenté dans Django et n'appartient pas à l'application ``member``, mais il est
|
Le modèle ``User`` est directement implémenté dans Django et n'appartient pas à l'application ``member``, mais il est
|
||||||
bon de rappeler à quoi ressemble ce modèle.
|
bon de rappeler à quoi ressemble ce modèle.
|
||||||
|
|
||||||
* ``date_joined`` : ``DateTimeField``, date à laquelle l'utilisateur⋅rice a été inscrit⋅e (*inutilisé dans la Note*)
|
* ``date_joined`` : ``DateTimeField``, date à laquelle l'utilisateur a été inscrit (*inutilisé dans la Note*)
|
||||||
* ``email`` : ``EmailField``, adresse e-mail de l'utilisateur⋅rice.
|
* ``email`` : ``EmailField``, adresse e-mail de l'utilisateur.
|
||||||
* ``first_name`` : ``CharField``, prénom de l'utilisateur⋅rice.
|
* ``first_name`` : ``CharField``, prénom de l'utilisateur.
|
||||||
* ``is_active`` : ``BooleanField``, indique si le compte est actif et peut se connecter.
|
* ``is_active`` : ``BooleanField``, indique si le compte est actif et peut se connecter.
|
||||||
* ``is_staff`` : ``BooleanField``, indique si l'utilisateur⋅rice peut se connecter à l'interface Django-admin.
|
* ``is_staff`` : ``BooleanField``, indique si l'utilisateur peut se connecter à l'interface Django-admin.
|
||||||
* ``is_superuser`` : ``BooleanField``, indique si l'utilisateur⋅rice dispose de droits super-utilisateur⋅rices, permettant n'importe quelle action en base de donnée (lecture, ajout, modification, suppression).
|
* ``is_superuser`` : ``BooleanField``, indique si l'utilisateur dispose de droits super-utilisateurs, permettant n'importe quelle action en base de donnée (lecture, ajout, modification, suppression).
|
||||||
* ``last_login`` : ``DateTimeField``, date et heure de dernière connexion.
|
* ``last_login`` : ``DateTimeField``, date et heure de dernière connexion.
|
||||||
* ``last_name`` : ``CharField``, nom de famille de l'utilisateur⋅rice.
|
* ``last_name`` : ``CharField``, nom de famille de l'utilisateur.
|
||||||
* ``password`` : ``CharField``, contient le hash du mot de passe de l'utilisateur⋅rice. L'algorithme utilisé est celui par défaut de Django : PBKDF2 + HMAC + SHA256 avec 150000 itérations.
|
* ``password`` : ``CharField``, contient le hash du mot de passe de l'utilisateur. L'algorithme utilisé est celui par défaut de Django : PBKDF2 + HMAC + SHA256 avec 150000 itérations.
|
||||||
* ``username`` : ``CharField`` (unique), pseudo de l'utilisateur⋅rice.
|
* ``username`` : ``CharField`` (unique), pseudo de l'utilisateur.
|
||||||
|
|
||||||
Profil
|
Profil
|
||||||
~~~~~~
|
~~~~~~
|
||||||
|
|
||||||
Le modèle ``Profile`` contient un champ ``user`` de type ``OneToOneField``, ce qui permet de voir ce modèle comme une
|
Le modèle ``Profile`` contient un champ ``user`` de type ``OneToOneField``, ce qui permet de voir ce modèle comme une
|
||||||
extension du modèle ``User``, sans avoir à le réécrire. Il contient diverses informations personnelles sur
|
extension du modèle ``User``, sans avoir à le réécrire. Il contient diverses informations personnelles sur
|
||||||
l'utilisateur⋅rice, utiles pour l'adhésion au BDE :
|
l'utilisateur, utiles pour l'adhésion au BDE :
|
||||||
|
|
||||||
* ``user`` : ``OneToOneField(User)``, utilisateur⋅rice lié à ce profil
|
* ``user`` : ``OneToOneField(User)``, utilisateur lié à ce profil
|
||||||
* ``address`` : ``CharField``, adresse physique de l'utilisateur⋅rice
|
* ``address`` : ``CharField``, adresse physique de l'utilisateur
|
||||||
* ``paid`` : ``BooleanField``, indique si l'utilisateur⋅rice normalien⋅ne est rémunéré⋅e ou non (utile pour différencier les montants d'adhésion aux clubs)
|
* ``paid`` : ``BooleanField``, indique si l'utilisateur normalien est rémunéré ou non (utile pour différencier les montants d'adhésion aux clubs)
|
||||||
* ``phone_number`` : ``CharField``, numéro de téléphone de l'utilisateur⋅rice
|
* ``phone_number`` : ``CharField``, numéro de téléphone de l'utilisateur
|
||||||
* ``section`` : ``CharField``, section de l'ENS à laquelle appartient l'utilisateur⋅rice (exemple : 1A0,…)
|
* ``section`` : ``CharField``, section de l'ENS à laquelle apartient l'utilisateur (exemple : 1A0, ...)
|
||||||
|
|
||||||
Clubs
|
Clubs
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
La gestion des clubs est une différence majeure avec la Note Kfet 2015. La Note gère ainsi les adhésions des
|
La gestion des clubs est une différence majeure avec la Note Kfet 2015. La Note gère ainsi les adhésions des
|
||||||
utilisateur⋅rices aux différents clubs.
|
utilisateurs aux différents clubs.
|
||||||
|
|
||||||
* ``parent_club`` : ``ForeignKey(Club)``. La présence d'un club parent force l'adhésion au club parent avant de pouvoir adhérer au dit club. Tout club qui n'est pas le club BDE doit avoir le club BDE dans son arborescence.
|
* ``parent_club`` : ``ForeignKey(Club)``. La présence d'un club parent force l'adhésion au club parent avant de pouvoir adhérer au dit club. Tout club qui n'est pas le club BDE doit avoir le club BDE dans son arborescence.
|
||||||
* ``email`` : ``EmailField``, adresse e-mail sur laquelle contacter le bureau du club.
|
* ``email`` : ``EmailField``, adresse e-mail sur laquelle contacter le bureau du club.
|
||||||
* ``membership_start`` : ``DateField``, date à partir de laquelle il est possible d'adhérer à un club pour l'année suivante (si adhésions à l'année), en ignorant l'année. Par exemple, l'adhésion BDE est possible à partir du 01/08 par défaut, et c'est à cette date que les adhésions pour l'année future est possible.
|
* ``membership_start`` : ``DateField``, date à partir de laquelle il est possible d'adhérer à un club pour l'année suivante (si adhésions à l'année), en ignorant l'année. Par exemple, l'adhésion BDE est possible à partir du 31/08 par défaut, et c'est à cette date que les adhésions pour l'année future est possible.
|
||||||
* ``membership_end`` : ``DateField``, date maximale de fin d'adhésion. Pour le club BDE, il s'agit du 30/09 de l'année suivante. Si cette valeur vaut ``null``, la fin d'adhésion n'est pas limitée.
|
* ``membership_end`` : ``DateField``, date maximale de fin d'adhésion. Pour le club BDE, il s'agit du 30/09 de l'année suivante. Si cette valeur vaut ``null``, la fin d'adhésion n'est pas limitée.
|
||||||
* ``membership_duration`` : ``PositiveIntegerField``, durée (en jours) maximale d'adhésion. Par exemple, le club BDE permet des adhésions maximales de 13 mois, soit 396 jours.
|
* ``membership_duration`` : ``PositiveIntegerField``, durée (en jours) maximale d'adhésion. Par exemple, le club BDE permet des adhésions maximales de 13 mois, soit 396 jours.
|
||||||
* ``membership_fee_paid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un⋅e élève normalien⋅ne (donc rémunéré⋅e) puisse adhérer.
|
* ``membership_fee_paid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un élève normalien (donc rémunéré) puisse adhérer.
|
||||||
* ``membership_fee_unpaid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un⋅e étudiant⋅e normalien⋅ne (donc non rémunéré) puisse adhérer.
|
* ``membership_fee_unpaid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un étudiant normalien (donc non rémunéré) puisse adhérer.
|
||||||
* ``name`` : ``CharField``, nom du club.
|
* ``name`` : ``CharField``, nom du club.
|
||||||
* ``require_memberships`` : ``BooleanField``, indique si le club est un vrai club BDE qui nécessite des adhésions de club, ou s'il s'agit d'une note "pot commun" (organisation d'une activité, note de département, ...)
|
* ``require_memberships`` : ``BooleanField``, indique si le club est un vrai club BDE qui nécessite des adhésions de club, ou s'il s'agit d'une note "pot commun" (organisation d'une activité, note de département, ...)
|
||||||
|
|
||||||
|
@ -67,16 +67,16 @@ Adhésions
|
||||||
Comme indiqué précédemment, la note gère les adhésions.
|
Comme indiqué précédemment, la note gère les adhésions.
|
||||||
|
|
||||||
* ``club`` : ``ForeignKey(Club)``, club lié à l'adhésion.
|
* ``club`` : ``ForeignKey(Club)``, club lié à l'adhésion.
|
||||||
* ``user`` : ``ForeignKey(User)``, utilisateur⋅rice qui a adhéré.
|
* ``user`` : ``ForeignKey(User)``, utilisateur adhéré.
|
||||||
* ``date_start`` : ``DateField``, date de début d'adhésion.
|
* ``date_start`` : ``DateField``, date de début d'adhésion.
|
||||||
* ``date_end`` : ``DateField``, date de fin d'adhésion.
|
* ``date_end`` : ``DateField``, date de fin d'adhésion.
|
||||||
* ``fee`` : ``PositiveIntegerField``, montant de la cotisation payée.
|
* ``fee`` : ``PositiveIntegerField``, montant de la cotisation payée.
|
||||||
* ``roles`` : ``ManyToManyField(Role)``, liste des rôles endossés par l'adhérent⋅e.
|
* ``roles`` : ``ManyToManyField(Role)``, liste des rôles endossés par l'adhérent.
|
||||||
|
|
||||||
Rôles
|
Rôles
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
Comme indiqué le modèle des adhésions, les adhésions octroient des rôles aux adhérent⋅es, qui offrent des permissions
|
Comme indiqué le modèle des adhésions, les adhésions octroient des rôles aux adhérents, qui offrent des permissions
|
||||||
(cf ``RolesPermissions`` dans la page des permissions). Le modèle ``RolesPermissions`` possède un
|
(cf ``RolesPermissions`` dans la page des permissions). Le modèle ``RolesPermissions`` possède un
|
||||||
``OneToOneField(Role)``, qui implémente les permissions des rôles. Le modèle ``Role`` à proprement parler ne contient
|
``OneToOneField(Role)``, qui implémente les permissions des rôles. Le modèle ``Role`` à proprement parler ne contient
|
||||||
que le champ de son nom (``CharField``).
|
que le champ de son nom (``CharField``).
|
||||||
|
@ -88,7 +88,7 @@ Si le modèle ``MembershipTransaction`` appartient à l'application ``note``, il
|
||||||
Le modèle ``MembershipTransaction`` est une extension du modèle ``Transaction`` (application ``note``) qui est de type
|
Le modèle ``MembershipTransaction`` est une extension du modèle ``Transaction`` (application ``note``) qui est de type
|
||||||
polymorphique, et contient en plus des informations de base de la transaction un champ ``OneToOneField(Membership)``
|
polymorphique, et contient en plus des informations de base de la transaction un champ ``OneToOneField(Membership)``
|
||||||
faisant le lien entre l'adhésion et la transaction liée. Une adhésion club, si elle n'est pas gratuite,
|
faisant le lien entre l'adhésion et la transaction liée. Une adhésion club, si elle n'est pas gratuite,
|
||||||
génère en effet automatiquement une transaction de l'utilisateur⋅rice vers le club (voir section adhésions).
|
génère en effet automatiquement une transaction de l'utilisateur vers le club (voir section adhésions).
|
||||||
|
|
||||||
Graphe
|
Graphe
|
||||||
------
|
------
|
||||||
|
@ -100,28 +100,28 @@ Adhésions
|
||||||
---------
|
---------
|
||||||
|
|
||||||
La Note Kfet offre la possibilité aux clubs de gérer l'adhésion de leurs membres. En plus de réguler les cotisations
|
La Note Kfet offre la possibilité aux clubs de gérer l'adhésion de leurs membres. En plus de réguler les cotisations
|
||||||
des adhérent⋅es, des permissions sont octroyées sur la note en fonction des rôles au sein des clubs. Un rôle est une
|
des adhérents, des permissions sont octroyées sur la note en fonction des rôles au sein des clubs. Un rôle est une
|
||||||
fonction occupée au sein d'un club (Trésorièr⋅e de club, président⋅e de club, GC Kfet, Res[pot], respo info,…).
|
fonction occupée au sein d'un club (Trésorier de club, président de club, GCKfet, Res[pot], respo info, ...).
|
||||||
Une adhésion attribue à un⋅e adhérent⋅e ses rôles. Les rôles fournissent les permissions. Par exemple, læ trésorièr⋅e d'un
|
Une adhésion attribue à un adhérent ses rôles. Les rôles fournissent les permissions. Par exemple, le trésorier d'un
|
||||||
club a le droit de faire des transferts de et vers la note du club, tant que la source reste au-dessus de -50 €.
|
club a le droit de faire des transferts de et vers la note du club, tant que la source reste au-dessus de -50 €.
|
||||||
Une adhésion est considérée comme valide si la date du jour est comprise (au sens large) entre les dates de début et
|
Une adhésion est considérée comme valide si la date du jour est comprise (au sens large) entre les dates de début et
|
||||||
de fin d'adhésion.
|
de fin d'adhésion.
|
||||||
|
|
||||||
On peut ajouter une adhésion à un⋅e utilisateur⋅rice dans un club à tout⋅e non adhérent⋅e de ce club. La personne en charge
|
On peut ajouter une adhésion à un utilisateur dans un club à tout non adhérent de ce club. La personne en charge
|
||||||
d'adhérer quelqu'un choisit l'utilisateur⋅rice, les rôles au sein du club et la date de début d'adhésion. Cette date de
|
d'adhérer quelqu'un choisit l'utilisateur, les rôles au sein du club et la date de début d'adhésion. Cette date de
|
||||||
début d'adhésion doit se situer entre les champs ``club.membership_start`` et ``club.membership_end``,
|
début d'adhésion doit se situer entre les champs ``club.membership_start`` et ``club.membership_end``,
|
||||||
si ces champs sont non nuls. Si ``club.parent_club`` n'est pas nul, l'utilisateur⋅rice doit être membre de ce club.
|
si ces champs sont non nuls. Si ``club.parent_club`` n'est pas nul, l'utilisateur doit être membre de ce club.
|
||||||
Le montant de la cotisation est fixé en fonction du statut normalien de l'utilisateur⋅rice (``club.membership_fee_paid``
|
Le montant de la cotisation est fixé en fonction du statut normalien de l'utilisateur (``club.membership_fee_paid``
|
||||||
centimes pour les élèves et ``club.membership_fee_unpaid`` centimes pour les étudiant⋅es). La date de fin est calculée
|
centimes pour les élèves et ``club.membership_fee_unpaid`` centimes pour les étudiants). La date de fin est calculée
|
||||||
comme ce qui suit :
|
comme ce qui suit :
|
||||||
|
|
||||||
* Si ``club.membership_duration`` est non nul, alors ``date_end`` = ``date_start`` + ``club.membership_duration``
|
* Si ``club.membership_duration`` est non nul, alors ``date_end`` = ``date_start`` + ``club.membership_duration``
|
||||||
* Sinon ``club``, ``date_end`` = ``date_start`` + 424242 jours (suffisant pour tenir au moins une vie)
|
* Sinon ``club``, ``date_end`` = ``date_start`` + 424242 jours (suffisant pour tenir au moins une vie)
|
||||||
* Si ``club.membership_end`` est non nul, alors ``date_end`` = min(``date_end``, ``club.membership_end``)
|
* Si ``club.membership_end`` est non nul, alors ``date_end`` = min(``date_end``, ``club.membership_end``)
|
||||||
|
|
||||||
Si l'utilisateur⋅rice n'est pas membre du club ``Kfet``, l'adhésion n'est pas possible si le solde disponible sur sa note est
|
Si l'utilisateur n'est pas membre du club ``Kfet``, l'adhésion n'est pas possible si le solde disponible sur sa note est
|
||||||
insuffisant. Une fois toute ces contraintes vérifiées, l'adhésion est créée. Une transaction de type
|
insuffisant. Une fois toute ces contraintes vérifiées, l'adhésion est créée. Une transaction de type
|
||||||
``MembershipTransaction`` est automatiquement créée de la note de l'utilisateur⋅rice vers la note du club, finalisant l'adhésion.
|
``MembershipTransaction`` est automatiquement créée de la note de l'utilisateur vers la note du club, finalisant l'adhésion.
|
||||||
|
|
||||||
Réadhésions
|
Réadhésions
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
@ -137,7 +137,7 @@ Il est possible de réadhérer si :
|
||||||
* Il n'y a pas encore de réadhésion (pas d'adhésion au même club vérifiant ``new_membership.date_start`` >= ``club.membership_start``)
|
* Il n'y a pas encore de réadhésion (pas d'adhésion au même club vérifiant ``new_membership.date_start`` >= ``club.membership_start``)
|
||||||
|
|
||||||
Un bouton ``Réadhérer`` apparaît dans la liste des adhésions si le droit est permis et si ces contraintes sont vérifiées.
|
Un bouton ``Réadhérer`` apparaît dans la liste des adhésions si le droit est permis et si ces contraintes sont vérifiées.
|
||||||
En réadhérant, une nouvelle adhésion est créée pour l'utilisateur⋅rice avec les mêmes rôles, commençant le lendemain de la
|
En réadhérant, une nouvelle adhésion est créée pour l'utilisateur avec les mêmes rôles, commençant le lendemain de la
|
||||||
date d'expiration de la précédente adhésion. Si on réadhère le 16 août pour une adhésion finissant le 30 septembre,
|
date d'expiration de la précédente adhésion. Si on réadhère le 16 août pour une adhésion finissant le 30 septembre,
|
||||||
la nouvelle adhésion commencera le 1er octobre).
|
la nouvelle adhésion commencera le 1er octobre).
|
||||||
|
|
||||||
|
|
|
@ -7,23 +7,23 @@ Affichage
|
||||||
La page de consommations est principalement une communication entre l'`API <../api>`_ et la page en JavaScript.
|
La page de consommations est principalement une communication entre l'`API <../api>`_ et la page en JavaScript.
|
||||||
Elle est disponible à l'adresse ``/note/consos/``, et l'onglet n'est visible que pour ceux ayant le droit de voir au
|
Elle est disponible à l'adresse ``/note/consos/``, et l'onglet n'est visible que pour ceux ayant le droit de voir au
|
||||||
moins un bouton. L'affichage, comme tout le reste de la page, est géré avec Boostrap 4.
|
moins un bouton. L'affichage, comme tout le reste de la page, est géré avec Boostrap 4.
|
||||||
Les boutons que l'utilisateur⋅rice a le droit de voir sont triés par catégorie.
|
Les boutons que l'utilisateur a le droit de voir sont triés par catégorie.
|
||||||
|
|
||||||
Sélection des consommations
|
Sélection des consommations
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
Lorsque l'utilisateur⋅rice commence à taper un nom de note, un appel à l'API sur la page ``/api/note/alias`` est fait,
|
Lorsque l'utilisateur commence à taper un nom de note, un appel à l'API sur la page ``/api/note/alias`` est fait,
|
||||||
récupérant les 20 premiers aliases en accord avec la requête. Quand l'utilisateur⋅rice survole un alias, un appel à la page
|
récupérant les 20 premiers aliases en accord avec la requête. Quand l'utilisateur survole un alias, un appel à la page
|
||||||
``/api/note/note/<NOTE_ID>/`` est fait pour récupérer plus d'infos sur la note telles que le solde, le vrai nom de la
|
``/api/note/note/<NOTE_ID>/`` est fait pour récupérer plus d'infos sur la note telles que le solde, le vrai nom de la
|
||||||
note et la photo, si toutefois l'utilisateur⋅rice a le droit de voir ceci.
|
note et la photo, si toutefois l'utilisateur a le droit de voir ceci.
|
||||||
|
|
||||||
L'utilisateur⋅rice peut cliquer sur des aliases pour ajouter des émetteur⋅rices, et sur des boutons pour ajouter des consommations.
|
L'utilisateur peut cliquer sur des aliases pour ajouter des émetteurs, et sur des boutons pour ajouter des consommations.
|
||||||
Cliquer dans la liste des émetteur⋅rices supprime l'élément sélectionné.
|
Cliquer dans la liste des émetteurs supprime l'élément sélectionné.
|
||||||
|
|
||||||
Il y a deux possibilités pour faire consommer des adhérent⋅es :
|
Il ya deux possibilités pour faire consommer des adhérents :
|
||||||
- En mode **consommation simple** (mode par défaut), les consommations sont débitées dès que émetteur⋅rices et consommations
|
- En mode **consommation simple** (mode par défaut), les consommations sont débitées dès que émetteurs et consommations
|
||||||
sont renseignées.
|
sont renseignées.
|
||||||
- En mode **consommation double**, l'utilisateur⋅rice doit cliquer sur « **Consommer !** »" pour débiter toutes les consommations.
|
- En mode **consommation double**, l'utilisateur doit cliquer sur "Consommer !" pour débiter toutes les consommations.
|
||||||
|
|
||||||
Débit des consommations
|
Débit des consommations
|
||||||
-----------------------
|
-----------------------
|
||||||
|
@ -71,7 +71,7 @@ des types. Il vaut `42` lors de la rédaction de cette documentation, mais pourr
|
||||||
Si une erreur survient lors de la requête (droits insuffisants), un message apparaîtra en haut de page.
|
Si une erreur survient lors de la requête (droits insuffisants), un message apparaîtra en haut de page.
|
||||||
Dans tous les cas, tous les champs sont réinitialisés.
|
Dans tous les cas, tous les champs sont réinitialisés.
|
||||||
|
|
||||||
L'historique et le solde de l'utilisateur⋅rice sont ensuite mis à jour via jQuery, qui permet de recharger une partie de page Web.
|
L'historique et la balance de l'utilisateur sont ensuite mis à jour via jQuery, qui permet de recharger une partie de page Web.
|
||||||
|
|
||||||
Validation/dévalidation des transactions
|
Validation/dévalidation des transactions
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -86,4 +86,4 @@ une requête PATCH est faite à l'API sur l'adresse ``/api/note/transaction/tran
|
||||||
"valid": false
|
"valid": false
|
||||||
}
|
}
|
||||||
|
|
||||||
L'historique et le solde sont ensuite rafraîchis. Si une erreur survient, un message apparaîtra.
|
L'historique et la balance sont ensuite rafraîchis. Si une erreur survient, un message apparaîtra.
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
Application Note
|
Application Note
|
||||||
================
|
================
|
||||||
|
|
||||||
L'application ``note`` gère tout ce qui est en lien avec les flux d'argent et les notes (soldes) des utilisateur⋅rices.
|
L'application ``note`` gère tout ce qui est en lien avec les flux d'argent et les notes (balances) des utilisateurs.
|
||||||
|
|
||||||
La gestion des consommations s'effectue principalement via la page dédiée, dont le fonctionnement est expliqué
|
La gestion des consommations s'effectue principalement via la page dédiée, dont le fonctionnement est expliqué
|
||||||
dans la page `Consommations <consumptions>`_.
|
dans la page `Consommations <consumptions>`_.
|
||||||
|
|
||||||
Le fonctionnement des crédit/débit de note (avec le « monde extérieur » donc avec de l'argent réel) ainsi que les
|
Le fonctionnnemnent des crédit/débit de note (avec le "monde extérieur" donc avec de l'argent réel) ainsi que les
|
||||||
transferts/dons entre notes est détaillé sur la page `Transferts <transactions>`_.
|
transferts/dons entre notes est détaillé sur la page `Transferts <transactions>`_.
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
|
@ -6,7 +6,7 @@ Affichage
|
||||||
|
|
||||||
L'interface de la page de transferts est semblable à celles des consommations, et l'auto-complétion de note est géré de
|
L'interface de la page de transferts est semblable à celles des consommations, et l'auto-complétion de note est géré de
|
||||||
la même manière. La page se trouve à l'adresse ``/note/transfer/``. La liste des 20 transactions les plus récentes que
|
la même manière. La page se trouve à l'adresse ``/note/transfer/``. La liste des 20 transactions les plus récentes que
|
||||||
l'utilisateur⋅rice a le droit de voir est également présente.
|
l'utilisateur a le droit de voir est également présente.
|
||||||
|
|
||||||
Des boutons ``Don``, ``Transfert``, ``Crédit``, ``Retrait`` sont présents, représentant les différents modes de
|
Des boutons ``Don``, ``Transfert``, ``Crédit``, ``Retrait`` sont présents, représentant les différents modes de
|
||||||
transfert. Pour chaque transfert, un montant et une description sont attendus.
|
transfert. Pour chaque transfert, un montant et une description sont attendus.
|
||||||
|
@ -23,7 +23,7 @@ Onglets Crédit et retrait
|
||||||
Ces onglets ne sont visibles que par ceux qui ont le droit de voir les ``SpecialNote``.
|
Ces onglets ne sont visibles que par ceux qui ont le droit de voir les ``SpecialNote``.
|
||||||
|
|
||||||
Une boîte supplémentaire apparaît, demandant en plus de la note, du montant et de la raison le nom, le prénom et
|
Une boîte supplémentaire apparaît, demandant en plus de la note, du montant et de la raison le nom, le prénom et
|
||||||
la banque de la personne à recharger/retirer. Lorsqu'une note est sélectionnée, les champs « nom » et « prénom » sont
|
la banque de la personne à recharger/retirer. Lorsqu'une note est sélectionnée, les champs "nom" et "prénom" sont
|
||||||
remplis automatiquement. Par ailleurs, seule une note peut être choisie.
|
remplis automatiquement. Par ailleurs, seule une note peut être choisie.
|
||||||
|
|
||||||
Transfert
|
Transfert
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
Droits
|
Droits
|
||||||
======
|
======
|
||||||
|
|
||||||
Le système de droit par défaut de Django n'est pas suffisamment granulaire pour les besoins de la Note Kfet 2020.
|
Le système de droit par défault de django n'est pas suffisament granulaire pour les besoins de la NoteKfet2020.
|
||||||
Un système personnalisé a donc été développé.
|
Un système custom a donc été développé.
|
||||||
|
|
||||||
Il permet la création de Permission, qui autorise ou non a faire une action précise sur un ou des objets
|
Il permet la création de Permission, qui autorise ou non a faire une action précise sur un ou des objets
|
||||||
de la base de données.
|
de la base de données.
|
||||||
|
@ -22,12 +22,12 @@ Une permission est un Model Django dont les principaux attributs sont :
|
||||||
* ``query`` : Requête sur la cible, encodé en JSON, traduit en un Q object (cf `Query <#compilation-de-la-query>`_)
|
* ``query`` : Requête sur la cible, encodé en JSON, traduit en un Q object (cf `Query <#compilation-de-la-query>`_)
|
||||||
* ``field`` : le champ cible qui pourra être modifié. (tous les champs si vide)
|
* ``field`` : le champ cible qui pourra être modifié. (tous les champs si vide)
|
||||||
|
|
||||||
Pour savoir si un⋅e utilisateur⋅rice a le droit sur un modèle ou non, la requête est compilée (voir ci-dessous) en un filtre
|
Pour savoir si un utilisateur a le droit sur un modèle ou non, la requête est compilée (voir ci-dessous) en un filtre
|
||||||
de requête dans la base de données, un objet de la classe ``Q`` (En SQL l'objet Q s'interprète comme tout ce qui suit
|
de requête dans la base de données, un objet de la classe ``Q`` (En SQL l'objet Q s'interprete comme tout ce qui suit
|
||||||
un ``WHERE ...`` Ils peuvent être combiné à l'aide d'opérateurs logiques. Plus d'information sur les Q object dans la
|
un ``WHERE ...`` Ils peuvent être combiné à l'aide d'opérateurs logiques. Plus d'information sur les Q object dans la
|
||||||
`documentation officielle <https://docs.djangoproject.com/fr/2.2/topics/db/queries/#complex-lookups-with-q-objects>`_.
|
`documentation officielle <https://docs.djangoproject.com/fr/2.2/topics/db/queries/#complex-lookups-with-q-objects>`_.
|
||||||
|
|
||||||
Ce Q object sera donc utilisé pour savoir si l'instance que l'on veut modifier est concernée par notre permission.
|
Ce Q object sera donc utilisé pour savoir si l'instance que l'on veux modifier est concernée par notre permission.
|
||||||
|
|
||||||
Exception faite sur l'ajout d'objets : l'objet n'existant pas encore en base de données, il est ajouté puis supprimé
|
Exception faite sur l'ajout d'objets : l'objet n'existant pas encore en base de données, il est ajouté puis supprimé
|
||||||
à la volée, en prenant soin de désactiver les signaux.
|
à la volée, en prenant soin de désactiver les signaux.
|
||||||
|
@ -36,7 +36,7 @@ Compilation de la query
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
La query est enregistrée sous un format JSON, puis est traduite en requête ``Q`` récursivement en appliquant certains paramètres.
|
La query est enregistrée sous un format JSON, puis est traduite en requête ``Q`` récursivement en appliquant certains paramètres.
|
||||||
Le fonctionnemente de base des permission peux être décris avec les différents opérations :
|
Le fonctionnemente de base des permission peux être décris avec les differents opérations :
|
||||||
|
|
||||||
+----------------+-----------------------------+-------------------------------------+
|
+----------------+-----------------------------+-------------------------------------+
|
||||||
| opérations | JSON | Q object |
|
| opérations | JSON | Q object |
|
||||||
|
@ -64,7 +64,7 @@ Exemples
|
||||||
|
|
||||||
{"is_superuser": true}
|
{"is_superuser": true}
|
||||||
|
|
||||||
| si l'utilisateur⋅rice cible est un⋅e super utilisateur⋅rice.
|
| si l'utilisateur cible est un super utilisateur.
|
||||||
|
|
||||||
* sur le model ``Note`` :
|
* sur le model ``Note`` :
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ Exemples
|
||||||
["user","note", "pk"]
|
["user","note", "pk"]
|
||||||
}
|
}
|
||||||
|
|
||||||
| si l'identifiant de la note cible est l'identifiant de l'utilisateur⋅rice dont on regarde la permission.
|
| si l'identifiant de la note cible est l'identifiant de l'utilisateur dont on regarde la permission.
|
||||||
|
|
||||||
* sur le model ``Transaction``:
|
* sur le model ``Transaction``:
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ Exemples
|
||||||
["user", "note", "balance"]}
|
["user", "note", "balance"]}
|
||||||
]
|
]
|
||||||
|
|
||||||
| si la source est la note de l'utilisateur⋅rice et si le montant est inférieur à son solde.
|
| si la source est la note de l'utilisateur et si le montant est inférieur à son solde.
|
||||||
|
|
||||||
* Sur le model ``Alias``
|
* Sur le model ``Alias``
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ Exemples
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
| si l'alias appartient à une note de club ou s'il appartient à la note d'un⋅e utilisateur⋅rice membre du club Kfet.
|
| si l'alias appartient à une note de club ou s'il appartient à la note d'un utilisateur membre du club Kfet.
|
||||||
|
|
||||||
* sur le model ``Transaction``
|
* sur le model ``Transaction``
|
||||||
|
|
||||||
|
@ -130,19 +130,19 @@ Exemples
|
||||||
Masques de permissions
|
Masques de permissions
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
Chaque permission est associée à un masque. À la connexion, l'utilisateur⋅rice choisit le masque de droits avec lequel iel
|
Chaque permission est associée à un masque. À la connexion, l'utilisateur choisit le masque de droits avec lequel il
|
||||||
souhaite se connecter. Les masques sont ordonnés totalement, et l'utilisateur⋅rice aura effectivement une permission si iel est
|
souhaite se connecter. Les masques sont ordonnés totalement, et l'utilisateur aura effectivement une permission s'il est
|
||||||
en droit d'avoir la permission et si son masque est suffisamment haut.
|
en droit d'avoir la permission et si son masque est suffisamment haut.
|
||||||
|
|
||||||
Par exemple, si la permission de voir toutes les transactions est associée au masque « Droits note uniquement »,
|
Par exemple, si la permission de voir toutes les transactions est associée au masque "Droits note uniquement",
|
||||||
se connecter avec le masque « Droits basiques » n'octroiera pas cette permission tandis que le masque « Tous mes droits » oui.
|
se connecter avec le masque "Droits basiques" n'octroiera pas cette permission tandis que le masque "Tous mes droits" oui.
|
||||||
|
|
||||||
Signaux
|
Signaux
|
||||||
-------
|
-------
|
||||||
|
|
||||||
À chaque fois qu'un modèle est modifié, ajouté ou supprimé, les droits sont contrôlés. Si les droits ne sont pas
|
À chaque fois qu'un modèle est modifié, ajouté ou supprimé, les droits sont contrôlés. Si les droits ne sont pas
|
||||||
suffisants, une erreur est lancée. Pour ce qui est de la modification, on ne contrôle que les champs réellement
|
suffisants, une erreur est lancée. Pour ce qui est de la modification, on ne contrôle que les champs réellement
|
||||||
modifiés en comparant l'ancienne et la nouvelle instance.
|
modifiés en comparant l'ancienne et la nouvele instance.
|
||||||
|
|
||||||
Graphe des modèles
|
Graphe des modèles
|
||||||
------------------
|
------------------
|
||||||
|
|
|
@ -4,7 +4,7 @@ Inscriptions
|
||||||
L'inscription a la note se fait via une application dédiée, sans toutefois avoir de modèle en base de données.
|
L'inscription a la note se fait via une application dédiée, sans toutefois avoir de modèle en base de données.
|
||||||
|
|
||||||
Un formulaire d'inscription est disponible sur la page ``/registration/signup``, accessible depuis n'importe qui,
|
Un formulaire d'inscription est disponible sur la page ``/registration/signup``, accessible depuis n'importe qui,
|
||||||
authentifié⋅e ou non. Les informations suivantes sont demandées :
|
authentifié ou non. Les informations suivantes sont demandées :
|
||||||
|
|
||||||
* Prénom
|
* Prénom
|
||||||
* Nom de famille
|
* Nom de famille
|
||||||
|
@ -15,7 +15,7 @@ authentifié⋅e ou non. Les informations suivantes sont demandées :
|
||||||
* Département d'études
|
* Département d'études
|
||||||
* Promotion, année d'entrée à l'ENS
|
* Promotion, année d'entrée à l'ENS
|
||||||
* Adresse (optionnel)
|
* Adresse (optionnel)
|
||||||
* Payé⋅e (si la personne perçoit un salaire)
|
* Payé (si la personne perçoit un salaire)
|
||||||
|
|
||||||
Le mot de passe doit vérifier des contraintes de longueur, de complexité et d'éloignement des autres informations
|
Le mot de passe doit vérifier des contraintes de longueur, de complexité et d'éloignement des autres informations
|
||||||
personnelles.
|
personnelles.
|
||||||
|
@ -34,28 +34,28 @@ le compte sera enfin actif.
|
||||||
Pour récapituler : compte actif = adresse e-mail validée + inscription validée par le BDE.
|
Pour récapituler : compte actif = adresse e-mail validée + inscription validée par le BDE.
|
||||||
|
|
||||||
Lors de la validation de l'inscription, le BDE peut (et doit même) faire un crédit initial sur la future note de
|
Lors de la validation de l'inscription, le BDE peut (et doit même) faire un crédit initial sur la future note de
|
||||||
l'utilisateur⋅rice. Il peut spécifier le type de crédit (carte bancaire/espèces/chèque/virement bancaire), le prénom,
|
l'utilisateur. Il peut spécifier le type de crédit (carte bancaire/espèces/chèque/virement bancaire), le prénom,
|
||||||
le nom et la banque comme un crédit normal. Cependant, il peut aussi cocher une case "Société générale", si le nouveau
|
le nom et la banque comme un crédit normal. Cependant, il peut aussi cocher une case "Société générale", si le nouveau
|
||||||
membre indique avoir ouvert un compte à la Société générale via le partenariat Société générale - BDE de
|
membre indique avoir ouvert un compte à la Société générale via le partenariat Société générale - BDE de
|
||||||
l'ÉNS Paris-Saclay. Dans ce cas, tous les champs sont grisés.
|
l'ÉNS Paris-Saclay. Dans ce cas, tous les champs sont grisés.
|
||||||
|
|
||||||
Une fois l'inscription validée, détail de ce qu'il se passe :
|
Une fois l'inscription validée, détail de ce qu'il se passe :
|
||||||
|
|
||||||
* Si crédit de la société générale, on mémorise que le fait que la personne ait demandé ce crédit (voir
|
* Si crédit de la socitété générale, on mémorise que le fait que la personne ait demandé ce crédit (voir
|
||||||
`Trésorerie <treasury>`_ section crédits de la société générale). Nécessairement, le club Kfet doit être rejoint.
|
`Trésorerie <treasury>`_ section crédits de la société générale). Nécessairement, le club Kfet doit être rejoint.
|
||||||
* Sinon, on crédite la note du montant demandé par læ nouvelleau membre (avec comme description "Crédit TYPE (Inscription)"
|
* Sinon, on crédite la note du montant demandé par le nouveau membre (avec comme description "Crédit TYPE (Inscription)"
|
||||||
où TYPE est le type de crédit), après avoir vérifié que le crédit est suffisant (on n'ouvre pas une note négative)
|
où TYPE est le type de crédit), après avoir vérifié que le crédit est suffisant (on n'ouvre pas une note négative)
|
||||||
* On adhère la personne au BDE, l'adhésion commence aujourd'hui. Iel dispose d'un unique rôle : « Adhérent⋅e BDE »,
|
* On adhère la personne au BDE, l'adhésion commence aujourd'hui. Il dispose d'un unique rôle : "Adhérent BDE",
|
||||||
lui octroyant un faible nombre de permissions de base, telles que la visualisation de son compte.
|
lui octroyant un faible nombre de permissions de base, telles que la visualisation de son compte.
|
||||||
* On adhère la personne au club Kfet si cela est demandé, l'adhésion commence aujourd'hui. Iel dispose d'un unique rôle :
|
* On adhère la personne au club Kfet si cela est demandé, l'adhésion commence aujourd'hui. Il dispose d'un unique rôle :
|
||||||
« Adhérent⋅e Kfet » , lui octroyant un nombre un peu plus conséquent de permissions basiques, telles que la possibilité de
|
"Adhérent Kfet", lui octroyant un nombre un peu plus conséquent de permissions basiques, telles que la possibilité de
|
||||||
faire des transactions, d'accéder aux activités, au WEI,…
|
faire des transactions, d'accéder aux activités, au WEI, ...
|
||||||
* Si læ nouvelleau membre a indiqué avoir ouvert un compte à la société générale, alors les transactions sont invalidées,
|
* Si le nouveau membre a indiqué avoir ouvert un compte à la société générale, alors les transactions sont invalidées,
|
||||||
la note n'est pas débitée (commence alors à 0 €).
|
la note n'est pas débitée (commence alors à 0 €).
|
||||||
|
|
||||||
Par ailleurs, le BDE peut supprimer la demande d'inscription sans problème via un bouton dédié. Cette opération
|
Par ailleurs, le BDE peut supprimer la demande d'inscription sans problème via un bouton dédié. Cette opération
|
||||||
n'est pas réversible.
|
n'est pas réversible.
|
||||||
|
|
||||||
L'utilisateur⋅rice a enfin accès a sa note et peut faire des bêtises :)
|
L'utilisateur a enfin accès a sa note et peut faire des bêtises :)
|
||||||
|
|
||||||
L'inscription au BDE et à la Kfet est indépendante de l'inscription au WEI. Voir `WEI <wei>`_ pour l'inscription WEI.
|
L'inscription au BDE et à la Kfet est indépendante de l'inscription au WEI. Voir `WEI <wei>`_ pour l'inscription WEI.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
Application Trésorerie
|
Application Trésorerie
|
||||||
======================
|
======================
|
||||||
|
|
||||||
L'application de Trésorerie facilite la vie des trésorièr⋅es, et sert d'interface de création de facture.
|
L'application de Trésorerie facilite la vie des trésorier, et sert d'interface de création de facture.
|
||||||
Elle permet également le suivi des remises de chèques reçus par le BDE et des crédits de la Société générale.
|
Elle permet également le suivi des remises de chèques reçus par le BDE et des crédits de la Société générale.
|
||||||
|
|
||||||
Factures
|
Factures
|
||||||
|
@ -90,7 +90,7 @@ présent à l'adresse suivante :
|
||||||
|
|
||||||
On le remplit avec les données de la facture et les données du BDE, hard-codées. On copie le template rempli dans un
|
On le remplit avec les données de la facture et les données du BDE, hard-codées. On copie le template rempli dans un
|
||||||
ficher tex dans un dossier temporaire. On fait ensuite 2 appels à ``pdflatex`` pour générer la facture au format PDF.
|
ficher tex dans un dossier temporaire. On fait ensuite 2 appels à ``pdflatex`` pour générer la facture au format PDF.
|
||||||
Les deux appels sont nécessaires, il y a besoin d'un double rendu. Ensuite, le PDF est envoyé à l'utilisateur⋅rice et on
|
Les deux appels sont nécessaires, il y a besoin d'un double rendu. Ensuite, le PDF est envoyé à l'utilisateur et on
|
||||||
supprime les données temporaires.
|
supprime les données temporaires.
|
||||||
|
|
||||||
On remarque que les PDF sont générés à la volée et ne sont pas sauvegardés. Niveau performances, cela prend du temps
|
On remarque que les PDF sont générés à la volée et ne sont pas sauvegardés. Niveau performances, cela prend du temps
|
||||||
|
@ -155,7 +155,7 @@ Relations
|
||||||
~~~~~~~~~
|
~~~~~~~~~
|
||||||
|
|
||||||
* Toute transaction qui n'est pas attachée à une remise d'un bon type peut être attachée à une remise. Cela se passe
|
* Toute transaction qui n'est pas attachée à une remise d'un bon type peut être attachée à une remise. Cela se passe
|
||||||
par le biais d'un formulaire, où læ trésorièr⋅e peut vérifier et corriger au besoin nom, prénom, banque émettrice et montant.
|
par le biais d'un formulaire, où le trésorier peut vérifier et corriger au besoin nom, prénom, banque émettrice et montant.
|
||||||
|
|
||||||
* Toute transaction attachée à une remise encore ouverte peut être retirée.
|
* Toute transaction attachée à une remise encore ouverte peut être retirée.
|
||||||
* Pour clore une remise, il faut au moins 1 transaction associée.
|
* Pour clore une remise, il faut au moins 1 transaction associée.
|
||||||
|
@ -174,39 +174,39 @@ Modèle
|
||||||
|
|
||||||
Cette sous-application dispose d'un unique modèle "SogeCredit" avec les champs suivant :
|
Cette sous-application dispose d'un unique modèle "SogeCredit" avec les champs suivant :
|
||||||
|
|
||||||
* ``user`` : ``OneToOneField`` vers ``User``, utilisateur⋅rice associé à ce crédit (relation ``OneToOne`` car chaque
|
* ``user`` : ``OneToOneField`` vers ``User``, utilisateur associé à ce crédit (relation ``OneToOne`` car chaque
|
||||||
utilisateur⋅rice ne peut bénéficier qu'une seule fois d'un crédit de la Société générale)
|
utilisateur ne peut bénéficier qu'une seule fois d'un crédit de la Société générale)
|
||||||
* ``transactions`` : ``ManyToManyField`` vers ``MembershipTransaction``, liste des transactions d'adhésion associées
|
* ``transactions`` : ``ManyToManyField`` vers ``MembershipTransaction``, liste des transactions d'adhésion associées
|
||||||
à ce crédit, généralement adhésion BDE+Kfet+WEI même si cela n'est pas restreint
|
à ce crédit, généralement adhésion BDE+Kfet+WEI même si cela n'est pas restreint
|
||||||
* ``credit_transaction`` : ``OneToOneField`` vers ``SpecialTransaction``, peut être nulle, transaction de crédit de la
|
* ``credit_transaction`` : ``OneToOneField`` vers ``SpecialTransaction``, peut être nulle, transaction de crédit de la
|
||||||
Société générale vers la note de l'utilisateur⋅rice si celui-ci a été validé. C'est d'ailleurs le témoin
|
Société générale vers la note de l'utilisateur si celui-ci a été validé. C'est d'ailleurs le témoin
|
||||||
de validation du crédit.
|
de validation du crédit.
|
||||||
|
|
||||||
On sait qu'un⋅e utilisateur⋅rice a déjà demandé un crédit de la Société générale s'il existe un crédit associé à cet⋅te
|
On sait qu'un utilisateur a déjà demandé un crédit de la Société générale s'il existe un crédit associé à cet
|
||||||
utilisateur⋅rice avec une transaction associée. Par ailleurs, le modèle ``Profile`` contient une propriété ``soge`` qui
|
utilisateur avec une transaction associée. Par ailleurs, le modèle ``Profile`` contient une propriété ``soge`` qui
|
||||||
traduit exactement ceci, et qui vaut ``False`` si jamais l'application Trésorerie n'est pas chargée.
|
traduit exactement ceci, et qui vaut ``False`` si jamais l'application Trésorerie n'est pas chargée.
|
||||||
|
|
||||||
Si jamais l'utilisateur⋅rice n'a pas encore demandé de crédit de la Société générale (ou que celui-ci n'est pas encore validé),
|
Si jamais l'utilisateur n'a pas encore demandé de crédit de la Société générale (ou que celui-ci n'est pas encore validé),
|
||||||
l'utilisateur⋅rice peut demander un tel crédit lors de son adhésion BDE, de sa réadhésion BDE ou de son inscription au WEI.
|
l'utilisateur peut demander un tel crédit lors de son adhésion BDE, de sa réadhésion BDE ou de son inscription au WEI.
|
||||||
Dans les deux premiers cas, iel est invité⋅e à jumeler avec une nouvelle adhésion Kfet (merci de d'abord se réadhérer au
|
Dans les deux premiers cas, il est invité à jumeler avec une nouvelle adhésion Kfet (merci de d'abord se réadhérer au
|
||||||
BDE avant la Kfet dans ce cas).
|
BDE avant la Kfet dans ce cas).
|
||||||
|
|
||||||
Lorsqu'une telle demande est faite, l'adhésion est créée avec une transaction d'adhésion invalide. Cela implique que
|
Lorsqu'une telle demande est faite, l'adhésion est créée avec une transaction d'adhésion invalide. Cela implique que
|
||||||
la note source n'est pas débitée et la note destination n'est pas créditée.
|
la note source n'est pas débitée et la note destination n'est pas créditée.
|
||||||
|
|
||||||
Sur son interface, læ trésorièr⋅e peut récupérer les crédits de Société générale invalides. Deux options s'offrent à ellui :
|
Sur son interface, le trésorier peut récupérer les crédits de Société générale invalides. Deux options s'offrent à lui :
|
||||||
|
|
||||||
* Supprimer la demande. Dans ce cas, les transactions vont être validées, la note de l'utilisateur⋅rice sera débité, les
|
* Supprimer la demande. Dans ce cas, les transactions vont être validées, la note de l'utilisateur sera débité, les
|
||||||
clubs seront crédités. Puisque la demande sera supprimée, l'utilisateur⋅rice pourra à nouveau à l'avenir déclarer avoir
|
clubs seront crédités. Puisque la demande sera supprimée, l'utilisateur pourra à nouveau à l'avenir déclarer avoir
|
||||||
ouvert un compte à la Société générale. Cette option est utile dans le cas où l'utilisateur⋅rice est un boulet (ou pas,
|
ouvert un compte à la Société générale. Cette option est utile dans le cas où l'utilisateur est un boulet (ou pas,
|
||||||
pour d'autres raisons) et a déclaré vouloir ouvrir un compte à la Société générale sans ne rien faire.
|
pour d'autres raisons) et a déclaré vouloir ouvrir un compte à la Société générale sans ne rien faire.
|
||||||
Cette action est irréversible, et n'est pas possible si la note de l'utilisateur⋅rice n'a pas un solde suffisant.
|
Cette action est irréversible, et n'est pas possible si la note de l'utilisateur n'a pas un solde suffisant.
|
||||||
|
|
||||||
* Valider la demande. Dans ce cas, un crédit de la note "Virements bancaires" vers la note de l'utilisateur⋅rice sera créé,
|
* Valider la demande. Dans ce cas, un crédit de la note "Virements bancaires" vers la note de l'utilisateur sera créé,
|
||||||
la transaction sera liée à la demande via le champ ``credit_note`` (et donc la demande déclarée valide), et toutes les
|
la transaction sera liée à la demande via le champ ``credit_note`` (et donc la demande déclarée valide), et toutes les
|
||||||
transactions d'adhésion seront déclarées valides.
|
transactions d'adhésion seront déclarées valides.
|
||||||
|
|
||||||
* Demander à un⋅e respo info s'il y a un problème pour le régler avant de faire des bêtises. Je l'admets, ça fait trois options.
|
* Demander à un respo info s'il y a un problème pour le régler avant de faire des bêtises. Je l'admets, ça fait trois options.
|
||||||
|
|
||||||
La validité d'une transaction d'adhésion n'a aucune influence sur l'adhésion elle-même. Toutefois, cela se remarque rapidement ...
|
La validité d'une transaction d'adhésion n'a aucune influence sur l'adhésion elle-même. Toutefois, cela se remarque rapidement ...
|
||||||
|
|
||||||
|
|
|
@ -19,14 +19,14 @@ Champs hérités de ``Club`` de l'application ``member`` :
|
||||||
|
|
||||||
* ``parent_club`` : ``ForeignKey(Club)``. Ce champ vaut toujours ``Kfet`` dans le cas d'un WEI : on doit être membre du
|
* ``parent_club`` : ``ForeignKey(Club)``. Ce champ vaut toujours ``Kfet`` dans le cas d'un WEI : on doit être membre du
|
||||||
club Kfet pour participer au WEI.
|
club Kfet pour participer au WEI.
|
||||||
* ``email`` : ``EmailField``, adresse e-mail sur laquelle contacter les gérant⋅es du WEI.
|
* ``email`` : ``EmailField``, adresse e-mail sur laquelle contacter les gérants du WEI.
|
||||||
* ``membership_start`` : ``DateField``, date à partir de laquelle il est possible de s'inscrire au WEI.
|
* ``membership_start`` : ``DateField``, date à partir de laquelle il est possible de s'inscrire au WEI.
|
||||||
* ``membership_end`` : ``DateField``, date de fin d'adhésion possible au WEI.
|
* ``membership_end`` : ``DateField``, date de fin d'adhésion possible au WEI.
|
||||||
* ``membership_duration`` : ``PositiveIntegerField``, inutilisé dans le cas d'un WEI, vaut ``None``.
|
* ``membership_duration`` : ``PositiveIntegerField``, inutilisé dans le cas d'un WEI, vaut ``None``.
|
||||||
* ``membership_fee_paid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un⋅e élève normalien⋅ne
|
* ``membership_fee_paid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un élève normalien
|
||||||
(donc rémunéré⋅e) puisse adhérer.
|
(donc rémunéré) puisse adhérer.
|
||||||
* ``membership_fee_unpaid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un⋅e étudiant⋅e
|
* ``membership_fee_unpaid`` : ``PositiveIntegerField``, montant de la cotisation (en centimes) pour qu'un étudiant
|
||||||
normalien⋅ne (donc non rémunéré⋅e) puisse adhérer.
|
normalien (donc non rémunéré) puisse adhérer.
|
||||||
* ``name`` : ``CharField``, nom du WEI.
|
* ``name`` : ``CharField``, nom du WEI.
|
||||||
* ``require_memberships`` : ``BooleanField``, vaut toujours ``True`` pour le WEI.
|
* ``require_memberships`` : ``BooleanField``, vaut toujours ``True`` pour le WEI.
|
||||||
|
|
||||||
|
@ -65,27 +65,27 @@ que de dissocier les rôles propres au WEI des rôles s'appliquant pour n'import
|
||||||
WEIRegistration
|
WEIRegistration
|
||||||
~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Inscription au WEI, contenant les informations avant validation. Ce modèle est créé dès lors que quelqu'un⋅e se pré-inscrit au WEI.
|
Inscription au WEI, contenant les informations avant validation. Ce modèle est créé dès lors que quelqu'un se pré-inscrit au WEI.
|
||||||
|
|
||||||
* ``user`` : ``ForeignKey(User)``, utilisateur⋅rice qui s'est pré-inscrit⋅e. Ce champ est unique avec ``wei``.
|
* ``user`` : ``ForeignKey(User)``, utilisateur qui s'est pré-inscrit. Ce champ est unique avec ``wei``.
|
||||||
* ``wei`` : ``ForeignKey(WEIClub)``, le WEI auquel l'utilisateur⋅rice s'est pré-inscrit⋅e. Ce champ est unique avec ``user``.
|
* ``wei`` : ``ForeignKey(WEIClub)``, le WEI auquel l'utilisateur s'est pré-inscrit. Ce champ est unique avec ``user``.
|
||||||
* ``soge_credit`` : ``BooleanField``, indique si l'utilisateur⋅rice a déclaré vouloir ouvrir un compte à la Société générale.
|
* ``soge_credit`` : ``BooleanField``, indique si l'utilisateur a déclaré vouloir ouvrir un compte à la Société générale.
|
||||||
* ``caution_check`` : ``BooleanField``, indique si l'utilisateur⋅rice (en 2ème année ou plus) a bien remis son chèque de
|
* ``caution_check`` : ``BooleanField``, indique si l'utilisateur (en 2ème année ou plus) a bien remis son chèque de
|
||||||
caution auprès de la trésorerie.
|
caution auprès de la trésorerie.
|
||||||
* ``birth_date`` : ``DateField``, date de naissance de l'utilisateur⋅rice.
|
* ``birth_date`` : ``DateField``, date de naissance de l'utilisateur.
|
||||||
* ``gender`` : ``CharField`` parmi ``male`` (Homme), ``female`` (Femme), ``non binary`` (Non binaire), genre de la personne.
|
* ``gender`` : ``CharField`` parmi ``male`` (Homme), ``female`` (Femme), ``non binary`` (Non binaire), genre de la personne.
|
||||||
* ``health_issues`` : ``TextField``, problèmes de santé déclarés par l'utilisateur⋅rice.
|
* ``health_issues`` : ``TextField``, problèmes de santé déclarés par l'utilisateur.
|
||||||
* ``emergency_contact_name`` : ``CharField``, nom du contact en cas d'urgence.
|
* ``emergency_contact_name`` : ``CharField``, nom du contact en cas d'urgence.
|
||||||
* ``emergency_contact_phone`` : ``CharField``, numéro de téléphone du contact en cas d'urgence.
|
* ``emergency_contact_phone`` : ``CharField``, numéro de téléphone du contact en cas d'urgence.
|
||||||
* ``ml_events_registration`` : ``BooleanField``, déclare si l'utilisateur⋅rice veut s'inscrire à la liste de diffusion des
|
* ``ml_events_registration`` : ``BooleanField``, déclare si l'utilisateur veut s'inscrire à la liste de diffusion des
|
||||||
événements du BDE (1A uniquement)
|
événements du BDE (1A uniquement)
|
||||||
* ``ml_art_registration`` : ``BooleanField``, déclare si l'utilisateur⋅rice veut s'inscrire à la liste de diffusion des
|
* ``ml_art_registration`` : ``BooleanField``, déclare si l'utilisateur veut s'inscrire à la liste de diffusion des
|
||||||
actualités du BDA (1A uniquement)
|
actualités du BDA (1A uniquement)
|
||||||
* ``ml_sport_registration`` : ``BooleanField``, déclare si l'utilisateur⋅rice veut s'inscrire à la liste de diffusion des
|
* ``ml_sport_registration`` : ``BooleanField``, déclare si l'utilisateur veut s'inscrire à la liste de diffusion des
|
||||||
actualités du BDS (1A uniquement)
|
actualités du BDS (1A uniquement)
|
||||||
* ``first_year`` : ``BooleanField``, indique si l'inscription est d'un⋅e 1A ou non. Non modifiable par n'importe qui.
|
* ``first_year`` : ``BooleanField``, indique si l'inscription est d'un 1A ou non. Non modifiable par n'importe qui.
|
||||||
* ``information_json`` : ``TextField`` non modifiable manuellement par n'importe qui stockant les informations du
|
* ``information_json`` : ``TextField`` non modifiable manuellement par n'importe qui stockant les informations du
|
||||||
questionnaire d'inscription au WEI pour les 1A, et stocke les demandes faites par un⋅e 2A+ concernant bus, équipes et rôles.
|
questionnaire d'inscription au WEI pour les 1A, et stocke les demandes faites par un 2A+ concerant bus, équipes et rôles.
|
||||||
On utilise un ``TextField`` contenant des données au format JSON pour permettre de la modularité au fil des années,
|
On utilise un ``TextField`` contenant des données au format JSON pour permettre de la modularité au fil des années,
|
||||||
sans avoir à tout casser à chaque fois.
|
sans avoir à tout casser à chaque fois.
|
||||||
|
|
||||||
|
@ -94,19 +94,19 @@ WEIMembership
|
||||||
|
|
||||||
Ce modèle hérite de ``Membership`` et contient les informations d'une adhésion au WEI.
|
Ce modèle hérite de ``Membership`` et contient les informations d'une adhésion au WEI.
|
||||||
|
|
||||||
* ``bus`` : ``ForeignKey(Bus)``, bus dans lequel se trouve l'utilisateur⋅rice.
|
* ``bus`` : ``ForeignKey(Bus)``, bus dans lequel se trouve l'utilisateur.
|
||||||
* ``team`` : ``ForeignKey(BusTeam)`` pouvant être nulle (pour les chefs de bus et électrons libres), équipe dans laquelle
|
* ``team`` : ``ForeignKey(BusTeam)`` pouvant être nulle (pour les chefs de bus et électrons libres), équipe dans laquelle
|
||||||
se trouve l'utilisateur⋅rice.
|
se trouve l'utilisateur.
|
||||||
* ``registration`` : ``OneToOneField(WEIRegistration)``, informations de la pré-inscription.
|
* ``registration`` : ``OneToOneField(WEIRegistration)``, informations de la pré-inscription.
|
||||||
|
|
||||||
Champs hérités du modèle ``Membership`` :
|
Champs hérités du modèle ``Membership`` :
|
||||||
|
|
||||||
* ``club`` : ``ForeignKey(Club)``, club lié à l'adhésion. Doit être un ``WEIClub``.
|
* ``club`` : ``ForeignKey(Club)``, club lié à l'adhésion. Doit être un ``WEIClub``.
|
||||||
* ``user`` : ``ForeignKey(User)``, utilisateur⋅rice qui a adhéré.
|
* ``user`` : ``ForeignKey(User)``, utilisateur adhéré.
|
||||||
* ``date_start`` : ``DateField``, date de début d'adhésion.
|
* ``date_start`` : ``DateField``, date de début d'adhésion.
|
||||||
* ``date_end`` : ``DateField``, date de fin d'adhésion.
|
* ``date_end`` : ``DateField``, date de fin d'adhésion.
|
||||||
* ``fee`` : ``PositiveIntegerField``, montant de la cotisation payée.
|
* ``fee`` : ``PositiveIntegerField``, montant de la cotisation payée.
|
||||||
* ``roles`` : ``ManyToManyField(Role)``, liste des rôles endossés par l'adhérent⋅e. Les rôles doivent être des ``WEIRole``.
|
* ``roles`` : ``ManyToManyField(Role)``, liste des rôles endossés par l'adhérent. Les rôles doivent être des ``WEIRole``.
|
||||||
|
|
||||||
Graphe des modèles
|
Graphe des modèles
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -123,32 +123,32 @@ Fonctionnement
|
||||||
Création d'un WEI
|
Création d'un WEI
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Seul un⋅e respo info peut créer un WEI. Pour cela, se rendre dans l'onglet WEI, puis « Liste des WEI » et enfin
|
Seul un respo info peut créer un WEI. Pour cela, se rendre dans l'onglet WEI, puis "Liste des WEI" et enfin
|
||||||
« Créer un WEI ». Diverses informations sont demandées, comme le nom du WEI, l'adresse mail de contact, l'année du WEI
|
"Créer un WEI". Diverses informations sont demandées, comme le nom du WEI, l'adresse mail de contact, l'année du WEI
|
||||||
(doit être unique), les dates de début et de fin, et les dates pendant lesquelles les utilisateurs peuvent s'inscrire.
|
(doit être unique), les dates de début et de fin, et les dates pendant lesquelles les utilisateurs peuvent s'inscrire.
|
||||||
|
|
||||||
Don des droits à un GC WEI
|
Don des droits à un GC WEI
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Læ GC WEI peut gérer tout ce qui a un rapport avec le WEI. Iel ne peut cependant pas créer le WEI, ce privilège est
|
Le GC WEI peut gérer tout ce qui a un rapport avec le WEI. Il ne peut cependant pas créer le WEI, ce privilège est
|
||||||
réservé aux respos info. Pour avoir ses droits, læ GC WEI doit s'inscrire au WEI avec le rôle GC WEI, et donc payer
|
réservé au respo info. Pour avoir ses droits, le GC WEI doit s'inscrire au WEI avec le rôle GC WEI, et donc payer
|
||||||
en premièr⋅e sa cotisation. C'est donc aux respos info de créer l'adhésion du GC WEI. Voir ci-dessous pour l'inscription au WEI.
|
en premier sa cotisation. C'est donc au respo info de créer l'adhésion du GC WEI. Voir ci-dessous pour l'inscription au WEI.
|
||||||
|
|
||||||
S'inscrire au WEI
|
S'inscrire au WEI
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
N'importe quel⋅le utilisateur⋅rice peut s'auto-inscrire au WEI, lorsque les dates d'adhésion le permettent. Celleux qui se sont
|
N'importe quel utilisateur peut s'auto-inscrire au WEI, lorsque les dates d'adhésion le permettent. Ceux qui se sont
|
||||||
déjà inscrit⋅es peuvent également inscrire un⋅e 1A. Seul⋅es les GC WEI et les respos info peuvent inscrire un⋅e autre 2A+.
|
déjà inscrits peuvent également inscrire un 1A. Seuls les GC WEI et les respo info peuvent inscrire un autre 2A+.
|
||||||
|
|
||||||
À tout moment, tant que le WEI n'est pas passé, l'inscription peut être modifiée, même après validation.
|
À tout moment, tant que le WEI n'est pas passé, l'inscription peut être modifiée, même après validation.
|
||||||
|
|
||||||
Inscription d'un⋅e 2A+
|
Inscription d'un 2A+
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Comme indiqué, les 2A+ sont assez autonomes dans leur inscription au WEI. Iels remplissent le questionnaire et sont
|
Comme indiqué, les 2A+ sont assez autonomes dans leur inscription au WEI. Ils remplissent le questionnaire et sont
|
||||||
ensuite pré-inscrit⋅es. Le questionnaire se compose de plusieurs champs (voir WEIRegistration) :
|
ensuite pré-inscrits. Le questionnaire se compose de plusieurs champs (voir WEIRegistration) :
|
||||||
|
|
||||||
* Est-ce que l'utilisateur⋅rice a déclaré avoir ouvert un compte à la Société générale ? (Option disponible uniquemement
|
* Est-ce que l'utilisateur a déclaré avoir ouvert un compte à la Société générale ? (Option disponible uniquemement
|
||||||
si cela n'a pas été fait une année avant)
|
si cela n'a pas été fait une année avant)
|
||||||
* Date de naissance
|
* Date de naissance
|
||||||
* Genre (Homme/Femme/Non-binaire)
|
* Genre (Homme/Femme/Non-binaire)
|
||||||
|
@ -159,17 +159,17 @@ ensuite pré-inscrit⋅es. Le questionnaire se compose de plusieurs champs (voir
|
||||||
* Équipes préférées (choix multiple éventuellement vide, vide pour les chefs de bus/staff)
|
* Équipes préférées (choix multiple éventuellement vide, vide pour les chefs de bus/staff)
|
||||||
* Rôles souhaités
|
* Rôles souhaités
|
||||||
|
|
||||||
Les trois derniers champs n'ont aucun caractère définitif et sont simplement là en suggestion pour læ GC WEI qui
|
Les trois derniers champs n'ont aucun caractère définitif et sont simplement là en suggestion pour le GC WEI qui
|
||||||
validera l'inscription. C'est utile si on hésite entre plusieurs bus.
|
validera l'inscription. C'est utile si on hésite entre plusieurs bus.
|
||||||
|
|
||||||
L'inscription est ensuite créée, le GC WEI devra ensuite la valider (voir plus bas).
|
L'inscription est ensuite créée, le GC WEI devra ensuite la valider (voir plus bas).
|
||||||
|
|
||||||
Inscription d'un⋅e 1A
|
Inscription d'un 1A
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
N'importe quelle personne déjà inscrite au WEI peut inscrire un⋅e 1A. Le formulaire 1A est assez peu différent du formulaire 2A+ :
|
N'importe quelle personne déjà inscrite au WEI peut inscrire un 1A. Le formulaire 1A est assez peu différent du formulaire 2A+ :
|
||||||
|
|
||||||
* Est-ce que l'utilisateur⋅rice a déclaré avoir ouvert un compte à la Société générale ?
|
* Est-ce que l'utilisateur a déclaré avoir ouvert un compte à la Société générale ?
|
||||||
* Date de naissance
|
* Date de naissance
|
||||||
* Genre (Homme/Femme/Non-binaire)
|
* Genre (Homme/Femme/Non-binaire)
|
||||||
* Problèmes de santé
|
* Problèmes de santé
|
||||||
|
@ -179,10 +179,10 @@ N'importe quelle personne déjà inscrite au WEI peut inscrire un⋅e 1A. Le for
|
||||||
* S'inscrire à la ML BDA
|
* S'inscrire à la ML BDA
|
||||||
* S'inscrire à la ML BDS
|
* S'inscrire à la ML BDS
|
||||||
|
|
||||||
Læ 1A ne peut donc pas choisir de son bus et de son équipe, et peut s'inscrire aux listes de diffusion.
|
Le 1A ne peut donc pas choisir de son bus et de son équipe, et peut s'inscrire aux listes de diffusion.
|
||||||
Il y a néanmoins une différence majeure : une fois le formulaire rempli, un questionnaire se lance.
|
Il y a néanmoins une différence majeure : une fois le formulaire rempli, un questionnaire se lance.
|
||||||
Ce questionnaire peut varier au fil des années (voir section Questionnaire), et contient divers formulaires de collecte
|
Ce questionnaire peut varier au fil des années (voir section Questionnaire), et contient divers formulaires de collecte
|
||||||
de données qui serviront à déterminer quel est le meilleur bus pour ce⋅tte nouvelleau utilisateur⋅rice.
|
de données qui serviront à déterminer quel est le meilleur bus pour ce nouvel utilisateur.
|
||||||
|
|
||||||
Questionnaire 1A
|
Questionnaire 1A
|
||||||
^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^
|
||||||
|
@ -200,7 +200,7 @@ Je veux changer d'algorithme de répartition, que faire ?
|
||||||
|
|
||||||
Cette section est plus technique et s'adresse surtout aux respos info en cours de mandat.
|
Cette section est plus technique et s'adresse surtout aux respos info en cours de mandat.
|
||||||
|
|
||||||
Première règle : on ne supprime rien (sauf si vraiment c'est du mauvais boulot). En prenant exemple sur des fichiers déjà existant tels que ``apps/wei/forms/surveys/wei2021.py``, créer un nouveau fichier ``apps/wei/forms/surveys/wei20XY.py``. Ce fichier doit inclure les éléments suivants :
|
Première règle : on ne supprime rien (sauf si vraiment c'est du mauvais boulot). En prenant exemple sur des fichiers déjà existant tels que ``apps/wei/forms/surveys/wei2020.py``, créer un nouveau fichier ``apps/wei/forms/surveys/wei20XY.py``. Ce fichier doit inclure les éléments suivants :
|
||||||
|
|
||||||
WEISurvey
|
WEISurvey
|
||||||
"""""""""
|
"""""""""
|
||||||
|
@ -223,7 +223,7 @@ Une classe héritant de ``wei.forms.surveys.base.WEISurvey``, comportant les él
|
||||||
Naturellement, il est implicite qu'une fonction ayant pour premier argument ``cls`` doit être annotée par ``@classmethod``.
|
Naturellement, il est implicite qu'une fonction ayant pour premier argument ``cls`` doit être annotée par ``@classmethod``.
|
||||||
Nativement, la classe ``WEISurvey`` comprend les informations suivantes :
|
Nativement, la classe ``WEISurvey`` comprend les informations suivantes :
|
||||||
|
|
||||||
* ``registration``, le modèle ``WEIRegistration`` de l'utilisateur⋅rice qui remplit le questionnaire
|
* ``registration``, le modèle ``WEIRegistration`` de l'utilisateur qui remplit le questionnaire
|
||||||
* ``information``, instance de ``WEISurveyInformation``, contient les données du questionnaire en cours de remplissage.
|
* ``information``, instance de ``WEISurveyInformation``, contient les données du questionnaire en cours de remplissage.
|
||||||
* ``get_wei(cls)``, renvoie le WEI correspondant à l'année du sondage.
|
* ``get_wei(cls)``, renvoie le WEI correspondant à l'année du sondage.
|
||||||
* ``save(self)``, enregistre les informations du sondage dans l'objet ``registration`` associé, qui est ensuite
|
* ``save(self)``, enregistre les informations du sondage dans l'objet ``registration`` associé, qui est ensuite
|
||||||
|
@ -291,7 +291,7 @@ pour unique effet d'appeler la fonction ``run_algorithm`` décrite plus tôt. Un
|
||||||
n'a pas été évoqué d'adhésion. L'adhésion est ensuite manuelle, l'algorithme ne fournit qu'une suggestion.
|
n'a pas été évoqué d'adhésion. L'adhésion est ensuite manuelle, l'algorithme ne fournit qu'une suggestion.
|
||||||
|
|
||||||
Cette structure, complexe mais raisonnable, permet de gérer plus ou moins proprement la répartition des 1A,
|
Cette structure, complexe mais raisonnable, permet de gérer plus ou moins proprement la répartition des 1A,
|
||||||
en limitant très fortement le hard code. Ami⋅e nouvelleau développeur⋅se, merci de bien penser à la propreté du code :)
|
en limitant très fortement le hard code. Ami nouveau développeur, merci de bien penser à la propreté du code :)
|
||||||
En particulier, on évitera de mentionner dans le code le nom des bus, et profiter du champ ``information_json``
|
En particulier, on évitera de mentionner dans le code le nom des bus, et profiter du champ ``information_json``
|
||||||
présent dans le modèle ``Bus``.
|
présent dans le modèle ``Bus``.
|
||||||
|
|
||||||
|
@ -300,34 +300,34 @@ Valider les inscriptions
|
||||||
|
|
||||||
Cette partie est moins technique.
|
Cette partie est moins technique.
|
||||||
|
|
||||||
Une fois la pré-inscription faite, elle doit être validée par le BDE, afin de procéder au paiement. Læ GC WEI a accès à
|
Une fois la pré-inscription faite, elle doit être validée par le BDE, afin de procéder au paiement. Le GC WEI a accès à
|
||||||
la liste des inscriptions non validées, soit sur la page de détails du WEI, soit sur un tableau plus large avec filtre.
|
la liste des inscriptions non validées, soit sur la page de détails du WEI, soit sur un tableau plus large avec filtre.
|
||||||
Une inscription non validée peut soit être validée, soit supprimée (la suppression est irréversible).
|
Une inscription non validée peut soit être validée, soit supprimée (la suppression est irréversible).
|
||||||
|
|
||||||
Lorsque læ GC WEI veut valider une inscription, iel a accès au récapitulatif de l'inscription ainsi qu'aux informations
|
Lorsque le GC WEI veut valider une inscription, il a accès au récapitulatif de l'inscription ainsi qu'aux informations
|
||||||
personnelles de l'utilisateur⋅rice. Il lui est proposé de les modifier si besoin (du moins les informations liées au WEI,
|
personnelles de l'utilisateur. Il lui est proposé de les modifier si besoin (du moins les informations liées au WEI,
|
||||||
pas les informations personnelles). Iel a enfin accès aux résultats du sondage et la sortie de l'algorithme s'il s'agit
|
pas les informations personnelles). Il a enfin accès aux résultats du sondage et la sortie de l'algorithme s'il s'agit
|
||||||
d'un⋅e 1A, aux préférences d'un⋅e 2A+. Avant de valider, læ GC WEI doit sélectionner un bus, éventuellement une équipe
|
d'un 1A, aux préférences d'un 2A+. Avant de valider, le GC WEI doit sélectionner un bus, éventuellement une équipe
|
||||||
et un rôle. Si c'est un⋅e 1A et que l'algorithme a tourné, ou si c'est un⋅e 2A+ qui n'a fait qu'un seul choix de bus,
|
et un rôle. Si c'est un 1A et que l'algorithme a tourné, ou si c'est un 2A+ qui n'a fait qu'un seul choix de bus,
|
||||||
d'équipe, de rôles, les champs sont automatiquement pré-remplis.
|
d'équipe, de rôles, les champs sont automatiquement pré-remplis.
|
||||||
|
|
||||||
Quelques restrictions cependant :
|
Quelques restrictions cependant :
|
||||||
|
|
||||||
* Si c'est un⋅e 2A+, le chèque de caution doit être déclaré déposé
|
* Si c'est un 2A+, le chèque de caution doit être déclaré déposé
|
||||||
* Si l'inscription se fait via la Société générale, un message expliquant la situation apparaît : la transaction de
|
* Si l'inscription se fait via la Société générale, un message expliquant la situation apparaît : la transaction de
|
||||||
paiement sera créée mais invalidée, les trésorièr⋅es devront confirmer plus tard sur leur interface que le compte
|
paiement sera créée mais invalidée, les trésoriers devront confirmer plus tard sur leur interface que le compte
|
||||||
à la Société générale a bien été créé avant de valider la transaction (voir `Trésorerie <treasury>`_ section
|
à la Société générale a bien été créé avant de valider la transaction (voir `Trésorerie <treasury>`_ section
|
||||||
Crédit de la Société générale).
|
Crédit de la Société générale).
|
||||||
* Dans le cas contraire, l'utilisateur⋅rice doit avoir le solde nécessaire sur sa note avant de pouvoir adhérer.
|
* Dans le cas contraire, l'utilisateur doit avoir le solde nécessaire sur sa note avant de pouvoir adhérer.
|
||||||
* L'utilisateur⋅rice doit enfin être membre du club Kfet. Un lien est présent pour le faire adhérer ou réadhérer selon le cas.
|
* L'utilisateur doit enfin être membre du club Kfet. Un lien est présent pour le faire adhérer ou réadhérer selon le cas.
|
||||||
|
|
||||||
Si tout est bon, læ GC WEI peut valider. L'utilisateur⋅rice a bien payé son WEI, et son interface est un peu plus grande.
|
Si tout est bon, le GC WEI peut valider. L'utilisateur a bien payé son WEI, et son interface est un peu plus grande.
|
||||||
Iel peut toujours changer ses paramètres au besoin. Un⋅e 1A ne voit rien de plus avant la fin du WEI.
|
Il peut toujours changer ses paramètres au besoin. Un 1A ne voit rien de plus avant la fin du WEI.
|
||||||
|
|
||||||
Un⋅e adhérent⋅e WEI non 1A a accès à la liste des bus, des équipes et de leur descriptions. Les chef⋅fes de bus peuvent gérer
|
Un adhérent WEI non 1A a accès à la liste des bus, des équipes et de leur descriptions. Les chefs de bus peuvent gérer
|
||||||
les bus et leurs équipes. Les chef⋅fes d'équipe peuvent gérer leurs équipes. Cela inclut avoir accès à la liste des membres
|
les bus et leurs équipes. Les chefs d'équipe peuvent gérer leurs équipes. Cela inclut avoir accès à la liste des membres
|
||||||
de ce bus / de cette équipe.
|
de ce bus / de cette équipe.
|
||||||
|
|
||||||
Un export au format PDF de la liste des membres *visibles* est disponible pour chacun⋅e.
|
Un export au format PDF de la liste des membres *visibles* est disponible pour chacun.
|
||||||
|
|
||||||
Bon WEI à toustes !
|
Bon WEI à tous !
|
||||||
|
|
|
@ -3,7 +3,7 @@ Service d'Authentification Centralisé (CAS)
|
||||||
|
|
||||||
Un `CAS <https://fr.wikipedia.org/wiki/Central_Authentication_Service>`_ est
|
Un `CAS <https://fr.wikipedia.org/wiki/Central_Authentication_Service>`_ est
|
||||||
déployé sur la Note Kfet. Il est accessible à l'adresse `<https://note.crans.org/cas/>`_.
|
déployé sur la Note Kfet. Il est accessible à l'adresse `<https://note.crans.org/cas/>`_.
|
||||||
Il a pour but uniquement d'authentifier les utilisateur⋅rices via la note et ne communique
|
Il a pour but uniquement d'authentifier les utilisateurs via la note et ne communique
|
||||||
que peu d'informations.
|
que peu d'informations.
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
|
|
|
@ -12,10 +12,10 @@ Applications externes
|
||||||
L'utilisation de la note par des services externes est actuellement en beta. Il est
|
L'utilisation de la note par des services externes est actuellement en beta. Il est
|
||||||
fort à parier que cette utilisation sera revue et améliorée à l'avenir.
|
fort à parier que cette utilisation sera revue et améliorée à l'avenir.
|
||||||
|
|
||||||
Puisque la Note Kfet recense tous les comptes des adhérent⋅es BDE, les clubs ont alors
|
Puisque la Note Kfet recense tous les comptes des adhérents BDE, les clubs ont alors
|
||||||
la possibilité de développer leurs propres applications et de les interfacer avec la
|
la possibilité de développer leurs propres applications et de les interfacer avec la
|
||||||
note. De cette façon, chaque application peut authentifier ses utilisateur⋅rices via la note,
|
note. De cette façon, chaque application peut authentifier ses utilisateurs via la note,
|
||||||
et récupérer leurs adhésions, leur nom de note afin d'éventuellement faire des transferts
|
et récupérer leurs adhésion, leur nom de note afin d'éventuellement faire des transferts
|
||||||
via l'API.
|
via l'API.
|
||||||
|
|
||||||
Deux protocoles d'authentification sont implémentées :
|
Deux protocoles d'authentification sont implémentées :
|
||||||
|
@ -25,4 +25,4 @@ Deux protocoles d'authentification sont implémentées :
|
||||||
|
|
||||||
À ce jour, il n'y a pas encore d'exemple d'utilisation d'application qui utilise ce
|
À ce jour, il n'y a pas encore d'exemple d'utilisation d'application qui utilise ce
|
||||||
mécanisme, mais on peut imaginer par exemple que la Mediatek ou l'AMAP implémentent
|
mécanisme, mais on peut imaginer par exemple que la Mediatek ou l'AMAP implémentent
|
||||||
ces protocoles pour récupérer leurs adhérent⋅es.
|
ces protocoles pour récupérer leurs adhérents.
|
||||||
|
|
|
@ -2,12 +2,12 @@ OAuth2
|
||||||
======
|
======
|
||||||
|
|
||||||
L'authentification `OAuth2 <https://fr.wikipedia.org/wiki/OAuth>`_ est supportée par la
|
L'authentification `OAuth2 <https://fr.wikipedia.org/wiki/OAuth>`_ est supportée par la
|
||||||
Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateur⋅rices, mais
|
Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateurs, mais aussi
|
||||||
aussi de transmettre des informations à un service tiers tels que des informations
|
de transmettre des informations à un service tiers tels que des informations personnelles,
|
||||||
personnelles, le solde de la note ou encore les adhésions de l'utilisateur⋅rice, en
|
le solde de la note ou encore les adhésions de l'utilisateur, en l'avertissant sur
|
||||||
l'avertissant sur quelles données sont effectivement collectées. Ainsi, il est possible
|
quelles données sont effectivement collectées. Ainsi, il est possible de développer des
|
||||||
de développer des appplications tierces qui peuvent se baser sur les données de la Note
|
appplications tierces qui peuvent se baser sur les données de la Note Kfet ou encore
|
||||||
Kfet ou encore faire des transactions.
|
faire des transactions.
|
||||||
|
|
||||||
|
|
||||||
Configuration du serveur
|
Configuration du serveur
|
||||||
|
@ -79,7 +79,7 @@ Il vous suffit de donner à votre application :
|
||||||
* Les scopes, qui peuvent être récupérées sur cette page : `<https://note.crans.org/permission/scopes/>`_
|
* Les scopes, qui peuvent être récupérées sur cette page : `<https://note.crans.org/permission/scopes/>`_
|
||||||
* L'URL d'autorisation : `<https://note.crans.org/o/authorize/>`_
|
* L'URL d'autorisation : `<https://note.crans.org/o/authorize/>`_
|
||||||
* L'URL d'obtention de jeton : `<https://note.crans.org/o/token/>`_
|
* L'URL d'obtention de jeton : `<https://note.crans.org/o/token/>`_
|
||||||
* Si besoin, l'URL de récupération des informations de l'utilisateur⋅rice : `<https://note.crans.org/api/me/>`_
|
* Si besoin, l'URL de récupération des informations de l'utilisateur : `<https://note.crans.org/api/me/>`_
|
||||||
|
|
||||||
N'hésitez pas à consulter la page `<https://note.crans.org/api/me/>`_ pour s'imprégner
|
N'hésitez pas à consulter la page `<https://note.crans.org/api/me/>`_ pour s'imprégner
|
||||||
du format renvoyé.
|
du format renvoyé.
|
||||||
|
@ -88,14 +88,14 @@ du format renvoyé.
|
||||||
|
|
||||||
Un petit mot sur les scopes : tel qu'implémenté, une scope est une permission unitaire
|
Un petit mot sur les scopes : tel qu'implémenté, une scope est une permission unitaire
|
||||||
(telle que décrite dans le modèle ``Permission``) associée à un club. Ainsi, un jeton
|
(telle que décrite dans le modèle ``Permission``) associée à un club. Ainsi, un jeton
|
||||||
a accès à une scope si et seulement si læ propriétaire du jeton dispose d'une adhésion
|
a accès à une scope si et seulement si le/la propriétaire du jeton dispose d'une adhésion
|
||||||
courante dans le club lié à la scope qui lui octroie cette permission.
|
courante dans le club lié à la scope qui lui octroie cette permission.
|
||||||
|
|
||||||
Par exemple, un jeton pourra avoir accès à la permission de créer des transactions en lien
|
Par exemple, un jeton pourra avoir accès à la permission de créer des transactions en lien
|
||||||
avec un club si et seulement si læ propriétaire du jeton est trésorièr⋅e du club.
|
avec un club si et seulement si le propriétaire du jeton est trésorier du club.
|
||||||
|
|
||||||
La vérification des droits de læ propriétaire est faite systématiquement, afin de ne pas
|
La vérification des droits du propriétaire est faite systématiquement, afin de ne pas
|
||||||
faire confiance au jeton en cas de droits révoqués à saon propriétaire.
|
faire confiance au jeton en cas de droits révoqués à son propriétaire.
|
||||||
|
|
||||||
Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
|
Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
|
||||||
jetons.
|
jetons.
|
||||||
|
@ -118,8 +118,8 @@ du format renvoyé.
|
||||||
|
|
||||||
Par exemple, vous pourriez demander la permission d'accéder
|
Par exemple, vous pourriez demander la permission d'accéder
|
||||||
aux membres d'un club ou de faire des transactions, et agir
|
aux membres d'un club ou de faire des transactions, et agir
|
||||||
uniquement dans le cas où l'utilisateur⋅rice connecté⋅e
|
uniquement dans le cas où l'utilisateur connecté possède la
|
||||||
possède la permission problématique.
|
permission problématique.
|
||||||
|
|
||||||
Avec Django-allauth
|
Avec Django-allauth
|
||||||
###################
|
###################
|
||||||
|
@ -152,7 +152,7 @@ Le paramètre ``DOMAIN`` permet de changer d'instance de Note Kfet. Par défaut,
|
||||||
se connectera à ``note.crans.org`` si vous ne renseignez rien.
|
se connectera à ``note.crans.org`` si vous ne renseignez rien.
|
||||||
|
|
||||||
Le paramètre ``SCOPE`` permet de définir les scopes à demander.
|
Le paramètre ``SCOPE`` permet de définir les scopes à demander.
|
||||||
Dans l'exemple ci-dessous, les permissions d'accéder à l'utilisateur⋅rice
|
Dans l'exemple ci-dessous, les permissions d'accéder à l'utilisateur
|
||||||
et au profil sont demandées.
|
et au profil sont demandées.
|
||||||
|
|
||||||
En créant l'application sur la note, vous pouvez renseigner
|
En créant l'application sur la note, vous pouvez renseigner
|
||||||
|
@ -200,7 +200,7 @@ cas où elle n'est pas explicitement indiquée lors de l'autorisation.
|
||||||
|
|
||||||
Lorsqu'un client veut s'authentifier via la Note Kfet, il va devoir accéder à une page
|
Lorsqu'un client veut s'authentifier via la Note Kfet, il va devoir accéder à une page
|
||||||
d'authentification. La page d'autorisation est `<https://note.crans.org/o/authorize/>`_,
|
d'authentification. La page d'autorisation est `<https://note.crans.org/o/authorize/>`_,
|
||||||
c'est sur cette page qu'il faut rediriger les utilisateur⋅rices. Il faut mettre en paramètre GET :
|
c'est sur cette page qu'il faut rediriger les utilisateurs. Il faut mettre en paramètre GET :
|
||||||
|
|
||||||
* ``client_id`` : l'identifiant client de l'application (public) ;
|
* ``client_id`` : l'identifiant client de l'application (public) ;
|
||||||
* ``response_type`` : mettre ``code`` ;
|
* ``response_type`` : mettre ``code`` ;
|
||||||
|
@ -211,10 +211,10 @@ c'est sur cette page qu'il faut rediriger les utilisateur⋅rices. Il faut mettr
|
||||||
* ``state`` : optionnel, peut être utilisé pour permettre au client de détecter des requêtes
|
* ``state`` : optionnel, peut être utilisé pour permettre au client de détecter des requêtes
|
||||||
provenant d'autres sites.
|
provenant d'autres sites.
|
||||||
|
|
||||||
Sur cette page, les permissions demandées seront listées, et l'utilisateur⋅rice aura le
|
Sur cette page, les permissions demandées seront listées, et l'utilisateur aura le choix
|
||||||
choix d'accepter ou non. Dans les deux cas, l'utilisateur⋅rice sera redirigée vers
|
d'accepter ou non. Dans les deux cas, l'utilisateur sera redirigée vers ``redirect_uri``,
|
||||||
``redirect_uri``, avec pour paramètre GET soit le message d'erreur, soit un paramètre
|
avec pour paramètre GET soit le message d'erreur, soit un paramètre ``code`` correspondant
|
||||||
``code`` correspondant au code d'autorisation.
|
au code d'autorisation.
|
||||||
|
|
||||||
Une fois ce code d'autorisation récupéré, il faut désormais récupérer le jeton d'accès.
|
Une fois ce code d'autorisation récupéré, il faut désormais récupérer le jeton d'accès.
|
||||||
Il faut pour cela aller sur l'URL `<https://note.crans.org/o/token/>`_, effectuer une
|
Il faut pour cela aller sur l'URL `<https://note.crans.org/o/token/>`_, effectuer une
|
||||||
|
|
175
docs/faq.rst
175
docs/faq.rst
|
@ -5,26 +5,27 @@ Des transactions anormales sont apparues sur mon compte.
|
||||||
--------------------------------------------------------
|
--------------------------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Tu dois immédiatement contacter les trésorièr⋅es du BDE (voir ci-dessous) pour
|
Tu dois immédiatement contacter les trésoriers du BDE (voir ci-dessous) pour
|
||||||
signaler l'incident. Précise bien ton nom de note, l'heure de la transaction ainsi que
|
signaler l'incident. Précise bien ton nom de note, l'heure de la transaction
|
||||||
l'alias utilisé pour faire la transaction (en plaçant ta souris sur ton pseudo sur la
|
ainsi que l'alias utilisé pour faire la transaction (en plaçant ta souris sur
|
||||||
ligne de transaction, l'alias utilisé apparaît). La raison la plus courante est que tu
|
ton pseudo sur la ligne de transaction, l'alias utilisé apparaît). La raison
|
||||||
as un alias qui est trop proche d'une autre personne. Même si la Note Kfet 2020 essaie
|
la plus courante est que tu as un alias qui est trop proche d'un autre d'une
|
||||||
d'éviter ça, tu es invité⋅e à supprimer l'alias problématique, ou tout du moins
|
autre personne. Même si la Note Kfet 2020 essaie d'éviter ça, tu es invité⋅e
|
||||||
t'assurer que la confusion ne puisse plus arriver.
|
à supprimer l'alias problématique, ou tout du moins t'assurer que la confusion
|
||||||
|
ne puisse plus arriver.
|
||||||
|
|
||||||
|
|
||||||
Je souhaite consommer mais le solde de ma note est insuffisant
|
Je souhaite consommer mais le solde de ma note est insuffisant
|
||||||
--------------------------------------------------------------
|
--------------------------------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Le BDE ne fait pas crédit à ses adhérent⋅es. Il est de ton devoir de t'assurer d'avoir
|
Le BDE ne fait pas crédit à ses adhérents. Il est de ton devoir de t'assurer
|
||||||
en permanence un solde positif sur ta note. Les permanencièr⋅es à la Kfet ont la
|
d'avoir en permanence un solde positif sur ta note. Les permanenciers à la
|
||||||
possibilité de refuser une consommation qui fait passer en négatif, et ont obligation
|
Kfet ont la possibilité de refuser une consommation qui fait passer en négatif,
|
||||||
de refuser si la consommation est alcoolisée, en accord avec la règlementation en
|
et ont obligation de refuser si la consommation est alcoolisée, en accord avec
|
||||||
vigueur.
|
la règlementation en vigueur.
|
||||||
|
|
||||||
Les trésorièr⋅es connaissent la liste des personnes en situation irrégulière et
|
Les trésoriers connaissent la liste des personnes en situation irrégulière et
|
||||||
n'hésiteront pas à faire des rappels pour recharger la note.
|
n'hésiteront pas à faire des rappels pour recharger la note.
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,10 +33,10 @@ Comment recharger ma note ?
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Le solde de la note peut être rechargé soit par espèces, par chèque à l'ordre de
|
Le solde de la note peut être rechargé soit par espèces, par chèque à l'ordre
|
||||||
l'amicale des élèves de l'ENS Paris-Saclay, par carte bancaire via un terminal de
|
de l'amicale des élèves de l'ENS Cachan, par carte bancaire via un terminal
|
||||||
paiement électronique ou encore par virement bancaire, dont les coordonnées sont à
|
de paiement électronique ou encore par virement bancaire, dont les coordonnées
|
||||||
demander auprès des trésorièr⋅es BDE.
|
sont à demander auprès des trésoriers BDE.
|
||||||
|
|
||||||
Les trois premières options sont à faire directement dans la Kfet.
|
Les trois premières options sont à faire directement dans la Kfet.
|
||||||
|
|
||||||
|
@ -44,9 +45,9 @@ Je pars en stage / en vacances. Puis-je bloquer ma note ?
|
||||||
---------------------------------------------------------
|
---------------------------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Bien sûr : il te suffit de te rendre sur ton compte et de cliquer sur le bouton dédié.
|
Bien sûr : il te suffit de te rendre sur ton compte et de cliquer sur le bouton
|
||||||
Ta note ne sera plus affichée par les autres personnes et les transferts seront
|
dédié. Ta note ne sera plus affichée par les autres personnes et les transferts
|
||||||
impossibles, sauf pour les trésorièr⋅es BDE et respo info.
|
seront impossibles, sauf pour les trésoriers BDE et respo info.
|
||||||
|
|
||||||
Il est toutefois de ton devoir de rembourser tout ce que tu dois.
|
Il est toutefois de ton devoir de rembourser tout ce que tu dois.
|
||||||
|
|
||||||
|
@ -55,87 +56,88 @@ Quelle est la limite maximale au nombre d'alias d'une note ?
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Certain⋅es parlent d'une dizaine d'alias par note.
|
Certains parlent d'une dizaine d'alias par note.
|
||||||
|
|
||||||
Sois conscient⋅e qu'ajouter des alias ne peut qu'augmenter la probabilité de
|
Sois conscient⋅e qu'ajouter des alias ne peut qu'augmenter la probabilité de
|
||||||
collisions avec une autre note, et peut aussi retarder la livraison de ta commande
|
collisions avec une autre note, et peut aussi retarder la livraison de ta
|
||||||
lors d'une perm bouffe.
|
commande lors d'un perm bouffe.
|
||||||
|
|
||||||
|
|
||||||
Je suis trésorièr⋅e d'un club, qu'ai-je le droit de faire ?
|
Je suis trésorier d'un club, qu'ai-je le droit de faire ?
|
||||||
-----------------------------------------------------------
|
---------------------------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Être trésorièr⋅e d'un club donne la responsabilité de gérer la trésorerie du club, et
|
Être trésorier d'un club donne la responsabilité de gérer la trésorerie du
|
||||||
donc de gérer sa note. Vous obtenez donc le droit d'effectuer n'importe quelle
|
club, et donc de gérer sa note. Vous obtenez donc le droit d'effectuer
|
||||||
transaction via la note en provenance ou à destination de la note de votre club. Vous
|
n'importe quelle transaction via la note en provenance ou à destination de
|
||||||
pouvez également gérer les adhésions de votre club, en permettant à n'importe quel⋅le
|
la note de votre club. Vous pouvez également gérer les adhésions de votre club,
|
||||||
adhérent⋅e BDE de rejoindre votre club, en prélevant d'éventuels frais d'adhésion. Les
|
en permettant à n'importe quel adhérent BDE de rejoindre votre club, en prélevant
|
||||||
paramètres du club peuvent être également modifiés.
|
d'éventuels frais d'adhésion. Les paramètres du club peuvent être également modifiés.
|
||||||
|
|
||||||
.. danger::
|
.. danger::
|
||||||
Avoir des droits sur la Note Kfet ne signifie pas que vous devez les utiliser. Chaque
|
Avoir des droits sur la Note Kfet ne signifie pas que vous devez les utiliser.
|
||||||
opération nécessitant des droits doit être fait pour une bonne raison, et doit avoir
|
Chaque opération nécessitant des droits doit être fait pour une bonne raison,
|
||||||
un lien avec votre club. Vous n'avez par exemple pas le droit d'aller récupérer des
|
et doit avoir un lien avec votre club. Vous n'avez par exemple pas le droit
|
||||||
informations personnelles d'adhérent⋅es pour une raison personnelle. En revanche,
|
d'aller récupérer des informations personnelles d'adhérents pour une raison
|
||||||
faire le lien entre nom/prénom et nom de note est bien sûr permis pour faciliter des
|
personnelle. En revanche, faire le lien entre nom/prénom et nom de note est
|
||||||
transferts. Tout abus de droits constaté pourra mener à des sanctions prises par le
|
bien sûr permis pour faciliter des transferts. Tout abus de droits constaté
|
||||||
bureau du BDE.
|
pourra mener à des sanctions prises par le bureau du BDE.
|
||||||
|
|
||||||
|
|
||||||
Je suis trésorièr⋅e d'un club, je n'arrive pas à voir le solde du club / faire des transactions
|
Je suis trésorier d'un club, je n'arrive pas à voir le solde du club / faire des transactions
|
||||||
-----------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
As-tu bien vérifié que tu t'es connecté⋅e initialement avec tous tes droits ? Sinon,
|
As-tu bien vérifié que tu t'es connecté⋅e initialement avec tous tes droits ?
|
||||||
si tes droits sont tout récents, tu dois te déconnecter et te reconnecter pour que tes
|
Sinon, si tes droits sont tout récents, tu dois te déconnecter et te reconnecter
|
||||||
droits soient bien pris en compte.
|
pour que tes droits soient bien pris en compte.
|
||||||
|
|
||||||
La Note permet de se connecter avec différents filtres de permission afin de pouvoir
|
La Note permet de se connecter avec différents filtres de permission afin de
|
||||||
prêter son ordinateur avec une session ouverte pour faire quelques opérations en
|
pouvoir prêter son ordinateur avec une session ouverte pour faire quelques
|
||||||
empêchant l'accès à des opérations trop sensibles.
|
opérations en empêchant l'accès à des opérations trop sensibles.
|
||||||
|
|
||||||
|
|
||||||
Je suis trésorièr⋅e d'un club. Puis-je créer un bouton ?
|
Je suis trésorier d'un club. Puis-je créer un bouton ?
|
||||||
--------------------------------------------------------
|
------------------------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Oui bien sûr ! Tant qu'il redirige bien vers la note de ton club.
|
Oui bien sûr ! Tant qu'il redirige bien vers la note de ton club.
|
||||||
|
|
||||||
Pour cela, rends-toi à la page `</note/buttons/>`_ pour afficher la liste des boutons,
|
Pour cela, rends-toi à la page `</note/buttons/>`_ pour afficher la liste des
|
||||||
puis tu auras accès à l'interface pour créer un bouton. Une fois le bouton créé, il
|
boutons, puis tu auras accès à l'interface pour créer un bouton. Une fois le
|
||||||
apparaîtra dans l'onglet ``Consommations``.
|
bouton créé, il apparaîtra dans l'onglet ``Consommations``.
|
||||||
|
|
||||||
Il faut noter que tant qu'il n'y a pas de boutons visibles pour ton club, tu n'auras
|
Il faut noter que tant qu'il n'y a pas de boutons visibles pour ton club, tu
|
||||||
pas accès à l'interface de consommations, et tu devras nécessairement cliquer sur le
|
n'auras pas accès à l'interface de consommations, et tu devras nécessairement
|
||||||
lien ci-dessus pour accéder à l'interface d'édition des boutons. Une fois qu'un bouton
|
cliquer sur le lien ci-dessus pour accéder à l'interface d'édition des boutons.
|
||||||
pour ton club est visible, l'interface consommations devient accessible.
|
Une fois qu'un bouton pour ton club est visible, l'interface consommations
|
||||||
|
devient accessible.
|
||||||
|
|
||||||
|
|
||||||
Après passation, je suis trésorièr⋅e d'un club. Comment récupérer mes droits note ?
|
Après passation, je suis trésorier d'un club. Comment récupérer mes droits note ?
|
||||||
-----------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Tu dois pour cela contacter les trésorièr⋅es BDE (voir ci-dessous). Iels vous
|
Tu dois pour cela contacter les trésoriers BDE (voir ci-dessous). Ils vous
|
||||||
expliqueront en détails vos droits et vos interdits et vous donneront les droits
|
expliqueront en détails vos droits et vos interdits et vous donneront les
|
||||||
requis.
|
droits requis.
|
||||||
|
|
||||||
|
|
||||||
Je souhaite contacter un⋅e trésorièr⋅e
|
Je souhaite contacter un trésorier
|
||||||
--------------------------------------
|
----------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Pour contacter un⋅e trésorièr⋅e, il te suffit d'envoyer un mail à l'adresse
|
Pour contacter un trésorier, il te suffit d'envoyer un mail à l'adresse
|
||||||
`tresorerie.bde@lists.crans.org <tresorerie.bde@lists.crans.org>`_. Pense bien à
|
`tresorerie.bde@lists.crans.org <tresorerie.bde@lists.crans.org>`_. Pense bien
|
||||||
donner ton nom de note, voire à envoyer un scan de ta carte d'identité si ta demande
|
à donner ton nom de note, voire à envoyer un scan de ta carte d'identité si ta
|
||||||
concerne un virement entre le compte du BDE et ton propre compte.
|
demande concerne un virement entre le compte du BDE et ton propre compte.
|
||||||
|
|
||||||
|
|
||||||
J'ai trouvé un bug, comment le signaler ?
|
J'ai trouvé un bug, comment le signaler ?
|
||||||
-----------------------------------------
|
-----------------------------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
La Note Kfet est développée bénévolement par des membres du BDE. Nous mettons tous nos
|
La Note Kfet est développée bénévolement par des membres du BDE. Malgré tous nos
|
||||||
efforts pour fournir une plateforme sans erreur et la plus ergonomique possible.
|
efforts pour fournir une plateforme sans erreur et la plus ergonomique possible.
|
||||||
Toutefois, il n'est évidemment pas exclu que des bugs soient présents :)
|
Toutefois, il n'est évidemment pas exclu que des bugs soient présents :)
|
||||||
|
|
||||||
|
@ -154,44 +156,41 @@ Je souhaite contribuer
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
La Note Kfet est essentiellement développée par des responsables informatiques du BDE
|
La Note Kfet est essentiellement développée par des responsables informatiques du
|
||||||
de l'ENS Paris-Saclay. Toutefois, si vous souhaitez contribuer, vous pouvez bien sûr
|
BDE de l'ENS Paris-Saclay. Toutefois, si vous souhaitez contribuer, vous pouvez
|
||||||
le faire en accord avec la licence GPLv3 avec laquelle la Note Kfet est distribuée.
|
bien sûr le faire en accord avec la licence GPLv3 avec laquelle la Note Kfet est
|
||||||
Pour cela, si vous êtes adhérent⋅e Crans, vous pouvez proposer une demande de fusion
|
distribuée. Pour cela, si vous êtes adhérent Crans, vous pouvez proposer une
|
||||||
de votre code. Si ce n'est pas le cas, vous pouvez envoyer un mail à
|
demande de fusion de votre code. Si ce n'est pas le cas, vous pouvez envoyer un
|
||||||
`notekfet2020@lists.crans.org <notekfet2020@lists.crans.org>`_. Dans les deux cas,
|
mail à `notekfet2020@lists.crans.org <notekfet2020@lists.crans.org>`_.
|
||||||
merci de rejoindre le canal ``#note`` sur IRC :)
|
Dans les deux cas, merci de rejoindre le canal ``#note`` sur IRC :)
|
||||||
|
|
||||||
|
|
||||||
Contributeur⋅rices
|
Contributeurs
|
||||||
------------------
|
-------------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
La version 2020 de la Note Kfet a été développée sous le mandat de la
|
La version 2020 de la Note Kfet a été développée sous le mandat de la
|
||||||
Saper[list]popette. Son développement a commencé à l'été 2019. Après un mois de beta à
|
Saper[list]popette. Son développement a commencé à l'été 2019. Après un mois de beta
|
||||||
l'été 2020, son déploiement en production s'est fait le samedi 5 septembre 2020.
|
à l'été 2020, son déploiement en production s'est fait le samedi 5 septembre 2020.
|
||||||
|
|
||||||
Elle succède à presque 6 années de la
|
Elle succède à presque 6 années de la
|
||||||
`Note Kfet 2015 <https://wiki.crans.org/NoteKfet/NoteKfet2015>`_, alors en production
|
`Note Kfet 2015 <https://wiki.crans.org/NoteKfet/NoteKfet2015>`_, alors en production
|
||||||
depuis le 6 octobre 2014.
|
depuis le 6 octobre 2014.
|
||||||
|
|
||||||
Liste des contributeur⋅rices majeur⋅es, par ordre alphabétique :
|
Liste des contributeurs majeurs, par ordre alphabétique :
|
||||||
|
|
||||||
* bleizi
|
* Pierre-André « PAC » COMBY
|
||||||
* erdnaxe
|
* Yohann « ÿnérant » D'ANELLO
|
||||||
* esum
|
* Benjamin « esum » GRAILLOT
|
||||||
* korenst1
|
* Alexandre « erdnaxe » IOOSS
|
||||||
* nicomarg
|
|
||||||
* PAC
|
|
||||||
* ÿnérant
|
|
||||||
|
|
||||||
|
|
||||||
Hébergement
|
Hébergement
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
En accord entre le BDE de l'ENS Paris-Saclay et le Crans, l'instance de production
|
En accord entre de l'ENS Paris-Saclay et le Crans, l'instance de production présente
|
||||||
présente sur `<https://note.crans.org/>`_ est hébergée sur l'un des serveurs du Crans.
|
sur `<https://note.crans.org/>`_ est hébergée sur l'un des serveurs du Crans.
|
||||||
Les données sont hébergées à l'adresse :
|
Les données sont hébergées à l'adresse :
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
La note, c'est quoi ?
|
La note, c'est quoi ?
|
||||||
=====================
|
=====================
|
||||||
|
|
||||||
La Note Kfet est un porte-monnaie virtuel proposé gratuitement à toustes les
|
La Note Kfet est un porte-monnaie virtuel proposé gratuitement à tous les adhérents BDE.
|
||||||
adhérent⋅es BDE. C'est le moyen de paiement privilégié au sein du campus, que ce soit
|
C'est le moyen de paiement privilégié au sein du campus, que ce soit pour payer des
|
||||||
pour payer des activités du BDE, ou bien pour faire des remboursements entre ami⋅es.
|
activités du BDE, ou bien pour faire des remboursements entre amis. La note contient
|
||||||
La note contient également la base d'adhérent⋅es du BDE et contient de nombreux outils
|
également la base d'adhérents du BDE et contient de nombreux outils facilitant la vie
|
||||||
facilitant la vie des différents bureaux de clubs, en particulier les trésorièr⋅es et
|
des différents bureaux de clubs, en particulier les trésoriers et surtout les trésoriers
|
||||||
surtout les trésorièr⋅es BDE.
|
BDE.
|
||||||
|
|
||||||
La Note Kfet est accessible à l'adresse `<https://note.crans.org>`_. La version actuelle
|
La Note Kfet est accessible à l'adresse `<https://note.crans.org>`_. La version actuelle
|
||||||
a été développée par le BDE 2020-2021, et est maintenue par les respos infos du BDE de
|
a été développée par le BDE 2020-2021, et est maintenue par les respos infos du BDE de
|
||||||
|
@ -23,7 +23,7 @@ Chaque adhérent⋅e BDE dispose d'un compte appelé « note », créé lors de
|
||||||
Une note est associée à un solde en euros, et à un pseudo appelé « nom de note ».
|
Une note est associée à un solde en euros, et à un pseudo appelé « nom de note ».
|
||||||
Le solde associé à la note correspond au solde de l'adhérent⋅e, avec lequel il est
|
Le solde associé à la note correspond au solde de l'adhérent⋅e, avec lequel il est
|
||||||
possible de payer tout ce qui est en lien avec le BDE. Le nom de note suffit à
|
possible de payer tout ce qui est en lien avec le BDE. Le nom de note suffit à
|
||||||
identifier une personne, et il suffit par exemple de donner ce nom à un⋅e permanencièr⋅e
|
identifier une personne, et il suffit par exemple de donner ce nom à un permanencier
|
||||||
à la Kfet pour acheter un produit.
|
à la Kfet pour acheter un produit.
|
||||||
|
|
||||||
Faire une transaction
|
Faire une transaction
|
||||||
|
@ -34,10 +34,10 @@ d'un⋅e autre adhérent⋅e, pourvu que le montant de la transaction n'excède
|
||||||
propre solde. Pour cela, il suffit d'aller sur la page de transferts, qui est la
|
propre solde. Pour cela, il suffit d'aller sur la page de transferts, qui est la
|
||||||
page par défaut après connexion : `<https://note.crans.org/note/transfer/>`_
|
page par défaut après connexion : `<https://note.crans.org/note/transfer/>`_
|
||||||
Le formulaire pour effectuer un transfert apparaît alors. En cliquant sur le bouton
|
Le formulaire pour effectuer un transfert apparaît alors. En cliquant sur le bouton
|
||||||
« Je suis l'émetteur⋅rice », le champ « Émetteur⋅rices » est directement complété
|
« Je suis l'émetteur », le champ « Émetteurs » est directement complété avec votre
|
||||||
avec votre propre note. Il vous suffit alors de remplir le champ « Destinataires »
|
propre note. Il vous suffit alors de remplir le champ « Destinataires » avec le ou
|
||||||
avec le⋅s nom⋅s de note que vous voulez créditer, de spécifier le montant et la raison
|
les noms de note que vous voulez créditer, de spécifier le montant et la raison de
|
||||||
de votre transfert, puis de cliquer sur le bouton « Virement ». Après quelques secondes,
|
votre transfert, puis de cliquer sur le bouton « Virement ». Après quelques secondes,
|
||||||
si votre solde est suffisant et que la transaction est acceptée, un message de
|
si votre solde est suffisant et que la transaction est acceptée, un message de
|
||||||
confirmation apparaîtra et la transaction apparaîtra dans l'historique ci-dessous.
|
confirmation apparaîtra et la transaction apparaîtra dans l'historique ci-dessous.
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ Consulter ses données personnelles
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
La Note Kfet sert non seulement à faire des transactions d'argent virtuel au sein
|
La Note Kfet sert non seulement à faire des transactions d'argent virtuel au sein
|
||||||
du BDE, mais aussi à permettre au BDE et à ses clubs de gérer leurs adhérent⋅es.
|
du BDE, mais aussi à permettre au BDE et à ses clubs de gérer leurs adhérents.
|
||||||
À cet effet, diverses informations personnelles sont collectées par la Note.
|
À cet effet, diverses informations personnelles sont collectées par la Note.
|
||||||
|
|
||||||
Pour accéder à votre compte, une fois connecté⋅e, rendez-vous dans le menu en haut
|
Pour accéder à votre compte, une fois connecté⋅e, rendez-vous dans le menu en haut
|
||||||
|
@ -75,26 +75,26 @@ présentes sont :
|
||||||
* Département
|
* Département
|
||||||
* Promotion
|
* Promotion
|
||||||
* Adresse
|
* Adresse
|
||||||
* Élève/étudiant⋅e
|
* Élève/étudiant
|
||||||
* Inscription aux listes de diffusion du BDE, du BDA et du BDS
|
* Inscription aux listes de diffusion du BDE, du BDA et du BDS
|
||||||
|
|
||||||
Les trois premières informations sont obligatoires pour pouvoir vous contacter
|
Les trois premières informations sont obligatoires pour pouvoir vous contacter
|
||||||
et pouvoir tenir un registre d'adhérent⋅es.
|
et pouvoir tenir un registre d'adhérent.
|
||||||
|
|
||||||
Le numéro de téléphone et l'adresse ne sont utilisés uniquement en cas d'urgence,
|
Le numéro de téléphone et l'adresse ne sont utilisés uniquement en cas d'urgence,
|
||||||
notamment en cas de WEI, et ne sont pas des champs obligatoires. De plus,
|
notamment en cas de WEI, et ne sont pas des champs obligatoires. De plus,
|
||||||
promotion, département et section ne sont utilisés sérieusement que en cas
|
promotion, département et section ne sont utilisés sérieusement que en cas
|
||||||
de WEI, et ne sont pas utilisés sinon.
|
de WEI, et ne sont pas utilisés sinon.
|
||||||
|
|
||||||
La distinction élève/étudiant⋅e permet de distinguer les achats qui peuvent avoir
|
La distinction élève/étudiant permet de distinguer les achats qui peuvent avoir
|
||||||
des prix différents pour les élèves et pour les étudiant⋅es, notamment en cas de
|
des prix différents pour les élèves et pour les étudiants, notamment en cas de
|
||||||
transferts d'argent, de WEI ou d'adhésion à certains clubs.
|
transferts d'argent, de WEI ou d'adhésion à certains clubs.
|
||||||
|
|
||||||
Hormis le pseudo, toutes ces informations ont un caractère confidentiel qui les
|
Hormis le pseudo, toutes ces informations ont un caractère confidentiel qui les
|
||||||
rend privées et inaccessible aux utilisateur⋅rices. Les trésorièr⋅es BDE et les respos
|
rend privées et inaccessible aux utilisateurs. Les trésoriers BDE et les respos
|
||||||
info ont accès à toutes les informations (en cas de besoin uniquement, le cas
|
info ont accès à toutes les informations (en cas de besoin uniquement, le cas
|
||||||
contraire est considéré comme de l'abus de droit qui est punissable), et les
|
contraire est considéré comme de l'abus de droit qui est punissable), et les
|
||||||
trésorièr⋅es de club ont accès au nom et au prénom afin de faciliter l'association
|
trésoriers de club ont accès au nom et au prénom afin de faciliter l'association
|
||||||
à un nom de note.
|
à un nom de note.
|
||||||
|
|
||||||
Dans la partie informations personnelles, il est également possible de modifier
|
Dans la partie informations personnelles, il est également possible de modifier
|
||||||
|
@ -106,7 +106,7 @@ de nom de note.
|
||||||
Le tableau des adhésions actives sur la partie du haut contient l'ensemble des
|
Le tableau des adhésions actives sur la partie du haut contient l'ensemble des
|
||||||
clubs auxquels vous êtes adhérent⋅es, avec les dates de début, de fin, votre
|
clubs auxquels vous êtes adhérent⋅es, avec les dates de début, de fin, votre
|
||||||
rôle au sein du club (généralement simple membre, mais cela peut également
|
rôle au sein du club (généralement simple membre, mais cela peut également
|
||||||
être trésorièr⋅e par exemple) ainsi que la cotisation que vous avez éventuellement
|
être trésorier⋅ère par exemple) ainsi que la cotisation que vous avez éventuellement
|
||||||
payée au club.
|
payée au club.
|
||||||
|
|
||||||
L'historique des transactions contient l'ensemble des transactions qui ont fait
|
L'historique des transactions contient l'ensemble des transactions qui ont fait
|
||||||
|
|
|
@ -3,7 +3,7 @@ Documentation de la Note Kfet 2020
|
||||||
|
|
||||||
|
|
||||||
Bienvenue sur la documentation de la Note Kfet 2020. Cette documentation est à la fois
|
Bienvenue sur la documentation de la Note Kfet 2020. Cette documentation est à la fois
|
||||||
destinée aux adhérent⋅es BDE pour découvrir le fonctionnement de la note, mais aussi aux
|
destinée aux adhérents BDE pour découvrir le fonctionnement de la note, mais aussi aux
|
||||||
respos info qui souhaitent découvrir comment fonctionne la note sous le capot.
|
respos info qui souhaitent découvrir comment fonctionne la note sous le capot.
|
||||||
|
|
||||||
Des informations complémentaires sont également disponibles sur le `Wiki Crans <https://wiki.crans.org/NoteKfet/NoteKfet2020/>`_.
|
Des informations complémentaires sont également disponibles sur le `Wiki Crans <https://wiki.crans.org/NoteKfet/NoteKfet2020/>`_.
|
||||||
|
|
|
@ -82,7 +82,7 @@ Pour cela, on peut simplement faire :
|
||||||
$ source env/bin/activate
|
$ source env/bin/activate
|
||||||
(env) $
|
(env) $
|
||||||
|
|
||||||
À noter que ``source`` peut s'abréger par ``.`` uniquement.
|
À noter que ``source`` peut s'abbréger par ``.`` uniquement.
|
||||||
|
|
||||||
Vous êtes donc dans un environnement virtuel Python. Pour installer les dépendances
|
Vous êtes donc dans un environnement virtuel Python. Pour installer les dépendances
|
||||||
de la note :
|
de la note :
|
||||||
|
@ -162,8 +162,8 @@ Ouvrez votre navigateur, tapez `<http://localhost:8000/>`_, enjoy :)
|
||||||
optimisé pour recevoir des requêtes en parallèle ou être utilisé en production.
|
optimisé pour recevoir des requêtes en parallèle ou être utilisé en production.
|
||||||
|
|
||||||
|
|
||||||
Créer un⋅e super-utilisateur⋅rice
|
Créer un super-utilisateur
|
||||||
---------------------------------
|
--------------------------
|
||||||
|
|
||||||
La commande ``./manage.py createsuperuser`` vous permettra de créer un⋅e
|
La commande ``./manage.py createsuperuser`` vous permettra de créer un super-utilisateur
|
||||||
super-utilisateur⋅rice initial⋅e.
|
initial.
|
||||||
|
|
|
@ -15,7 +15,7 @@ version 1.11, qui n'est plus maintenue par Django depuis déjà quelques mois.
|
||||||
Les versions stables de Django sont les versions 2.2 et 3.2, Debian Bullseye
|
Les versions stables de Django sont les versions 2.2 et 3.2, Debian Bullseye
|
||||||
utilisant la version 2.2.
|
utilisant la version 2.2.
|
||||||
|
|
||||||
Afin de permettre à ses utilisateur⋅rices d'utiliser les dernières fonctionnalités de
|
Afin de permettre à ses utilisateurs d'utiliser les dernières fonctionnalités de
|
||||||
certains paquets, Debian a créé une distribution particulière, appelée
|
certains paquets, Debian a créé une distribution particulière, appelée
|
||||||
``buster-backports``. Cette distribution contient des paquets de la distribution
|
``buster-backports``. Cette distribution contient des paquets de la distribution
|
||||||
à venir « ``testing`` » (``bullseye`` à l'heure où cette documentation est écrite)
|
à venir « ``testing`` » (``bullseye`` à l'heure où cette documentation est écrite)
|
||||||
|
@ -187,7 +187,7 @@ clé sous forme de chaîne de caractère suffisamment longue (64 caractères par
|
||||||
qui n'est pas à transmettre et qui évite d'autres sites malveillants de faire des requêtes
|
qui n'est pas à transmettre et qui évite d'autres sites malveillants de faire des requêtes
|
||||||
directement sur la note.
|
directement sur la note.
|
||||||
|
|
||||||
Le champ ``CONTACT_EMAIL`` correspond l'adresse mail que les adhérent⋅es peuvent contacter
|
Le champ ``CONTACT_EMAIL`` correspond l'adresse mail que les adhérent⋅e⋅s peuvent contacter
|
||||||
en cas de problème. C'est là où le champ ``Nous contacter`` redirigera.
|
en cas de problème. C'est là où le champ ``Nous contacter`` redirigera.
|
||||||
|
|
||||||
Le champ ``NOTE_URL`` correspond au nom de domaine autorisé à accéder au site. C'est également
|
Le champ ``NOTE_URL`` correspond au nom de domaine autorisé à accéder au site. C'est également
|
||||||
|
@ -636,5 +636,5 @@ On peut enfin redémarrer le serveur Web. Les données ont bien été copiées.
|
||||||
|
|
||||||
.. caution::
|
.. caution::
|
||||||
|
|
||||||
On ne copiera **jamais** des données d'adhérent⋅es sur une machine personnelle.
|
On ne copiera **jamais** des données d'adhérent⋅e⋅s sur une machine personnelle.
|
||||||
Ce type d'opération doit s'effectuer impérativement entre des serveurs du BDE.
|
Ce type d'opération doit s'effectuer impérativement entre des serveurs du BDE.
|
||||||
|
|
|
@ -139,8 +139,8 @@ de diffusion utiles.
|
||||||
Il prend 2 options :
|
Il prend 2 options :
|
||||||
|
|
||||||
* ``--type``, qui prend en argument ``members`` (défaut), ``clubs``, ``events``, ``art``,
|
* ``--type``, qui prend en argument ``members`` (défaut), ``clubs``, ``events``, ``art``,
|
||||||
``sport``, qui permet respectivement de sortir la liste des adresses mails des adhérent⋅es
|
``sport``, qui permet respectivement de sortir la liste des adresses mails des adhérents
|
||||||
actuel⋅les (pour la liste ``adherents.bde@lists.crans.org), des clubs (pour
|
actuels (pour la liste ``adherents.bde@lists.crans.org), des clubs (pour
|
||||||
``clubs@lists.crans.org``), des personnes à abonner à ``evenements@lists.crans.org``,
|
``clubs@lists.crans.org``), des personnes à abonner à ``evenements@lists.crans.org``,
|
||||||
à ``all.bda@lists.crans.org`` et enfin à ``bds@lists.crans.org``.
|
à ``all.bda@lists.crans.org`` et enfin à ``bds@lists.crans.org``.
|
||||||
* ``--lang``, qui prend en argument ``fr`` ou ``en``. N'est utile que pour la ML événements,
|
* ``--lang``, qui prend en argument ``fr`` ou ``en``. N'est utile que pour la ML événements,
|
||||||
|
@ -157,8 +157,8 @@ malheureusement pas aussi simple que de simplement supposer que ces listes sont
|
||||||
À terme, il pourrait être envisageable de synchroniser automatiquement les listes avec la note.
|
À terme, il pourrait être envisageable de synchroniser automatiquement les listes avec la note.
|
||||||
|
|
||||||
|
|
||||||
Suppression d'un⋅e utilisateur⋅rice
|
Suppression d'un utilisateur
|
||||||
-----------------------------------
|
----------------------------
|
||||||
|
|
||||||
Le script s'appelle ``force_delete_user``.
|
Le script s'appelle ``force_delete_user``.
|
||||||
|
|
||||||
|
@ -167,7 +167,7 @@ Le script s'appelle ``force_delete_user``.
|
||||||
Ce script est dangereux. À n'utiliser qu'avec de très grosses pincettes si vous savez
|
Ce script est dangereux. À n'utiliser qu'avec de très grosses pincettes si vous savez
|
||||||
ce que vous faites. Seul cas d'usage pour l'instant recensé : supprimer des comptes en
|
ce que vous faites. Seul cas d'usage pour l'instant recensé : supprimer des comptes en
|
||||||
double qui se sont malencontreusement retrouvés validés pour raison de Sogé et de mauvaise
|
double qui se sont malencontreusement retrouvés validés pour raison de Sogé et de mauvaise
|
||||||
communication au sein des trésorièr⋅es.
|
communication au sein des trésorier⋅ère⋅s.
|
||||||
|
|
||||||
Il n'est certainement pas prévu de supprimer des vrais comptes existants via ce script.
|
Il n'est certainement pas prévu de supprimer des vrais comptes existants via ce script.
|
||||||
On ne supprime pas l'historique. Si jamais quelqu'un demanderait à supprimer son compte,
|
On ne supprime pas l'historique. Si jamais quelqu'un demanderait à supprimer son compte,
|
||||||
|
@ -175,8 +175,8 @@ Le script s'appelle ``force_delete_user``.
|
||||||
|
|
||||||
Ce script est utile lorsqu'il faut supprimer un compte créer par erreur. Tant que la validation
|
Ce script est utile lorsqu'il faut supprimer un compte créer par erreur. Tant que la validation
|
||||||
n'est pas faite, il suffit en général de cliquer sur le bouton « Supprimer le compte » sur
|
n'est pas faite, il suffit en général de cliquer sur le bouton « Supprimer le compte » sur
|
||||||
l'interface de validation. Cela supprimera l'utilisateur⋅rice et le profil associé, sans
|
l'interface de validation. Cela supprimera l'utilisateur et le profil associé, sans toucher
|
||||||
toucher à une quelconque note puisqu'elle ne sera pas créée.
|
à une quelconque note puisqu'elle ne sera pas créée.
|
||||||
|
|
||||||
Ce script supprime donc un compte ainsi que toutes les données associées (note, alias,
|
Ce script supprime donc un compte ainsi que toutes les données associées (note, alias,
|
||||||
transactions). Il n'est donc pas à prendre à la légère, et vous devez savoir ce que vous
|
transactions). Il n'est donc pas à prendre à la légère, et vous devez savoir ce que vous
|
||||||
|
@ -192,8 +192,8 @@ pour supprimer tout ce qu'il faut, et une validation manuelle sera requise pour
|
||||||
la suppression. L'option ``--doit`` évite cette confirmation manuelle.
|
la suppression. L'option ``--doit`` évite cette confirmation manuelle.
|
||||||
**Vous n'avez jamais à utiliser cette option en théorie.**
|
**Vous n'avez jamais à utiliser cette option en théorie.**
|
||||||
|
|
||||||
À la fin du processus, un mail est envoyé aux administrateur⋅rices pour les prévenir des
|
À la fin du processus, un mail est envoyé aux administrateurs pour les prévenir des
|
||||||
éléments supprimés.
|
élements supprimés.
|
||||||
|
|
||||||
Des données réelles jamais tu ne supprimeras.
|
Des données réelles jamais tu ne supprimeras.
|
||||||
|
|
||||||
|
@ -210,18 +210,18 @@ la Note Kfet 2015.
|
||||||
s'est déroulé.
|
s'est déroulé.
|
||||||
|
|
||||||
|
|
||||||
Ajouter un⋅e super-utilisateur⋅rice
|
Ajouter un super-utilisateur
|
||||||
-----------------------------------
|
----------------------------
|
||||||
|
|
||||||
Le script s'appelle ``make_su``.
|
Le script s'appelle ``make_su``.
|
||||||
|
|
||||||
Il prend en argument un pseudo.
|
Il prend en argument un pseudo.
|
||||||
|
|
||||||
Avec l'option ``--SUPER, -S``, la personne avec ce pseudo devient super-utilisateur⋅rice,
|
Avec l'option ``--SUPER, -S``, la personne avec ce pseudo devient super-utilisateur,
|
||||||
et obtiens donc les pleins pouvoirs sur la note. À ne donner qu'aux respos info.
|
et obtiens donc les pleins pouvoirs sur la note. À ne donner qu'aux respos info.
|
||||||
|
|
||||||
Avec l'option ``--STAFF, -s``, la personne avec ce pseudo acquiert le statut équipe,
|
Avec l'option ``--STAFF, -s``, la personne avec ce pseudo acquiert le statut équipe,
|
||||||
et obtient l'accès à django-admin. À ne donner qu'aux respos info.
|
et obtiens l'accès à django-admin. À ne donner qu'aux respos info.
|
||||||
|
|
||||||
|
|
||||||
Rafraîchissement des activités
|
Rafraîchissement des activités
|
||||||
|
@ -264,20 +264,20 @@ Envoi des rappels de négatif
|
||||||
|
|
||||||
Le script s'appelle ``send_mail_to_negative_balances``.
|
Le script s'appelle ``send_mail_to_negative_balances``.
|
||||||
|
|
||||||
Il sert à rappeler aux adhérent⋅es et clubs en négatif qui le sont, mais également
|
Il sert à rappeler aux adhérent⋅e⋅s et clubs en négatif qu'ils le sont, mais également
|
||||||
à envoyer aux trésorièr⋅es et respos info la liste des adhérent⋅es en négatif.
|
à envoyer aux trésorier⋅ère⋅s et respos info la liste des adhérent⋅e⋅s en négatif.
|
||||||
|
|
||||||
Il prend les options suivantes :
|
Il prend les options suivantes :
|
||||||
|
|
||||||
* ``--spam, -s`` : envoie à chaque adhérent⋅e en négatif un rappel par mail pour recharger
|
* ``--spam, -s`` : envoie à chaque adhérent⋅e en négatif un rappel par mail pour recharger
|
||||||
* ``--report, -r`` : envoie le rapport aux trésorièr⋅es et respos info
|
* ``--report, -r`` : envoie le rapport aux trésorier⋅ère⋅s et respos info
|
||||||
* ``--negative-amount,-n`` : définit le solde maximal en-dessous duquel les notes
|
* ``--negative-amount,-n`` : définit le solde maximal en-dessous duquel les notes
|
||||||
apparaitront sur le rapport / seront spammées
|
apparaitront sur le rapport / seront spammées
|
||||||
* ``--add-years, -y`` : ajoute également les adhérent⋅es des ``n`` dernières années
|
* ``--add-years, -y`` : ajoute également les adhérent⋅e⋅s des ``n`` dernières années
|
||||||
|
|
||||||
Ce script est appelé tous les mardis à 5h00 pour spammer les utilisateur⋅ices en
|
Ce script est appelé tous les mardis à 5h00 pour spammer les utilisateur⋅rice⋅s en
|
||||||
négatif et tous les 6 du mois pour envoyer le rapport des notes d'adhérent⋅es ou de
|
négatif et tous les 6 du mois pour envoyer le rapport des notes d'adhérent⋅e⋅s ou de
|
||||||
vieilleux adhérent⋅es de moins d'un an sous -10 €.
|
vieux/vieilles adhérent⋅e⋅s de moins d'un an sous -10 €.
|
||||||
|
|
||||||
|
|
||||||
Envoi des rapports
|
Envoi des rapports
|
||||||
|
@ -285,8 +285,8 @@ Envoi des rapports
|
||||||
|
|
||||||
Le script s'appelle ``send_reports``.
|
Le script s'appelle ``send_reports``.
|
||||||
|
|
||||||
Les utilisateur⋅rices ont la possibilité de recevoir sur demande un rapport à la
|
Les utilisateurs ont la possibilité de recevoir sur demande un rapport à la fréquence de
|
||||||
fréquence de leur choix (en jours) des transactions effectuées sur leur note.
|
leur choix (en jours) des transactions effectuées sur leur note.
|
||||||
|
|
||||||
Le script prend 2 options :
|
Le script prend 2 options :
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,9 +9,9 @@ msgstr ""
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-10-07 09:07+0200\n"
|
"POT-Creation-Date: 2022-10-07 09:07+0200\n"
|
||||||
"PO-Revision-Date: 2020-11-16 20:21+0000\n"
|
"PO-Revision-Date: 2020-11-16 20:21+0000\n"
|
||||||
"Last-Translator: Emmy D'ANELLO <ynerant@crans.org>\n"
|
"Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n"
|
||||||
"Language-Team: German <http://translate.ynerant.fr/projects/nk20/nk20-js/de/>"
|
"Language-Team: German <http://translate.ynerant.fr/projects/nk20/nk20-js/de/"
|
||||||
"\n"
|
">\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue