mirror of https://gitlab.crans.org/bde/nk20
Merge branch 'beta' into migration-django-4-2
This commit is contained in:
commit
48a5b04579
|
@ -1,3 +1,3 @@
|
||||||
[submodule "apps/scripts"]
|
[submodule "apps/scripts"]
|
||||||
path = apps/scripts
|
path = apps/scripts
|
||||||
url = https://gitlab.crans.org/bde/nk20-scripts.git
|
url = https://gitlab.crans.org/bde/nk20-scripts
|
||||||
|
|
|
@ -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 utilisateur initial
|
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Enjoy :
|
6. Enjoy :
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'activity.apps.ActivityConfig'
|
default_app_config = 'activity.apps.ActivityConfig'
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from 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
|
from .models import Activity, ActivityType, Entry, Guest, Opener
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Activity, site=admin_site)
|
@admin.register(Activity, site=admin_site)
|
||||||
|
@ -45,3 +45,11 @@ 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,9 +1,11 @@
|
||||||
# Copyright (C) 2018-2021 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
|
from ..models import Activity, ActivityType, Entry, Guest, GuestTransaction, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTypeSerializer(serializers.ModelSerializer):
|
class ActivityTypeSerializer(serializers.ModelSerializer):
|
||||||
|
@ -59,3 +61,17 @@ 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-2021 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
|
from .views import ActivityTypeViewSet, ActivityViewSet, EntryViewSet, GuestViewSet, OpenerViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_activity_urls(router, path):
|
def register_activity_urls(router, path):
|
||||||
|
@ -12,3 +12,4 @@ 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,12 +1,15 @@
|
||||||
# Copyright (C) 2018-2021 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.filters import SearchFilter
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer
|
from .serializers import ActivitySerializer, ActivityTypeSerializer, EntrySerializer, GuestSerializer, OpenerSerializer
|
||||||
from ..models import Activity, ActivityType, Entry, Guest
|
from ..models import Activity, ActivityType, Entry, Guest, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
class ActivityTypeViewSet(ReadProtectedModelViewSet):
|
||||||
|
@ -29,7 +32,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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
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',
|
||||||
|
@ -47,7 +50,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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
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',
|
||||||
|
@ -62,7 +65,36 @@ class EntryViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Entry.objects.order_by('id')
|
queryset = Entry.objects.order_by('id')
|
||||||
serializer_class = EntrySerializer
|
serializer_class = EntrySerializer
|
||||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
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)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
@ -44,7 +44,7 @@ class ActivityForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Activity
|
model = Activity
|
||||||
exclude = ('creater', 'valid', 'open', )
|
exclude = ('creater', 'valid', 'open', 'opener', )
|
||||||
widgets = {
|
widgets = {
|
||||||
"organizer": Autocomplete(
|
"organizer": Autocomplete(
|
||||||
model=Club,
|
model=Club,
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.28 on 2024-03-23 13:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('activity', '0002_auto_20200904_2341'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='activity',
|
||||||
|
name='description',
|
||||||
|
field=models.TextField(blank=True, default='', verbose_name='description'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,28 @@
|
||||||
|
# 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
|
from note.models import NoteUser, Transaction, Note
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,6 +66,8 @@ class Activity(models.Model):
|
||||||
|
|
||||||
description = models.TextField(
|
description = models.TextField(
|
||||||
verbose_name=_('description'),
|
verbose_name=_('description'),
|
||||||
|
blank=True,
|
||||||
|
default="",
|
||||||
)
|
)
|
||||||
|
|
||||||
location = models.CharField(
|
location = models.CharField(
|
||||||
|
@ -308,3 +310,31 @@ 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))
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* On form submit, add a new opener
|
||||||
|
*/
|
||||||
|
function form_create_opener (e) {
|
||||||
|
// Do not submit HTML form
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// Get data and send to API
|
||||||
|
const formData = new FormData(e.target)
|
||||||
|
$.getJSON('/api/note/alias/'+formData.get('opener') + '/',
|
||||||
|
function (opener_alias) {
|
||||||
|
create_opener(formData.get('activity'), opener_alias.note)
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an opener between an activity and a user
|
||||||
|
* @param activity:Integer activity id
|
||||||
|
* @param opener:Integer user note id
|
||||||
|
*/
|
||||||
|
function create_opener(activity, opener) {
|
||||||
|
$.post('/api/activity/opener/', {
|
||||||
|
activity: activity,
|
||||||
|
opener: opener,
|
||||||
|
csrfmiddlewaretoken: CSRF_TOKEN
|
||||||
|
}).done(function () {
|
||||||
|
// Reload tables
|
||||||
|
$('#opener_table').load(location.pathname + ' #opener_table')
|
||||||
|
addMsg(gettext('Opener successfully added'), 'success')
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On click of "delete", delete the opener
|
||||||
|
* @param button_id:Integer Opener id to remove
|
||||||
|
*/
|
||||||
|
function delete_button (button_id) {
|
||||||
|
$.ajax({
|
||||||
|
url: '/api/activity/opener/' + button_id + '/',
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
||||||
|
}).done(function () {
|
||||||
|
addMsg(gettext('Opener successfully deleted'), 'success')
|
||||||
|
$('#opener_table').load(location.pathname + ' #opener_table')
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
// Attach event
|
||||||
|
document.getElementById('form_opener').addEventListener('submit', form_create_opener)
|
||||||
|
})
|
|
@ -1,15 +1,17 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
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
|
from .models import Activity, Entry, Guest, Opener
|
||||||
|
|
||||||
|
|
||||||
class ActivityTable(tables.Table):
|
class ActivityTable(tables.Table):
|
||||||
|
@ -113,3 +115,34 @@ 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,11 +4,31 @@ 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">
|
||||||
|
@ -22,6 +42,8 @@ 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({
|
||||||
|
|
|
@ -17,4 +17,27 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
var date_end = document.getElementById("id_date_end");
|
||||||
|
var date_start = document.getElementById("id_date_start");
|
||||||
|
|
||||||
|
function update_date_end (){
|
||||||
|
if(date_end.value=="" || date_end.value<date_start.value){
|
||||||
|
date_end.value = date_start.value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function update_date_start (){
|
||||||
|
if(date_start.value=="" || date_end.value<date_start.value){
|
||||||
|
date_start.value = date_end.value;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
date_start.addEventListener('focusout', update_date_end);
|
||||||
|
date_end.addEventListener('focusout', update_date_start);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -46,4 +46,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</h3>
|
</h3>
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
@ -17,14 +17,16 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.views import View
|
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_tables2.views import SingleTableView
|
from django.views.generic.list import ListView
|
||||||
|
from django_tables2.views import MultiTableMixin, SingleTableMixin
|
||||||
|
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
|
from .models import Activity, Entry, Guest, Opener
|
||||||
from .tables import ActivityTable, EntryTable, GuestTable
|
from .tables import ActivityTable, EntryTable, GuestTable, OpenerTable
|
||||||
|
|
||||||
|
|
||||||
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
|
@ -57,26 +59,36 @@ class ActivityCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, ListView):
|
||||||
"""
|
"""
|
||||||
Displays all Activities, and classify if they are on-going or upcoming ones.
|
Displays all Activities, and classify if they are on-going or upcoming ones.
|
||||||
"""
|
"""
|
||||||
model = Activity
|
model = Activity
|
||||||
table_class = ActivityTable
|
tables = [
|
||||||
ordering = ('-date_start',)
|
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_data(self):
|
||||||
|
# first table = all activities, second table = upcoming
|
||||||
|
return [
|
||||||
|
self.get_queryset().order_by("-date_start"),
|
||||||
|
Activity.objects.filter(date_end__gt=timezone.now())
|
||||||
|
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))
|
||||||
|
.distinct()
|
||||||
|
.order_by("date_start")
|
||||||
|
]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
tables = context["tables"]
|
||||||
context['upcoming'] = ActivityTable(
|
for name, table in zip(["table", "upcoming"], tables):
|
||||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
context[name] = table
|
||||||
prefix='upcoming-',
|
|
||||||
)
|
|
||||||
|
|
||||||
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||||
context["started_activities"] = started_activities
|
context["started_activities"] = started_activities
|
||||||
|
@ -84,7 +96,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Shows details about one activity. Add guest to context
|
Shows details about one activity. Add guest to context
|
||||||
"""
|
"""
|
||||||
|
@ -92,15 +104,40 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
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()
|
||||||
|
|
||||||
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
tables = context["tables"]
|
||||||
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
|
for name, table in zip(["guests", "opener"], tables):
|
||||||
context["guests"] = table
|
context[name] = 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -157,12 +194,14 @@ 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, TemplateView):
|
class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, 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),
|
||||||
|
@ -197,13 +236,16 @@ class ActivityEntryView(LoginRequiredMixin, 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] != "^":
|
|
||||||
pattern = "^" + pattern
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
valid_regex = is_regex(pattern)
|
||||||
|
suffix = "__iregex" if valid_regex else "__istartswith"
|
||||||
|
pattern = "^" + pattern if valid_regex and pattern[0] != "^" else pattern
|
||||||
guest_qs = guest_qs.filter(
|
guest_qs = guest_qs.filter(
|
||||||
Q(first_name__iregex=pattern)
|
Q(**{f"first_name{suffix}": pattern})
|
||||||
| Q(last_name__iregex=pattern)
|
| Q(**{f"last_name{suffix}": pattern})
|
||||||
| Q(inviter__alias__name__iregex=pattern)
|
| Q(**{f"inviter__alias__name{suffix}": pattern})
|
||||||
| Q(inviter__alias__normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"inviter__alias__normalized_name{suffix}": Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
guest_qs = guest_qs.none()
|
guest_qs = guest_qs.none()
|
||||||
|
@ -235,11 +277,15 @@ class ActivityEntryView(LoginRequiredMixin, 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(note__noteuser__user__first_name__iregex=pattern)
|
Q(**{f"note__noteuser__user__first_name{suffix}": pattern})
|
||||||
| Q(note__noteuser__user__last_name__iregex=pattern)
|
| Q(**{f"note__noteuser__user__last_name{suffix}": pattern})
|
||||||
| Q(name__iregex=pattern)
|
| Q(**{f"name{suffix}": pattern})
|
||||||
| Q(normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"normalized_name{suffix}": Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
note_qs = note_qs.none()
|
note_qs = note_qs.none()
|
||||||
|
@ -251,15 +297,9 @@ class ActivityEntryView(LoginRequiredMixin, 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_context_data(self, **kwargs):
|
def get_table_data(self):
|
||||||
"""
|
|
||||||
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 = []
|
||||||
|
|
||||||
|
@ -272,8 +312,17 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
||||||
note.activity = activity
|
note.activity = activity
|
||||||
matched.append(note)
|
matched.append(note)
|
||||||
|
|
||||||
table = EntryTable(data=matched)
|
return 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)
|
||||||
|
|
||||||
|
@ -315,8 +364,8 @@ X-WR-CALNAME:Kfet Calendar
|
||||||
NAME:Kfet Calendar
|
NAME:Kfet Calendar
|
||||||
CALSCALE:GREGORIAN
|
CALSCALE:GREGORIAN
|
||||||
BEGIN:VTIMEZONE
|
BEGIN:VTIMEZONE
|
||||||
TZID:Europe/Berlin
|
TZID:Europe/Paris
|
||||||
X-LIC-LOCATION:Europe/Berlin
|
X-LIC-LOCATION:Europe/Paris
|
||||||
BEGIN:DAYLIGHT
|
BEGIN:DAYLIGHT
|
||||||
TZOFFSETFROM:+0100
|
TZOFFSETFROM:+0100
|
||||||
TZOFFSETTO:+0200
|
TZOFFSETTO:+0200
|
||||||
|
@ -338,10 +387,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;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_start)}
|
DTSTART:{"{:%Y%m%dT%H%M%S}Z".format(activity.date_start)}
|
||||||
DTEND;TZID=Europe/Berlin:{"{:%Y%m%dT%H%M%S}".format(activity.date_end)}
|
DTEND:{"{:%Y%m%dT%H%M%S}Z".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) + """
|
DESCRIPTION;CHARSET=UTF-8:""" + self.multilines(activity.description.replace("\n", "\\n"), 75, 26) + f"""
|
||||||
-- {activity.organizer.name}
|
-- {activity.organizer.name}
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'api.apps.APIConfig'
|
default_app_config = 'api.apps.APIConfig'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
import re
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from rest_framework.filters import SearchFilter
|
||||||
|
|
||||||
|
|
||||||
|
class RegexSafeSearchFilter(SearchFilter):
|
||||||
|
@lru_cache
|
||||||
|
def validate_regex(self, search_term) -> bool:
|
||||||
|
try:
|
||||||
|
re.compile(search_term)
|
||||||
|
return True
|
||||||
|
except re.error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_search_fields(self, view, request):
|
||||||
|
"""
|
||||||
|
Ensure that given regex are valid.
|
||||||
|
If not, we consider that the user is trying to search by substring.
|
||||||
|
"""
|
||||||
|
search_fields = super().get_search_fields(view, request)
|
||||||
|
search_terms = self.get_search_terms(request)
|
||||||
|
|
||||||
|
for search_term in search_terms:
|
||||||
|
if not self.validate_regex(search_term):
|
||||||
|
# Invalid regex. We assume we don't query by regex but by substring.
|
||||||
|
search_fields = [f.replace('$', '') for f in search_fields]
|
||||||
|
break
|
||||||
|
|
||||||
|
return search_fields
|
||||||
|
|
||||||
|
def get_search_terms(self, request):
|
||||||
|
"""
|
||||||
|
Ensure that search field is a valid regex query. If not, we remove extra characters.
|
||||||
|
"""
|
||||||
|
terms = super().get_search_terms(request)
|
||||||
|
if not all(self.validate_regex(term) for term in terms):
|
||||||
|
# Invalid regex. If a ^ is prefixed to the search term, we remove it.
|
||||||
|
terms = [term[1:] if term[0] == '^' else term for term in terms]
|
||||||
|
# Same for dollars.
|
||||||
|
terms = [term[:-1] if term[-1] == '$' else term for term in terms]
|
||||||
|
return terms
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
@ -12,11 +12,12 @@ 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
|
||||||
|
|
||||||
|
@ -87,7 +88,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 SearchFilter in backends:
|
if RegexSafeSearchFilter 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)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
|
@ -1,19 +1,29 @@
|
||||||
# Copyright (C) 2018-2021 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.
|
||||||
|
@ -60,34 +70,38 @@ 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
|
||||||
note__alias__name__iregex="^" + pattern
|
Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match with normalization
|
# Match with normalization
|
||||||
Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match on lower pattern
|
# Match on lower pattern
|
||||||
Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
# Match on firstname or lastname
|
# Match on firstname or lastname
|
||||||
(Q(last_name__iregex="^" + pattern) | Q(first_name__iregex="^" + pattern))
|
(Q(**{f"last_name{suffix}": prefix + pattern}) | Q(**{f"first_name{suffix}": prefix + pattern}))
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + pattern.lower())
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + pattern.lower()})
|
||||||
& ~Q(note__alias__normalized_name__iregex="^" + Alias.normalize(pattern))
|
& ~Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
& ~Q(note__alias__name__iregex="^" + pattern)
|
& ~Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
),
|
),
|
||||||
all=True,
|
all=True,
|
||||||
)
|
)
|
||||||
|
@ -107,6 +121,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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['id', 'app_label', 'model', ]
|
filterset_fields = ['id', 'app_label', 'model', ]
|
||||||
search_fields = ['$app_label', '$model', ]
|
search_fields = ['$app_label', '$model', ]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'logs.apps.LogsConfig'
|
default_app_config = 'logs.apps.LogsConfig'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import ChangelogViewSet
|
from .views import ChangelogViewSet
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
previous = instance._previous
|
previous = instance._previous
|
||||||
|
|
||||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e 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 dans la VM doit être un des alias note du respo info
|
# IMPORTANT : l'utilisateur⋅rice 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 utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e 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 dans la VM doit être un des alias note du respo info
|
# IMPORTANT : l'utilisateur⋅rice 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)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'member.apps.MemberConfig'
|
default_app_config = 'member.apps.MemberConfig'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import OrderingFilter, SearchFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
|
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
|
||||||
|
@ -17,7 +18,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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
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',
|
||||||
|
@ -34,7 +35,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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
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', ]
|
||||||
|
@ -49,7 +50,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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, OrderingFilter, RegexSafeSearchFilter]
|
||||||
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',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
from cas_server.auth import DjangoAuthUser # pragma: no cover
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
@ -139,6 +139,9 @@ class ImageForm(forms.Form):
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
return super().is_valid() or super().clean().get('image') is None
|
||||||
|
|
||||||
|
|
||||||
class ClubForm(forms.ModelForm):
|
class ClubForm(forms.ModelForm):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -152,7 +155,7 @@ class ClubForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Club
|
model = Club
|
||||||
fields = '__all__'
|
exclude = ("add_registration_form",)
|
||||||
widgets = {
|
widgets = {
|
||||||
"membership_fee_paid": AmountInput(),
|
"membership_fee_paid": AmountInput(),
|
||||||
"membership_fee_unpaid": AmountInput(),
|
"membership_fee_unpaid": AmountInput(),
|
||||||
|
@ -208,9 +211,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 est remplacé par un champ d'auto-complétion.
|
# Le champ d'utilisateur⋅rice 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 valides
|
# et récupère les noms d'utilisateur⋅rices valides
|
||||||
widgets = {
|
widgets = {
|
||||||
'user':
|
'user':
|
||||||
Autocomplete(
|
Autocomplete(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.28 on 2024-07-15 09:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('member', '0011_profile_vss_charter_read'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='club',
|
||||||
|
name='add_registration_form',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='add to registration form'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -259,6 +259,11 @@ class Club(models.Model):
|
||||||
help_text=_('Maximal date of a membership, after which members must renew it.'),
|
help_text=_('Maximal date of a membership, after which members must renew it.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_registration_form = models.BooleanField(
|
||||||
|
verbose_name=_("add to registration form"),
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("club")
|
verbose_name = _("club")
|
||||||
verbose_name_plural = _("clubs")
|
verbose_name_plural = _("clubs")
|
||||||
|
@ -290,7 +295,14 @@ class Club(models.Model):
|
||||||
|
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
|
|
||||||
while (today - self.membership_start).days >= 365:
|
# Avoid any problems on February 29
|
||||||
|
if self.membership_start.month == 2 and self.membership_start.day == 29:
|
||||||
|
self.membership_start -= datetime.timedelta(days=1)
|
||||||
|
if self.membership_end.month == 2 and self.membership_end.day == 29:
|
||||||
|
self.membership_end += datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
while today >= datetime.date(self.membership_start.year + 1,
|
||||||
|
self.membership_start.month, self.membership_start.day):
|
||||||
if self.membership_start:
|
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)
|
||||||
|
@ -468,10 +480,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 BDE") | Q(name="Membre de club")).all())
|
Role.objects.filter(Q(name="Adhérent⋅e 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 Kfet") | Q(name="Membre de club")).all())
|
Role.objects.filter(Q(name="Adhérent⋅e 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()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
|
@ -14,6 +14,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<form method="post" enctype="multipart/form-data" id="formUpload">
|
<form method="post" enctype="multipart/form-data" id="formUpload">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form |crispy }}
|
{{ form |crispy }}
|
||||||
|
{% if user.note.display_image != "pic/default.png" %}
|
||||||
|
<input type="submit" class="btn btn-primary" value="{% trans "Remove" %}">
|
||||||
|
{% endif %}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<!-- MODAL TO CROP THE IMAGE -->
|
<!-- MODAL TO CROP THE IMAGE -->
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
|
@ -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ésorier·ère de club") | Q(name="Bureau de club")).all()],
|
Q(name="Membre de club") | Q(name="Trésorièr⋅e 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()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
|
@ -16,8 +16,9 @@ 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 SingleTableView
|
from django_tables2.views import MultiTableMixin, SingleTableMixin, 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
|
||||||
|
@ -219,16 +220,20 @@ 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(
|
||||||
username__iregex="^" + pattern
|
Q(**{f"username{suffix}": prefix + pattern})
|
||||||
).union(
|
).union(
|
||||||
qs.filter(
|
qs.filter(
|
||||||
(Q(alias__iregex="^" + pattern)
|
(Q(**{f"alias{suffix}": prefix + pattern})
|
||||||
| Q(normalized_alias__iregex="^" + Alias.normalize(pattern))
|
| Q(**{f"normalized_alias{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
| Q(last_name__iregex="^" + pattern)
|
| Q(**{f"last_name{suffix}": prefix + pattern})
|
||||||
| Q(first_name__iregex="^" + pattern)
|
| Q(**{f"first_name{suffix}": prefix + pattern})
|
||||||
| Q(email__istartswith=pattern))
|
| Q(email__istartswith=pattern))
|
||||||
& ~Q(username__iregex="^" + pattern)
|
& ~Q(**{f"username{suffix}": prefix + pattern})
|
||||||
), all=True)
|
), all=True)
|
||||||
else:
|
else:
|
||||||
qs = qs.none()
|
qs = qs.none()
|
||||||
|
@ -243,7 +248,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, MultiTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View and manage user trust relationships
|
View and manage user trust relationships
|
||||||
"""
|
"""
|
||||||
|
@ -252,13 +257,25 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
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
|
|
||||||
context["trusting"] = TrustTable(
|
tables = context["tables"]
|
||||||
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
for name, table in zip(["trusting", "trusted_by"], tables):
|
||||||
context["trusted_by"] = TrustedTable(
|
context[name] = table
|
||||||
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
|
||||||
|
@ -277,7 +294,7 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View and manage user aliases.
|
View and manage user aliases.
|
||||||
"""
|
"""
|
||||||
|
@ -286,12 +303,15 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
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="",
|
||||||
|
@ -326,12 +346,15 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
|
||||||
"""Save image to note"""
|
"""Save image to note"""
|
||||||
image = form.cleaned_data['image']
|
image = form.cleaned_data['image']
|
||||||
|
|
||||||
# Rename as a PNG or GIF
|
if image is None:
|
||||||
extension = image.name.split(".")[-1]
|
image = "pic/default.png"
|
||||||
if extension == "gif":
|
|
||||||
image.name = "{}_pic.gif".format(self.object.note.pk)
|
|
||||||
else:
|
else:
|
||||||
image.name = "{}_pic.png".format(self.object.note.pk)
|
# Rename as a PNG or GIF
|
||||||
|
extension = image.name.split(".")[-1]
|
||||||
|
if extension == "gif":
|
||||||
|
image.name = "{}_pic.gif".format(self.object.note.pk)
|
||||||
|
else:
|
||||||
|
image.name = "{}_pic.png".format(self.object.note.pk)
|
||||||
|
|
||||||
# Save
|
# Save
|
||||||
self.object.note.display_image = image
|
self.object.note.display_image = image
|
||||||
|
@ -407,10 +430,15 @@ 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(name__iregex=pattern)
|
Q(**{f"name{suffix}": prefix + pattern})
|
||||||
| Q(note__alias__name__iregex=pattern)
|
| Q(**{f"note__alias__name{suffix}": prefix + pattern})
|
||||||
| Q(note__alias__normalized_name__iregex=Alias.normalize(pattern))
|
| Q(**{f"note__alias__normalized_name{suffix}": prefix + Alias.normalize(pattern)})
|
||||||
)
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
@ -507,7 +535,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
Manage aliases of a club.
|
Manage aliases of a club.
|
||||||
"""
|
"""
|
||||||
|
@ -516,11 +544,16 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
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="",
|
||||||
|
@ -824,8 +857,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||||
|
|
||||||
ret = super().form_valid(form)
|
ret = super().form_valid(form)
|
||||||
|
|
||||||
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
|
member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
|
||||||
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | 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 == "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:
|
||||||
|
@ -861,7 +894,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 Kfet") | Q(name="Membre de club")).all())
|
membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
@ -909,10 +942,15 @@ 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(user__first_name__iregex='^' + pattern)
|
Q(**{f"user__first_name{suffix}": prefix + pattern})
|
||||||
| Q(user__last_name__iregex='^' + pattern)
|
| Q(**{f"user__last_name{suffix}": prefix + pattern})
|
||||||
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
|
| Q(**{f"user__note__alias__normalized_name{suffix}": prefix + 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,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'note.apps.NoteConfig'
|
default_app_config = 'note.apps.NoteConfig'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
# Copyright (C) 2018-2021 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, SearchFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
from rest_framework import viewsets
|
from rest_framework import status, viewsets
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from api.filters import RegexSafeSearchFilter
|
||||||
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, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter, 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,10 +48,14 @@ 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(alias__name__iregex="^" + alias)
|
Q(**{f"alias__name{suffix}": alias_prefix + alias})
|
||||||
| Q(alias__normalized_name__iregex="^" + Alias.normalize(alias))
|
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||||
| Q(alias__normalized_name__iregex="^" + alias.lower())
|
| Q(**{f"alias__normalized_name{suffix}": alias_prefix + alias.lower()})
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset.order_by("id")
|
return queryset.order_by("id")
|
||||||
|
@ -65,7 +69,7 @@ class TrustViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
queryset = Trust.objects
|
queryset = Trust.objects
|
||||||
serializer_class = TrustSerializer
|
serializer_class = TrustSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, 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']
|
||||||
|
@ -91,11 +95,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/aliases/
|
then render it on /api/note/alias/
|
||||||
"""
|
"""
|
||||||
queryset = Alias.objects
|
queryset = Alias.objects
|
||||||
serializer_class = AliasSerializer
|
serializer_class = AliasSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, 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', ]
|
||||||
|
@ -126,18 +130,22 @@ 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(
|
||||||
name__iregex="^" + alias
|
**{f"name{suffix}": alias_prefix + alias}
|
||||||
).union(
|
).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
Q(**{f"normalized_name{suffix}": alias_prefix + Alias.normalize(alias)})
|
||||||
& ~Q(name__iregex="^" + alias)
|
& ~Q(**{f"name{suffix}": alias_prefix + alias})
|
||||||
),
|
),
|
||||||
all=True).union(
|
all=True).union(
|
||||||
queryset.filter(
|
queryset.filter(
|
||||||
Q(normalized_name__iregex="^" + alias.lower())
|
Q(**{f"normalized_name{suffix}": "^" + alias.lower()})
|
||||||
& ~Q(normalized_name__iregex="^" + Alias.normalize(alias))
|
& ~Q(**{f"normalized_name{suffix}": "^" + Alias.normalize(alias)})
|
||||||
& ~Q(name__iregex="^" + alias)
|
& ~Q(**{f"name{suffix}": "^" + alias})
|
||||||
),
|
),
|
||||||
all=True)
|
all=True)
|
||||||
|
|
||||||
|
@ -147,7 +155,7 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
||||||
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
queryset = Alias.objects
|
queryset = Alias.objects
|
||||||
serializer_class = ConsumerSerializer
|
serializer_class = ConsumerSerializer
|
||||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
filter_backends = [RegexSafeSearchFilter, 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', ]
|
||||||
|
@ -166,11 +174,7 @@ 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
|
||||||
try:
|
valid_regex = is_regex(alias)
|
||||||
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')
|
||||||
|
@ -198,7 +202,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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['name', 'templates', 'templates__name']
|
filterset_fields = ['name', 'templates', 'templates__name']
|
||||||
search_fields = ['$name', '$templates__name', ]
|
search_fields = ['$name', '$templates__name', ]
|
||||||
|
|
||||||
|
@ -211,7 +215,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 = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, 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', ]
|
||||||
|
@ -225,7 +229,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 = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [RegexSafeSearchFilter, 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',
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ 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 = [
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
|
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
// Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
// When a transaction is performed, lock the interface to prevent spam clicks.
|
// When a transaction is performed, lock the interface to prevent spam clicks.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import html
|
import html
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
Par ailleurs, le BDE ne sert pas d'alcool aux adhérent⋅es dont le solde
|
||||||
est inférieur à 0 € depuis plus de 24h.
|
est inférieur à 0 €.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -43,4 +43,4 @@
|
||||||
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
||||||
</p>
|
</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -9,7 +9,7 @@ Ce mail t'a été envoyé parce que le solde de ta Note Kfet
|
||||||
|
|
||||||
Ton solde actuel est de {{ note.balance|pretty_money }}.
|
Ton solde actuel est de {{ note.balance|pretty_money }}.
|
||||||
|
|
||||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
Par ailleurs, le BDE ne sert pas d'alcool aux adhérent·e·s dont le solde
|
||||||
est inférieur à 0 € depuis plus de 24h.
|
est inférieur à 0 € depuis plus de 24h.
|
||||||
|
|
||||||
Si tu ne comprends pas ton solde, tu peux consulter ton historique
|
Si tu ne comprends pas ton solde, tu peux consulter ton historique
|
||||||
|
@ -22,4 +22,4 @@ virement bancaire.
|
||||||
--
|
--
|
||||||
Le BDE
|
Le BDE
|
||||||
|
|
||||||
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django import template
|
from django import template
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from api.tests import TestAPI
|
from api.tests import TestAPI
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
@ -13,6 +13,7 @@ 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
|
||||||
|
@ -89,11 +90,15 @@ 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(name__iregex=pattern)
|
Q(**{f"name{suffix}": pattern})
|
||||||
| Q(destination__club__name__iregex=pattern)
|
| Q(**{f"destination__club__name{suffix}": pattern})
|
||||||
| Q(category__name__iregex=pattern)
|
| Q(**{f"category__name{suffix}": pattern})
|
||||||
| Q(description__iregex=pattern)
|
| Q(**{f"description{suffix}": pattern})
|
||||||
)
|
)
|
||||||
|
|
||||||
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
||||||
|
@ -223,7 +228,10 @@ 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"]:
|
||||||
transactions = transactions.filter(reason__iregex=data["reason"])
|
# Check if this is a valid regex. If not, we won't check regex
|
||||||
|
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,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'permission.apps.PermissionConfig'
|
default_app_config = 'permission.apps.PermissionConfig'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import PermissionViewSet, RoleViewSet
|
from .views import PermissionViewSet, RoleViewSet
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.filters import SearchFilter
|
from api.filters import RegexSafeSearchFilter
|
||||||
|
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||||
|
|
||||||
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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
|
filterset_fields = ['model', 'type', 'query', 'mask', 'field', 'permanent', ]
|
||||||
search_fields = ['$model__name', '$query', '$description', ]
|
search_fields = ['$model__model', '$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, SearchFilter]
|
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
|
||||||
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', ]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 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 sys
|
import sys
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": true,
|
"permanent": true,
|
||||||
"description": "Voir son compte utilisateur"
|
"description": "Voir son compte utilisateur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": true,
|
"permanent": true,
|
||||||
"description": "Voir sa propre note d'utilisateur"
|
"description": "Voir sa propre note d'utilisateur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -116,7 +116,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir les aliases des notes des clubs et des adhérents du club BDE"
|
"description": "Voir les alias des notes des clubs et des adhérent⋅es du club BDE"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -772,7 +772,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir les adhérents du club"
|
"description": "Voir les adhérent⋅es du club"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -788,7 +788,7 @@
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Ajouter un membre à un club"
|
"description": "Ajouter un⋅e membre à un club"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -852,7 +852,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier n'importe quel utilisateur"
|
"description": "Modifier n'importe quel⋅le utilisateur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -868,7 +868,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Ajouter un utilisateur"
|
"description": "Ajouter un⋅e utilisateur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1284,7 +1284,7 @@
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Inscrire un 1A au WEI"
|
"description": "Inscrire un⋅e 1A au WEI"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1956,7 +1956,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": true,
|
"permanent": true,
|
||||||
"description": "Voir mes activitées passées, même après la fin de l'adhésion BDE"
|
"description": "Voir mes activités passées, même après la fin de l'adhésion BDE"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2100,7 +2100,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir n'importe quel utilisateur"
|
"description": "Voir n'importe quel⋅le utilisateur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2228,7 +2228,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Créer une note d'utilisateur"
|
"description": "Créer une note d'utilisateur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2276,7 +2276,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir tous les adhérents de tous les clubs"
|
"description": "Voir toustes les adhérent⋅es de tous les clubs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2292,7 +2292,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Ajouter un membre à n'importe quel club"
|
"description": "Ajouter un⋅e membre à n'importe quel club"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2372,7 +2372,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "name",
|
"field": "name",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier le nom d'une activité non validée dont on est l'auteur"
|
"description": "Modifier le nom d'une activité non validée dont on est l'auteur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2388,7 +2388,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "description",
|
"field": "description",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier la description d'une activité non validée dont on est l'auteur"
|
"description": "Modifier la description d'une activité non validée dont on est l'auteur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2404,7 +2404,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "location",
|
"field": "location",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier le lieu d'une activité non validée dont on est l'auteur"
|
"description": "Modifier le lieu d'une activité non validée dont on est l'auteur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2420,7 +2420,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "activity_type",
|
"field": "activity_type",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier le type d'une activité non validée dont on est l'auteur"
|
"description": "Modifier le type d'une activité non validée dont on est l'auteur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2436,7 +2436,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "organizer",
|
"field": "organizer",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier l'organisateur d'une activité non validée dont on est l'auteur"
|
"description": "Modifier l'organisateur d'une activité non validée dont on est l'auteur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2452,7 +2452,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "attendees_club",
|
"field": "attendees_club",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier le club attendu d'une activité non validée dont on est l'auteur"
|
"description": "Modifier le club attendu d'une activité non validée dont on est l'auteur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2468,7 +2468,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "date_start",
|
"field": "date_start",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier la date de début d'une activité non validée dont on est l'auteur"
|
"description": "Modifier la date de début d'une activité non validée dont on est l'auteur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2484,7 +2484,7 @@
|
||||||
"mask": 1,
|
"mask": 1,
|
||||||
"field": "date_end",
|
"field": "date_end",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier la date de fin d'une activité non validée dont on est l'auteur"
|
"description": "Modifier la date de fin d'une activité non validée dont on est l'auteur⋅rice"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2591,12 +2591,12 @@
|
||||||
"note",
|
"note",
|
||||||
"transaction"
|
"transaction"
|
||||||
],
|
],
|
||||||
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}, {\"valid\": false}]",
|
"query": "[\"OR\", {\"source__balance__gte\": 0}, [\"AND\", [\"NOT\", {\"recurrenttransaction__template__category__name\": \"Alcool\"}], {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 2000]}}], {\"valid\": false}]",
|
||||||
"type": "add",
|
"type": "add",
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Créer une transaction quelconque tant que la source reste au-dessus de -20 €"
|
"description": "Créer une transaction quelconque tant que la source reste positive s'il s'agit d'alcool, sinon au-dessus de -20€"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2756,7 +2756,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Modifier n'importe quel utilisateur non encore inscrit"
|
"description": "Modifier n'importe quel⋅le utilisateur⋅rice non encore inscrit⋅e"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2788,7 +2788,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir tous les alias, y compris ceux des non adhérents"
|
"description": "Voir tous les alias, y compris ceux des non adhérent⋅es"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2820,7 +2820,7 @@
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir n'importe quel utilisateur non encore inscrit"
|
"description": "Voir n'importe quel⋅le utilisateur⋅rice non encore inscrit⋅e"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -2847,12 +2847,12 @@
|
||||||
"auth",
|
"auth",
|
||||||
"user"
|
"user"
|
||||||
],
|
],
|
||||||
"query": "{\"memberships__club__name\": \"BDE\", \"memberships__roles__name\": \"Adhérent BDE\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
|
"query": "{\"memberships__club__name\": \"BDE\", \"memberships__roles__name\": \"Adhérent⋅e BDE\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
|
||||||
"type": "view",
|
"type": "view",
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir n'importe quel utilisateur qui est adhérent BDE"
|
"description": "Voir n'importe quel⋅le utilisateur⋅rice qui est adhérent⋅e BDE"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3044,7 +3044,7 @@
|
||||||
"mask": 3,
|
"mask": 3,
|
||||||
"field": "",
|
"field": "",
|
||||||
"permanent": false,
|
"permanent": false,
|
||||||
"description": "Voir toutes les amitiés, y compris celles des non adhérents"
|
"description": "Voir toutes les amitiés, y compris celles des non adhérent⋅es"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -3111,12 +3111,205 @@
|
||||||
"description": "Voir ceux nous ayant pour ami, pour toujours"
|
"description": "Voir ceux nous ayant pour ami, pour toujours"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 199,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"activity"
|
||||||
|
],
|
||||||
|
"query": "{\"opener__in\": [\"user\", \"note\", \"activity_responsible\", [\"all\"]], \"open\": true, \"activity_type__manage_entries\":true}",
|
||||||
|
"type": "view",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Voir les activités ouvertes dont l'utilisateur⋅rice est ouvreur⋅se"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 200,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"activity"
|
||||||
|
],
|
||||||
|
"query": "{\"opener__in\": [\"user\", \"note\", \"activity_responsible\", [\"all\"]], \"open\": true, \"activity_type__manage_entries\":true}",
|
||||||
|
"type": "change",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "open",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Fermer les activités ouvertes dont l'utilisateur⋅rice est ouvreur⋅se"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 201,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"entry"
|
||||||
|
],
|
||||||
|
"query": "{\"activity__opener__in\": [\"user\", \"note\", \"activity_responsible\", [\"all\"]], \"activity__open\": true, \"activity__activity_type__manage_entries\":true}",
|
||||||
|
"type": "add",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Faire les entrées des activités ouvertes dont l'utilisateur⋅rice est ouvreur⋅se"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 202,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"entry"
|
||||||
|
],
|
||||||
|
"query": "{\"activity__opener__in\": [\"user\", \"note\", \"activity_responsible\", [\"all\"]]}",
|
||||||
|
"type": "view",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Voir les entrées des activités dont l'utilisateur⋅rice est ouvreur⋅se"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 203,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"guest"
|
||||||
|
],
|
||||||
|
"query": "{\"activity__opener__in\": [\"user\", \"note\", \"activity_responsible\", [\"all\"]]}",
|
||||||
|
"type": "view",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Voir les invité⋅es des activités dont l'utilisateur⋅rice est ouvreur⋅se"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 204,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"guesttransaction"
|
||||||
|
],
|
||||||
|
"query": "[\"NOT\", {\"pk__isnull\": [\"user\", \"note\", \"activity_responsible\", [\"filter\", {\"activity__open\": true, \"activity__activity_type__manage_entries\":true}], [\"exists\"]]}]",
|
||||||
|
"type": "add",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Créer une transaction d'invitation lorsque l'utilisateur⋅rice est ouvreur⋅se d'une activité ouverte"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 205,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"note",
|
||||||
|
"specialtransaction"
|
||||||
|
],
|
||||||
|
"query": "[\"NOT\", {\"pk__isnull\": [\"user\", \"note\", \"activity_responsible\", [\"filter\", {\"activity__open\": true, \"activity__activity_type__manage_entries\":true}], [\"exists\"]]}]",
|
||||||
|
"type": "add",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Créer un crédit ou un retrait quelconque lorsque l'utilisateur⋅rice est ouvreur⋅se d'une activité ouverte"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 206,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"note",
|
||||||
|
"notespecial"
|
||||||
|
],
|
||||||
|
"query": "[\"NOT\", {\"pk__isnull\": [\"user\", \"note\", \"activity_responsible\", [\"filter\", {\"activity__open\": true, \"activity__activity_type__manage_entries\":true}], [\"exists\"]]}]",
|
||||||
|
"type": "view",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Afficher l'interface crédit/retrait lorsque l'utilisateur⋅rice est ouvreur⋅se d'une activité ouverte"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 207,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"opener"
|
||||||
|
],
|
||||||
|
"query": "{}",
|
||||||
|
"type": "view",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Voir les ouvreur⋅ses des activités"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 208,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"opener"
|
||||||
|
],
|
||||||
|
"query": "{}",
|
||||||
|
"type": "add",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Ajouter des ouvreur⋅ses aux activités"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 209,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"opener"
|
||||||
|
],
|
||||||
|
"query": "{}",
|
||||||
|
"type": "delete",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Supprimer des ouvreur⋅ses aux activités"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"model": "permission.permission",
|
||||||
|
"pk": 210,
|
||||||
|
"fields": {
|
||||||
|
"model": [
|
||||||
|
"activity",
|
||||||
|
"activity"
|
||||||
|
],
|
||||||
|
"query": "{}",
|
||||||
|
"type": "change",
|
||||||
|
"mask": 2,
|
||||||
|
"field": "opener",
|
||||||
|
"permanent": false,
|
||||||
|
"description": "Voir le tableau des ouvreur⋅ses"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"model": "permission.role",
|
"model": "permission.role",
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": 1,
|
"for_club": 1,
|
||||||
"name": "Adh\u00e9rent BDE",
|
"name": "Adh\u00e9rent\u22c5e BDE",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
|
@ -3148,11 +3341,19 @@
|
||||||
187,
|
187,
|
||||||
188,
|
188,
|
||||||
189,
|
189,
|
||||||
190,
|
190,
|
||||||
191,
|
191,
|
||||||
195,
|
195,
|
||||||
196,
|
196,
|
||||||
198
|
198,
|
||||||
|
199,
|
||||||
|
200,
|
||||||
|
201,
|
||||||
|
202,
|
||||||
|
203,
|
||||||
|
204,
|
||||||
|
205,
|
||||||
|
206
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3161,7 +3362,7 @@
|
||||||
"pk": 2,
|
"pk": 2,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": 2,
|
"for_club": 2,
|
||||||
"name": "Adh\u00e9rent Kfet",
|
"name": "Adh\u00e9rent\u22c5e Kfet",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
22,
|
22,
|
||||||
36,
|
36,
|
||||||
|
@ -3225,10 +3426,11 @@
|
||||||
"pk": 5,
|
"pk": 5,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": null,
|
"for_club": null,
|
||||||
"name": "Pr\u00e9sident\u00b7e de club",
|
"name": "Pr\u00e9sident\u22c5e de club",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
62,
|
62,
|
||||||
142
|
142,
|
||||||
|
135
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3237,7 +3439,7 @@
|
||||||
"pk": 6,
|
"pk": 6,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": null,
|
"for_club": null,
|
||||||
"name": "Tr\u00e9sorier\u00b7\u00e8re de club",
|
"name": "Tr\u00e9sorièr\u22c5e de club",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
19,
|
19,
|
||||||
20,
|
20,
|
||||||
|
@ -3261,7 +3463,7 @@
|
||||||
"pk": 7,
|
"pk": 7,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": 1,
|
"for_club": 1,
|
||||||
"name": "Pr\u00e9sident\u00b7e BDE",
|
"name": "Pr\u00e9sident\u22c5e BDE",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
24,
|
24,
|
||||||
25,
|
25,
|
||||||
|
@ -3290,7 +3492,7 @@
|
||||||
"pk": 8,
|
"pk": 8,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": 1,
|
"for_club": 1,
|
||||||
"name": "Tr\u00e9sorier\u00b7\u00e8re BDE",
|
"name": "Tr\u00e9sorièr\u22c5e BDE",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
23,
|
23,
|
||||||
24,
|
24,
|
||||||
|
@ -3413,7 +3615,11 @@
|
||||||
46,
|
46,
|
||||||
148,
|
148,
|
||||||
149,
|
149,
|
||||||
182
|
182,
|
||||||
|
207,
|
||||||
|
208,
|
||||||
|
209,
|
||||||
|
210
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3458,7 +3664,7 @@
|
||||||
"pk": 13,
|
"pk": 13,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": null,
|
"for_club": null,
|
||||||
"name": "Chef de bus",
|
"name": "Chef\u22c5fe de bus",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
22,
|
22,
|
||||||
84,
|
84,
|
||||||
|
@ -3477,7 +3683,7 @@
|
||||||
"pk": 14,
|
"pk": 14,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": null,
|
"for_club": null,
|
||||||
"name": "Chef d'\u00e9quipe",
|
"name": "Chef\u22c5fe d'\u00e9quipe",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
22,
|
22,
|
||||||
84,
|
84,
|
||||||
|
@ -3526,7 +3732,7 @@
|
||||||
"pk": 18,
|
"pk": 18,
|
||||||
"fields": {
|
"fields": {
|
||||||
"for_club": null,
|
"for_club": null,
|
||||||
"name": "Adhérent WEI",
|
"name": "Adhérent\u22c5e WEI",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
77,
|
77,
|
||||||
114
|
114
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from rest_framework.permissions import DjangoObjectPermissions
|
from rest_framework.permissions import DjangoObjectPermissions
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 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 oauth2_provider.oauth2_validators import OAuth2Validator
|
from oauth2_provider.oauth2_validators import OAuth2Validator
|
||||||
from oauth2_provider.scopes import BaseScopes
|
from oauth2_provider.scopes import BaseScopes
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
@ -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 BDE")
|
roles = record.roles.filter((~(Q(name="Adhérent⋅e BDE")
|
||||||
| Q(name="Adhérent Kfet")
|
| Q(name="Adhérent⋅e 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()
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
@ -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 BDE"))
|
membership.roles.add(Role.objects.get(name="Adhérent⋅e 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 BDE"))
|
membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
resp = self.client.get(reverse('permission:scopes'))
|
resp = self.client.get(reverse('permission:scopes'))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 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 collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
@ -12,6 +12,7 @@ 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
|
||||||
|
@ -35,11 +36,9 @@ class ProtectQuerysetMixin:
|
||||||
try:
|
try:
|
||||||
return super().get_object(queryset)
|
return super().get_object(queryset)
|
||||||
except Http404 as e:
|
except Http404 as e:
|
||||||
try:
|
if self.get_queryset(filter_permissions=False).count() == self.get_queryset().count():
|
||||||
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)
|
||||||
|
@ -107,10 +106,31 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RightsView(TemplateView):
|
class RightsView(MultiTableMixin, 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)
|
||||||
|
|
||||||
|
@ -128,19 +148,9 @@ class RightsView(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:
|
||||||
special_memberships = Membership.objects.filter(
|
tables = context["tables"]
|
||||||
date_start__lte=date.today(),
|
for name, table in zip(["special_memberships_table", "superusers"], tables):
|
||||||
date_end__gte=date.today(),
|
context[name] = table
|
||||||
).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
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
default_app_config = 'registration.apps.RegistrationConfig'
|
default_app_config = 'registration.apps.RegistrationConfig'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 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
|
||||||
# Copied from https://gitlab.crans.org/bombar/codeflix/-/blob/master/codeflix/codeflix/tokens.py
|
# Copied from https://gitlab.crans.org/bombar/codeflix/-/blob/master/codeflix/codeflix/tokens.py
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -15,6 +16,7 @@ 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
|
||||||
|
@ -191,11 +193,16 @@ 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(first_name__iregex=pattern)
|
Q(**{f"first_name{suffix}": pattern})
|
||||||
| Q(last_name__iregex=pattern)
|
| Q(**{f"last_name{suffix}": pattern})
|
||||||
| Q(profile__section__iregex=pattern)
|
| Q(**{f"profile__section{suffix}": pattern})
|
||||||
| Q(username__iregex="^" + pattern)
|
| Q(**{f"username{suffix_username}": prefix + pattern})
|
||||||
)
|
)
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
@ -238,9 +245,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
|
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
|
||||||
kfet = Club.objects.get(name="Kfet")
|
kfet = Club.objects.get(name="Kfet")
|
||||||
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||||
if Club.objects.filter(name__iexact="BDA").exists():
|
for club in Club.objects.filter(add_registration_form=True):
|
||||||
bda = Club.objects.get(name__iexact="BDA")
|
fee += club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
|
||||||
fee += bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
|
|
||||||
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
|
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
|
||||||
|
|
||||||
# ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
|
# ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
|
||||||
|
@ -249,6 +255,16 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
# add clubs that are in registration form
|
||||||
|
for club in Club.objects.filter(add_registration_form=True).order_by("name"):
|
||||||
|
form_join_club = forms.BooleanField(
|
||||||
|
label=_("Join %(club)s Club") % {'club': club.name},
|
||||||
|
required=False,
|
||||||
|
initial=False,
|
||||||
|
)
|
||||||
|
form.fields.update({f"join_{club.id}": form_join_club})
|
||||||
|
|
||||||
user = self.get_object()
|
user = self.get_object()
|
||||||
form.fields["last_name"].initial = user.last_name
|
form.fields["last_name"].initial = user.last_name
|
||||||
form.fields["first_name"].initial = user.first_name
|
form.fields["first_name"].initial = user.first_name
|
||||||
|
@ -266,11 +282,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
form.add_error(None, _("An alias with a similar name already exists."))
|
form.add_error(None, _("An alias with a similar name already exists."))
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
|
|
||||||
# Check if BDA exist to propose membership at regisration
|
|
||||||
bda_exists = False
|
|
||||||
if Club.objects.filter(name__iexact="BDA").exists():
|
|
||||||
bda_exists = True
|
|
||||||
|
|
||||||
# Get form data
|
# Get form data
|
||||||
# soge = form.cleaned_data["soge"]
|
# soge = form.cleaned_data["soge"]
|
||||||
credit_type = form.cleaned_data["credit_type"]
|
credit_type = form.cleaned_data["credit_type"]
|
||||||
|
@ -280,8 +291,9 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
bank = form.cleaned_data["bank"]
|
bank = form.cleaned_data["bank"]
|
||||||
join_bde = form.cleaned_data["join_bde"]
|
join_bde = form.cleaned_data["join_bde"]
|
||||||
join_kfet = form.cleaned_data["join_kfet"]
|
join_kfet = form.cleaned_data["join_kfet"]
|
||||||
if bda_exists:
|
|
||||||
join_bda = form.cleaned_data["join_bda"]
|
clubs_registration = Club.objects.filter(add_registration_form=True).order_by("name")
|
||||||
|
join_clubs = [(club, form.cleaned_data[f"join_{club.id}"]) for club in clubs_registration]
|
||||||
|
|
||||||
# if soge:
|
# if soge:
|
||||||
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
|
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
|
||||||
|
@ -303,11 +315,12 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||||
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||||
# Add extra fee for the full membership
|
# Add extra fee for the full membership
|
||||||
fee += kfet_fee if join_kfet else 0
|
fee += kfet_fee if join_kfet else 0
|
||||||
if bda_exists:
|
clubs_fee = dict()
|
||||||
bda = Club.objects.get(name__iexact="BDA")
|
for club, join_club in join_clubs:
|
||||||
bda_fee = bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
|
club_fee = club.membership_fee_paid if user.profile.paid else club.membership_fee_unpaid
|
||||||
# Add extra fee for the bda membership
|
# Add extra fee for the club membership
|
||||||
fee += bda_fee if join_bda else 0
|
clubs_fee[club] = club_fee
|
||||||
|
fee += club_fee if join_club else 0
|
||||||
|
|
||||||
# # If the bank pays, then we don't credit now. Treasurers will validate the transaction
|
# # If the bank pays, then we don't credit now. Treasurers will validate the transaction
|
||||||
# # and credit the note later.
|
# # and credit the note later.
|
||||||
|
@ -370,7 +383,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 BDE"))
|
membership.roles.add(Role.objects.get(name="Adhérent⋅e BDE"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
if join_kfet:
|
if join_kfet:
|
||||||
|
@ -384,20 +397,21 @@ 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 Kfet"))
|
membership.roles.add(Role.objects.get(name="Adhérent⋅e Kfet"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
if bda_exists and join_bda:
|
for club, join_club in join_clubs:
|
||||||
# Create membership for the user to the BDA starting today
|
if join_club:
|
||||||
membership = Membership(
|
# Create membership for the user to the BDA starting today
|
||||||
club=bda,
|
membership = Membership(
|
||||||
user=user,
|
club=club,
|
||||||
fee=bda_fee,
|
user=user,
|
||||||
)
|
fee=clubs_fee[club],
|
||||||
membership.save()
|
)
|
||||||
membership.refresh_from_db()
|
membership.save()
|
||||||
membership.roles.add(Role.objects.get(name="Membre de club"))
|
membership.refresh_from_db()
|
||||||
membership.save()
|
membership.roles.add(Role.objects.get(name="Membre de club"))
|
||||||
|
membership.save()
|
||||||
|
|
||||||
# if soge:
|
# if soge:
|
||||||
# soge_credit = SogeCredit.objects.get(user=user)
|
# soge_credit = SogeCredit.objects.get(user=user)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue