mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 01:48:21 +02:00
Compare commits
8 Commits
a3073ba5a5
...
fix_pipeli
Author | SHA1 | Date | |
---|---|---|---|
81c12436a8 | |||
8eee14075c | |||
ab9ba62af1 | |||
8ca0648e75 | |||
77ecfd6ed5 | |||
0ab1367e55 | |||
524f0e098a | |||
1ca4246cbd |
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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 ReadProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"name": "Pot",
|
"name": "Pot",
|
||||||
"manage_entries": true,
|
"manage_entries": true,
|
||||||
"can_invite": true,
|
"can_invite": true,
|
||||||
"guest_entry_fee": 1000
|
"guest_entry_fee": 500
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,25 +28,5 @@
|
|||||||
"can_invite": false,
|
"can_invite": false,
|
||||||
"guest_entry_fee": 0
|
"guest_entry_fee": 0
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "activity.activitytype",
|
|
||||||
"pk": 5,
|
|
||||||
"fields": {
|
|
||||||
"name": "Soir\u00e9e avec entrées",
|
|
||||||
"manage_entries": true,
|
|
||||||
"can_invite": false,
|
|
||||||
"guest_entry_fee": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"model": "activity.activitytype",
|
|
||||||
"pk": 7,
|
|
||||||
"fields": {
|
|
||||||
"name": "Soir\u00e9e avec invitations",
|
|
||||||
"manage_entries": true,
|
|
||||||
"can_invite": true,
|
|
||||||
"guest_entry_fee": 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.28 on 2024-03-23 13:22
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('activity', '0002_auto_20200904_2341'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='activity',
|
|
||||||
name='description',
|
|
||||||
field=models.TextField(blank=True, default='', verbose_name='description'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@ -66,8 +66,6 @@ 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(
|
||||||
@ -125,14 +123,6 @@ class Activity(models.Model):
|
|||||||
verbose_name=_('open'),
|
verbose_name=_('open'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("activity")
|
|
||||||
verbose_name_plural = _("activities")
|
|
||||||
unique_together = ("name", "date_start", "date_end",)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -154,6 +144,14 @@ class Activity(models.Model):
|
|||||||
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
|
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("activity")
|
||||||
|
verbose_name_plural = _("activities")
|
||||||
|
unique_together = ("name", "date_start", "date_end",)
|
||||||
|
|
||||||
|
|
||||||
class Entry(models.Model):
|
class Entry(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -254,13 +252,14 @@ class Guest(models.Model):
|
|||||||
verbose_name=_("inviter"),
|
verbose_name=_("inviter"),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
@property
|
||||||
verbose_name = _("guest")
|
def has_entry(self):
|
||||||
verbose_name_plural = _("guests")
|
try:
|
||||||
unique_together = ("activity", "last_name", "first_name", )
|
if self.entry:
|
||||||
|
return True
|
||||||
def __str__(self):
|
return False
|
||||||
return self.first_name + " " + self.last_name
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||||
@ -291,14 +290,13 @@ class Guest(models.Model):
|
|||||||
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
return super().save(force_insert, force_update, using, update_fields)
|
||||||
|
|
||||||
@property
|
def __str__(self):
|
||||||
def has_entry(self):
|
return self.first_name + " " + self.last_name
|
||||||
try:
|
|
||||||
if self.entry:
|
class Meta:
|
||||||
return True
|
verbose_name = _("guest")
|
||||||
return False
|
verbose_name_plural = _("guests")
|
||||||
except AttributeError:
|
unique_together = ("activity", "last_name", "first_name", )
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class GuestTransaction(Transaction):
|
class GuestTransaction(Transaction):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
@ -17,24 +17,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
{% endblock %}
|
||||||
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 %}
|
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -76,7 +76,6 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
|||||||
context['upcoming'] = ActivityTable(
|
context['upcoming'] = ActivityTable(
|
||||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
||||||
prefix='upcoming-',
|
prefix='upcoming-',
|
||||||
order_by='date_start',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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,5 +1,5 @@
|
|||||||
from rest_framework.pagination import PageNumberPagination
|
from rest_framework.pagination import PageNumberPagination
|
||||||
|
|
||||||
|
|
||||||
class CustomPagination(PageNumberPagination):
|
class CustomPagination(PageNumberPagination):
|
||||||
page_size_query_param = 'page_size'
|
page_size_query_param = 'page_size'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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,32 +0,0 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
from note_kfet.admin import admin_site
|
|
||||||
|
|
||||||
from .models import QR_code, BasicFood, TransformedFood, Allergen
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(QR_code, site = admin_site)
|
|
||||||
class QR_codeAdmin(admin.ModelAdmin):
|
|
||||||
"""
|
|
||||||
TEMPORARY
|
|
||||||
"""
|
|
||||||
|
|
||||||
@admin.register(BasicFood, site = admin_site)
|
|
||||||
class Basic_foodAdmin(admin.ModelAdmin):
|
|
||||||
"""
|
|
||||||
TEMPORARY
|
|
||||||
"""
|
|
||||||
|
|
||||||
@admin.register(TransformedFood, site = admin_site)
|
|
||||||
class Transformed_foodAdmin(admin.ModelAdmin):
|
|
||||||
"""
|
|
||||||
TEMPORARY
|
|
||||||
"""
|
|
||||||
|
|
||||||
@admin.register(Allergen, site = admin_site)
|
|
||||||
class AllergenAdmin(admin.ModelAdmin):
|
|
||||||
"""
|
|
||||||
TEMPORARY
|
|
||||||
"""
|
|
@ -1,10 +0,0 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class FoodkfetConfig(AppConfig):
|
|
||||||
name = 'food'
|
|
||||||
verbose_name = _('food')
|
|
@ -1,92 +0,0 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from random import shuffle
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.utils import timezone
|
|
||||||
from member.models import Club
|
|
||||||
from note_kfet.inputs import Autocomplete, DatePickerInput, DateTimePickerInput
|
|
||||||
from note_kfet.middlewares import get_current_request
|
|
||||||
from permission.backends import PermissionBackend
|
|
||||||
|
|
||||||
from .models import QR_code, Allergen, BasicFood, TransformedFood
|
|
||||||
|
|
||||||
|
|
||||||
class BasicFoodForms(forms.ModelForm):
|
|
||||||
"""
|
|
||||||
Form for add non-transformed food
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
|
|
||||||
self.fields['name'].required = True
|
|
||||||
self.fields['owner'].required = True
|
|
||||||
self.fields['label'].help_text = _('The lot number must be contained in the picture')
|
|
||||||
|
|
||||||
# Some example
|
|
||||||
self.fields['name'].widget.attrs.update({"placeholder": _("pasta")})
|
|
||||||
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
|
|
||||||
shuffle(clubs)
|
|
||||||
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
|
||||||
def clean_dlm_or_dlc(self):
|
|
||||||
is_dlc = self.cleaned_data["is_DLC"]
|
|
||||||
is_ddm = self.cleaned_data["is_DDM"]
|
|
||||||
if is_dlc and is_ddm:
|
|
||||||
self.add_error("is_ddm", _("the product cannot be a DLC and a DDM"))
|
|
||||||
return is_ddm
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = BasicFood
|
|
||||||
fields = ('name', 'owner', 'is_DLC', 'is_DDM', 'expiry_date', 'label')
|
|
||||||
widgets = {
|
|
||||||
"owner": Autocomplete(
|
|
||||||
model = Club,
|
|
||||||
attrs = {"api_url": "/api/members/club/"},
|
|
||||||
),
|
|
||||||
'expiry_date': DatePickerInput(),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TransformedFoodForms(forms.ModelForm):
|
|
||||||
"""
|
|
||||||
Form for add transformed food
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.fields['name'].widget.attrs.update({"autofocus": "autofocus"})
|
|
||||||
self.fields['name'].required = True
|
|
||||||
self.fields['owner'].required = True
|
|
||||||
self.fields['creation_date'].required = True
|
|
||||||
self.fields['creation_date'].initial = timezone.now
|
|
||||||
self.fields['is_active'].initial = True
|
|
||||||
|
|
||||||
# Some example
|
|
||||||
self.fields['name'].widget.attrs.update({"placeholder": _("lasagna")})
|
|
||||||
clubs = list(Club.objects.filter(PermissionBackend.filter_queryset(get_current_request(), Club, "change")).all())
|
|
||||||
shuffle(clubs)
|
|
||||||
self.fields['owner'].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = TransformedFood
|
|
||||||
fields = ('name', 'creation_date', 'owner', 'is_active',)
|
|
||||||
widgets = {
|
|
||||||
"owner": Autocomplete(
|
|
||||||
model = Club,
|
|
||||||
attrs = {"api_url": "/api/members/club/"},
|
|
||||||
),
|
|
||||||
'creation_date': DateTimePickerInput(),
|
|
||||||
}
|
|
||||||
class AllergenForms(forms.ModelForm):
|
|
||||||
"""
|
|
||||||
Form for allergen
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Allergen
|
|
||||||
exclude = ['basic_food', 'transformed_food']
|
|
@ -1,97 +0,0 @@
|
|||||||
# Generated by Django 2.2.28 on 2024-05-25 20:32
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('member', '0011_profile_vss_charter_read'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='BasicFood',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=255, verbose_name='name')),
|
|
||||||
('is_DLC', models.BooleanField(default=False, verbose_name='is DLC')),
|
|
||||||
('is_DDM', models.BooleanField(default=False, verbose_name='is DDM')),
|
|
||||||
('arrival_date', models.DateTimeField(default=django.utils.timezone.now, verbose_name='arrival date')),
|
|
||||||
('expiry_date', models.DateTimeField(blank=True, null=True, verbose_name='expiry date')),
|
|
||||||
('label', models.ImageField(max_length=255, upload_to='label/', verbose_name='food label')),
|
|
||||||
('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
|
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Basic food',
|
|
||||||
'verbose_name_plural': 'Basic foods',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='TransformedFood',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=255, verbose_name='name')),
|
|
||||||
('creation_date', models.DateTimeField(verbose_name='creation date')),
|
|
||||||
('expiry_date', models.DateTimeField(verbose_name='expiry date')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='is active')),
|
|
||||||
('was_eaten', models.BooleanField(default=False, verbose_name='was eaten')),
|
|
||||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='member.Club', verbose_name='owner')),
|
|
||||||
('transformed_ingredient', models.ManyToManyField(blank=True, related_name='transformed_ingredient_inv', to='food.TransformedFood', verbose_name='transformed ingredient')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Transformed food',
|
|
||||||
'verbose_name_plural': 'Transformed foods',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='QR_code',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('qr_code_number', models.PositiveIntegerField(verbose_name='QR-code number')),
|
|
||||||
('basic_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.BasicFood', verbose_name='basic food')),
|
|
||||||
('transformed_food_container', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='QR_code', to='food.TransformedFood', verbose_name='transformed food container')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'QR-code',
|
|
||||||
'verbose_name_plural': 'QR-codes',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='basicfood',
|
|
||||||
name='transformed_food',
|
|
||||||
field=models.ManyToManyField(blank=True, related_name='BasicFood', to='food.TransformedFood', verbose_name='transformed food'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Allergen',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('gluten', models.BooleanField(default=False, verbose_name='gluten')),
|
|
||||||
('nut', models.BooleanField(default=False, verbose_name='nut')),
|
|
||||||
('crustecean', models.BooleanField(default=False, verbose_name='crustacean')),
|
|
||||||
('celery', models.BooleanField(default=False, verbose_name='celery')),
|
|
||||||
('egg', models.BooleanField(default=False, verbose_name='egg')),
|
|
||||||
('mustard', models.BooleanField(default=False, verbose_name='mustard')),
|
|
||||||
('fish', models.BooleanField(default=False, verbose_name='fish')),
|
|
||||||
('soy', models.BooleanField(default=False, verbose_name='soy')),
|
|
||||||
('milk', models.BooleanField(default=False, verbose_name='milk')),
|
|
||||||
('sulphite', models.BooleanField(default=False, verbose_name='sulphite')),
|
|
||||||
('lupine', models.BooleanField(default=False, verbose_name='lupine')),
|
|
||||||
('mollusc', models.BooleanField(default=False, verbose_name='mollusc')),
|
|
||||||
('groundnut', models.BooleanField(default=False, verbose_name='groundnut')),
|
|
||||||
('sesame', models.BooleanField(default=False, verbose_name='sesame')),
|
|
||||||
('alcohol', models.BooleanField(default=False, verbose_name='alcohol')),
|
|
||||||
('basic_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Allergen', to='food.BasicFood', verbose_name='basic food')),
|
|
||||||
('transformed_food', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Allergen', to='food.TransformedFood', verbose_name='transformed food')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Allergen',
|
|
||||||
'verbose_name_plural': 'Allergens',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,289 +0,0 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.core.validators import MinValueValidator
|
|
||||||
from django.db import models, transaction
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from member.models import Club
|
|
||||||
|
|
||||||
#################################################################
|
|
||||||
# TO DO
|
|
||||||
# - link allergen with one food (basic or transformed) with check
|
|
||||||
# - check on basic food
|
|
||||||
# - check on transformed food
|
|
||||||
#################################################################
|
|
||||||
|
|
||||||
class QR_code(models.Model):
|
|
||||||
"""
|
|
||||||
An QR_code model
|
|
||||||
"""
|
|
||||||
qr_code_number = models.PositiveIntegerField(
|
|
||||||
verbose_name=_("QR-code number"),
|
|
||||||
)
|
|
||||||
|
|
||||||
transformed_food_container = models.ForeignKey(
|
|
||||||
'TransformedFood',
|
|
||||||
on_delete = models.PROTECT,
|
|
||||||
related_name = 'QR_code',
|
|
||||||
null = True,
|
|
||||||
blank = True,
|
|
||||||
verbose_name = _('transformed food container'),
|
|
||||||
)
|
|
||||||
|
|
||||||
basic_food = models.ForeignKey(
|
|
||||||
'BasicFood',
|
|
||||||
on_delete = models.PROTECT,
|
|
||||||
related_name = 'QR_code',
|
|
||||||
null = True,
|
|
||||||
blank = True,
|
|
||||||
verbose_name = _('basic food'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("QR-code")
|
|
||||||
verbose_name_plural = _("QR-codes")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return _("QR-code number {qr_code_number}").format(qr_code_number=self.qr_code_number)
|
|
||||||
|
|
||||||
class Allergen(models.Model):
|
|
||||||
"""
|
|
||||||
A list of allergen and alimentary restrictions
|
|
||||||
"""
|
|
||||||
|
|
||||||
gluten = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('gluten'),
|
|
||||||
)
|
|
||||||
|
|
||||||
nut = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('nut'),
|
|
||||||
)
|
|
||||||
|
|
||||||
crustecean = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('crustacean'),
|
|
||||||
)
|
|
||||||
|
|
||||||
celery = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('celery'),
|
|
||||||
)
|
|
||||||
|
|
||||||
egg = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('egg'),
|
|
||||||
)
|
|
||||||
|
|
||||||
mustard = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('mustard'),
|
|
||||||
)
|
|
||||||
|
|
||||||
fish = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('fish'),
|
|
||||||
)
|
|
||||||
|
|
||||||
soy = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('soy'),
|
|
||||||
)
|
|
||||||
|
|
||||||
milk = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('milk'),
|
|
||||||
)
|
|
||||||
|
|
||||||
sulphite = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('sulphite'),
|
|
||||||
)
|
|
||||||
|
|
||||||
lupine = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('lupine'),
|
|
||||||
)
|
|
||||||
|
|
||||||
mollusc = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('mollusc'),
|
|
||||||
)
|
|
||||||
|
|
||||||
groundnut = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('groundnut'),
|
|
||||||
)
|
|
||||||
|
|
||||||
sesame = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('sesame'),
|
|
||||||
)
|
|
||||||
|
|
||||||
alcohol = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('alcohol'),
|
|
||||||
)
|
|
||||||
|
|
||||||
transformed_food = models.ForeignKey(
|
|
||||||
'TransformedFood',
|
|
||||||
on_delete = models.CASCADE,
|
|
||||||
related_name = 'Allergen',
|
|
||||||
blank = True,
|
|
||||||
null = True,
|
|
||||||
verbose_name = _('transformed food'),
|
|
||||||
)
|
|
||||||
|
|
||||||
basic_food = models.ForeignKey(
|
|
||||||
'BasicFood',
|
|
||||||
on_delete = models.CASCADE,
|
|
||||||
related_name = 'Allergen',
|
|
||||||
blank = True,
|
|
||||||
null = True,
|
|
||||||
verbose_name = _('basic food'),
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Allergen')
|
|
||||||
verbose_name_plural = _('Allergens')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return _('Allergens of #{id}').format(id=self.id)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BasicFood(models.Model):
|
|
||||||
"""
|
|
||||||
Food which has been directly buy on supermarket
|
|
||||||
"""
|
|
||||||
name = models.CharField(
|
|
||||||
verbose_name=_('name'),
|
|
||||||
max_length=255,
|
|
||||||
)
|
|
||||||
|
|
||||||
is_DLC = models.BooleanField(
|
|
||||||
verbose_name=_("is DLC"),
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
is_DDM = models.BooleanField(
|
|
||||||
verbose_name=_("is DDM"),
|
|
||||||
default=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
arrival_date = models.DateTimeField(
|
|
||||||
verbose_name=_('arrival date'),
|
|
||||||
default=timezone.now,
|
|
||||||
)
|
|
||||||
|
|
||||||
expiry_date = models.DateTimeField(
|
|
||||||
verbose_name=_('expiry date'),
|
|
||||||
blank=True,
|
|
||||||
null = True,
|
|
||||||
)
|
|
||||||
|
|
||||||
owner = models.ForeignKey(
|
|
||||||
Club,
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name= '+',
|
|
||||||
verbose_name=_('owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
label = models.ImageField(
|
|
||||||
verbose_name=_('food label'),
|
|
||||||
max_length=255,
|
|
||||||
blank=False,
|
|
||||||
null=False,
|
|
||||||
upload_to='label/',
|
|
||||||
)
|
|
||||||
|
|
||||||
was_eaten = models.BooleanField(
|
|
||||||
verbose_name=_('was eaten'),
|
|
||||||
default = False,
|
|
||||||
)
|
|
||||||
|
|
||||||
transformed_food = models.ManyToManyField(
|
|
||||||
'TransformedFood',
|
|
||||||
related_name= 'BasicFood',
|
|
||||||
blank = True,
|
|
||||||
verbose_name = _('transformed food'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name=_('Basic food')
|
|
||||||
verbose_name_plural=_('Basic foods')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, force_insert=False, force_update=False, using= None, update_fields=None):
|
|
||||||
# Check if is_DLC and is DDM are not both True
|
|
||||||
if self.is_DLC and self.is_DDM:
|
|
||||||
raise ValidationError("The product cannot be a DLC and a DDM")
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
|
||||||
|
|
||||||
|
|
||||||
class TransformedFood(models.Model):
|
|
||||||
"""
|
|
||||||
Transformed food are a mix between basic food and meal
|
|
||||||
"""
|
|
||||||
name = models.CharField(
|
|
||||||
max_length = 255,
|
|
||||||
verbose_name =_('name'),
|
|
||||||
)
|
|
||||||
|
|
||||||
creation_date = models.DateTimeField(
|
|
||||||
verbose_name =_('creation date'),
|
|
||||||
)
|
|
||||||
|
|
||||||
expiry_date = models.DateTimeField(
|
|
||||||
verbose_name =_('expiry date'),
|
|
||||||
)
|
|
||||||
|
|
||||||
owner = models.ForeignKey(
|
|
||||||
Club,
|
|
||||||
on_delete = models.PROTECT,
|
|
||||||
related_name = '+',
|
|
||||||
verbose_name =_('owner'),
|
|
||||||
)
|
|
||||||
|
|
||||||
transformed_ingredient = models.ManyToManyField(
|
|
||||||
"self",
|
|
||||||
blank = True,
|
|
||||||
symmetrical = False,
|
|
||||||
related_name = 'transformed_ingredient_inv',
|
|
||||||
verbose_name = _('transformed ingredient'),
|
|
||||||
)
|
|
||||||
|
|
||||||
is_active = models.BooleanField(
|
|
||||||
default = True,
|
|
||||||
verbose_name = _('is active'),
|
|
||||||
)
|
|
||||||
|
|
||||||
was_eaten = models.BooleanField(
|
|
||||||
default = False,
|
|
||||||
verbose_name = _('was eaten'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('Transformed food')
|
|
||||||
verbose_name_plural = _('Transformed foods')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, force_insert=False, force_update=False, using= None, update_fields=None):
|
|
||||||
return super().save(force_insert, force_update, using, update_fields)
|
|
@ -1,22 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% comment %}
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
{% endcomment %}
|
|
||||||
{% load i18n crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card bg-white mb-3">
|
|
||||||
<h3 class="card-header text-center">
|
|
||||||
HTML not finished <br>
|
|
||||||
{{ title }}
|
|
||||||
</h3>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
{{ allergenform|crispy }}
|
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,21 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% comment %}
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
{% endcomment %}
|
|
||||||
{% load i18n crispy_forms_tags %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="card bg-white mb-3">
|
|
||||||
<h3 class="card-header text-center">
|
|
||||||
HTML not finished <br>
|
|
||||||
{{ title }}
|
|
||||||
</h3>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{{ form|crispy }}
|
|
||||||
<button class="btn btn-primary" type="submit">{% trans "Submit"%}</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
@ -1,19 +0,0 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.urls import path
|
|
||||||
|
|
||||||
from . import views
|
|
||||||
|
|
||||||
###############################
|
|
||||||
# TO DO
|
|
||||||
# - name url correctly, thinking about the scheme of the app
|
|
||||||
###############################
|
|
||||||
|
|
||||||
app_name = 'food'
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path('0', views.BasicFoodCreateView.as_view(), name = 'basic_food'),
|
|
||||||
path('1', views.TransformedFoodCreateView.as_view(), name = 'transformed_food'),
|
|
||||||
]
|
|
||||||
|
|
@ -1,104 +0,0 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.db import transaction
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
from django.utils import timezone
|
|
||||||
from datetime import timedelta
|
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
from .forms import BasicFoodForms, TransformedFoodForms, AllergenForms
|
|
||||||
from .models import BasicFood, TransformedFood, Allergen
|
|
||||||
|
|
||||||
|
|
||||||
class BasicFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|
||||||
#####################################################################
|
|
||||||
# TO DO
|
|
||||||
# - fix picture save
|
|
||||||
# - implement solution crop and convert image (reuse or recode ImageForm from members apps
|
|
||||||
# - implement AllergenForms
|
|
||||||
# - redirect to another view after the poll is submitted
|
|
||||||
#####################################################################
|
|
||||||
"""
|
|
||||||
A view to add a basic food
|
|
||||||
"""
|
|
||||||
model = BasicFood
|
|
||||||
form_class = BasicFoodForms
|
|
||||||
template_name = 'food/basic_food_form.html'
|
|
||||||
extra_context = {"title": _("Add a new aliment")}
|
|
||||||
|
|
||||||
def get_sample_object(self):
|
|
||||||
return BasicFood(
|
|
||||||
name="",
|
|
||||||
is_DLC=False,
|
|
||||||
is_DDM=False,
|
|
||||||
expiry_date=timezone.now(),
|
|
||||||
label='pic/default.png',
|
|
||||||
)
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def form_valid(self, form):
|
|
||||||
form.instance.creater = self.request.user
|
|
||||||
basic_food_form = BasicFoodForms(data=self.request.POST)
|
|
||||||
allergen_form = AllergenForms(data=self.request.POST)
|
|
||||||
if not basic_food_form.is_valid() or not allergen_form.is_valid():
|
|
||||||
return self.form_invalid(form)
|
|
||||||
|
|
||||||
# Save the aliment and the allergens associed
|
|
||||||
basic_food = form.save(commit=False)
|
|
||||||
# We assume the date of labeling and the same as the date of arrival
|
|
||||||
basic_food.arrival_date = timezone.now
|
|
||||||
basic_food._force_save = True
|
|
||||||
basic_food.save()
|
|
||||||
basic_food.refresh_from_db()
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
|
||||||
self.object.refresh_from_db()
|
|
||||||
# TEMPORARY, I create a fonctionnal view before
|
|
||||||
# return reverse_lazy('food:basicfood', kwargs={"pk": self.object.pk})
|
|
||||||
return '0'
|
|
||||||
|
|
||||||
class TransformedFoodCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|
||||||
###############################################
|
|
||||||
# TO DO
|
|
||||||
# -redirect to another view after submit
|
|
||||||
###############################################
|
|
||||||
"""
|
|
||||||
A view to add a tranformed food
|
|
||||||
"""
|
|
||||||
model = TransformedFood
|
|
||||||
template_name = 'food/transformed_food_form.html'
|
|
||||||
form_class = TransformedFoodForms
|
|
||||||
extra_context = {"title": _("Add a new meal")}
|
|
||||||
|
|
||||||
def get_sample_object(self):
|
|
||||||
return TransformedFood(
|
|
||||||
name="",
|
|
||||||
creation_date=timezone.now(),
|
|
||||||
)
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def form_valid(self, form):
|
|
||||||
form.instance.creater = self.request.user
|
|
||||||
transformed_food_form = TransformedFoodForms(data=self.request.POST)
|
|
||||||
if not transformed_food_form.is_valid():
|
|
||||||
return self.form_invalid(form)
|
|
||||||
|
|
||||||
# Save the aliment and allergens associated
|
|
||||||
transformed_food = form.save(commit=False)
|
|
||||||
# Without microbiological analyzes, the storage time is 3 days
|
|
||||||
transformed_food.expiry_date = transformed_food.creation_date + timedelta(days = 3)
|
|
||||||
transformed_food._force_save = True
|
|
||||||
transformed_food.save()
|
|
||||||
transformed_food.refresh_from_db()
|
|
||||||
return super().form_valid(form)
|
|
||||||
|
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
|
||||||
self.object.refresh_from_db()
|
|
||||||
# TEMPORARY, I create a fonctionnal view before
|
|
||||||
# return reverse_lazy('food:tranformed_food', kwargs={"pk": self.object.pk})
|
|
||||||
return '1'
|
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -76,6 +76,9 @@ class Changelog(models.Model):
|
|||||||
verbose_name=_('timestamp'),
|
verbose_name=_('timestamp'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def delete(self, using=None, keep_parents=False):
|
||||||
|
raise ValidationError(_("Logs cannot be destroyed."))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("changelog")
|
verbose_name = _("changelog")
|
||||||
verbose_name_plural = _("changelogs")
|
verbose_name_plural = _("changelogs")
|
||||||
@ -83,6 +86,3 @@ class Changelog(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
|
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
|
||||||
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
|
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
|
||||||
raise ValidationError(_("Logs cannot be destroyed."))
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -47,13 +47,6 @@ class ProfileForm(forms.ModelForm):
|
|||||||
|
|
||||||
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
|
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
|
||||||
|
|
||||||
VSS_charter_read = forms.BooleanField(
|
|
||||||
required=True,
|
|
||||||
label=_("Anti-VSS (<em>Violences Sexistes et Sexuelles</em>) charter read and approved"),
|
|
||||||
help_text=_("Tick after having read and accepted the anti-VSS charter \
|
|
||||||
<a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available here in pdf</a>")
|
|
||||||
)
|
|
||||||
|
|
||||||
def clean_promotion(self):
|
def clean_promotion(self):
|
||||||
promotion = self.cleaned_data["promotion"]
|
promotion = self.cleaned_data["promotion"]
|
||||||
if promotion > timezone.now().year:
|
if promotion > timezone.now().year:
|
||||||
@ -121,7 +114,7 @@ class ImageForm(forms.Form):
|
|||||||
frame = frame.crop((x, y, x + w, y + h))
|
frame = frame.crop((x, y, x + w, y + h))
|
||||||
frame = frame.resize(
|
frame = frame.resize(
|
||||||
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
|
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
|
||||||
Image.LANCZOS,
|
Image.ANTIALIAS,
|
||||||
)
|
)
|
||||||
frames.append(frame)
|
frames.append(frame)
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.28 on 2023-08-23 21:29
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('member', '0009_auto_20220904_2325'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='promotion',
|
|
||||||
field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.28 on 2023-08-31 09:50
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('member', '0010_new_default_year'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='VSS_charter_read',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='VSS charter read'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
@ -28,6 +28,7 @@ class Profile(models.Model):
|
|||||||
We do not want to patch the Django Contrib :model:`auth.User`model;
|
We do not want to patch the Django Contrib :model:`auth.User`model;
|
||||||
so this model add an user profile with additional information.
|
so this model add an user profile with additional information.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = models.OneToOneField(
|
user = models.OneToOneField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
@ -133,22 +134,6 @@ class Profile(models.Model):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
VSS_charter_read = models.BooleanField(
|
|
||||||
verbose_name=_("VSS charter read"),
|
|
||||||
default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('user profile')
|
|
||||||
verbose_name_plural = _('user profile')
|
|
||||||
indexes = [models.Index(fields=['user'])]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.user)
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return reverse('member:user_detail', args=(self.user_id,))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ens_year(self):
|
def ens_year(self):
|
||||||
"""
|
"""
|
||||||
@ -173,6 +158,17 @@ class Profile(models.Model):
|
|||||||
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
|
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('user profile')
|
||||||
|
verbose_name_plural = _('user profile')
|
||||||
|
indexes = [models.Index(fields=['user'])]
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('member:user_detail', args=(self.user_id,))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.user)
|
||||||
|
|
||||||
def send_email_validation_link(self):
|
def send_email_validation_link(self):
|
||||||
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
|
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
|
||||||
token = email_validation_token.make_token(self.user)
|
token = email_validation_token.make_token(self.user)
|
||||||
@ -204,11 +200,9 @@ class Club(models.Model):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
unique=True,
|
unique=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
email = models.EmailField(
|
email = models.EmailField(
|
||||||
verbose_name=_('email'),
|
verbose_name=_('email'),
|
||||||
)
|
)
|
||||||
|
|
||||||
parent_club = models.ForeignKey(
|
parent_club = models.ForeignKey(
|
||||||
'self',
|
'self',
|
||||||
null=True,
|
null=True,
|
||||||
@ -259,12 +253,25 @@ 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.'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
def update_membership_dates(self):
|
||||||
verbose_name = _("club")
|
"""
|
||||||
verbose_name_plural = _("clubs")
|
This function is called each time the club detail view is displayed.
|
||||||
|
Update the year of the membership dates.
|
||||||
|
"""
|
||||||
|
if not self.membership_start or not self.membership_end:
|
||||||
|
return
|
||||||
|
|
||||||
def __str__(self):
|
today = datetime.date.today()
|
||||||
return self.name
|
|
||||||
|
if (today - self.membership_start).days >= 365:
|
||||||
|
if self.membership_start:
|
||||||
|
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||||
|
self.membership_start.month, self.membership_start.day)
|
||||||
|
if self.membership_end:
|
||||||
|
self.membership_end = datetime.date(self.membership_end.year + 1,
|
||||||
|
self.membership_end.month, self.membership_end.day)
|
||||||
|
self._force_save = True
|
||||||
|
self.save(force_update=True)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, force_insert=False, force_update=False, using=None,
|
def save(self, force_insert=False, force_update=False, using=None,
|
||||||
@ -277,29 +284,16 @@ class Club(models.Model):
|
|||||||
self.membership_end = None
|
self.membership_end = None
|
||||||
super().save(force_insert, force_update, update_fields)
|
super().save(force_insert, force_update, update_fields)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("club")
|
||||||
|
verbose_name_plural = _("clubs")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse_lazy('member:club_detail', args=(self.pk,))
|
return reverse_lazy('member:club_detail', args=(self.pk,))
|
||||||
|
|
||||||
def update_membership_dates(self):
|
|
||||||
"""
|
|
||||||
This function is called each time the club detail view is displayed.
|
|
||||||
Update the year of the membership dates.
|
|
||||||
"""
|
|
||||||
if not self.membership_start or not self.membership_end:
|
|
||||||
return
|
|
||||||
|
|
||||||
today = datetime.date.today()
|
|
||||||
|
|
||||||
while (today - self.membership_start).days >= 365:
|
|
||||||
if self.membership_start:
|
|
||||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
|
||||||
self.membership_start.month, self.membership_start.day)
|
|
||||||
if self.membership_end:
|
|
||||||
self.membership_end = datetime.date(self.membership_end.year + 1,
|
|
||||||
self.membership_end.month, self.membership_end.day)
|
|
||||||
self._force_save = True
|
|
||||||
self.save(force_update=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Membership(models.Model):
|
class Membership(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -339,66 +333,6 @@ class Membership(models.Model):
|
|||||||
verbose_name=_('fee'),
|
verbose_name=_('fee'),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _('membership')
|
|
||||||
verbose_name_plural = _('memberships')
|
|
||||||
indexes = [models.Index(fields=['user'])]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
|
||||||
"""
|
|
||||||
# Ensure that club membership dates are valid
|
|
||||||
old_membership_start = self.club.membership_start
|
|
||||||
self.club.update_membership_dates()
|
|
||||||
if self.club.membership_start != old_membership_start:
|
|
||||||
self.club.save()
|
|
||||||
|
|
||||||
created = not self.pk
|
|
||||||
if not created:
|
|
||||||
for role in self.roles.all():
|
|
||||||
club = role.for_club
|
|
||||||
if club is not None:
|
|
||||||
if club.pk != self.club_id:
|
|
||||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
|
||||||
.format(role=role.name, club=club.name))
|
|
||||||
else:
|
|
||||||
if Membership.objects.filter(
|
|
||||||
user=self.user,
|
|
||||||
club=self.club,
|
|
||||||
date_start__lte=self.date_start,
|
|
||||||
date_end__gte=self.date_start,
|
|
||||||
).exists():
|
|
||||||
raise ValidationError(_('User is already a member of the club'))
|
|
||||||
|
|
||||||
if self.club.parent_club is not None:
|
|
||||||
# Check that the user is already a member of the parent club if the membership is created
|
|
||||||
if not Membership.objects.filter(
|
|
||||||
user=self.user,
|
|
||||||
club=self.club.parent_club,
|
|
||||||
date_start__gte=self.club.parent_club.membership_start,
|
|
||||||
).exists():
|
|
||||||
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
|
||||||
self.renew_parent()
|
|
||||||
else:
|
|
||||||
raise ValidationError(_('User is not a member of the parent club')
|
|
||||||
+ ' ' + self.club.parent_club.name)
|
|
||||||
|
|
||||||
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
|
||||||
|
|
||||||
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
|
||||||
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
|
||||||
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
|
||||||
self.date_end = self.club.membership_end
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
self.make_transaction()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def valid(self):
|
def valid(self):
|
||||||
"""
|
"""
|
||||||
@ -476,6 +410,58 @@ class Membership(models.Model):
|
|||||||
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()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||||
|
"""
|
||||||
|
# Ensure that club membership dates are valid
|
||||||
|
old_membership_start = self.club.membership_start
|
||||||
|
self.club.update_membership_dates()
|
||||||
|
if self.club.membership_start != old_membership_start:
|
||||||
|
self.club.save()
|
||||||
|
|
||||||
|
created = not self.pk
|
||||||
|
if not created:
|
||||||
|
for role in self.roles.all():
|
||||||
|
club = role.for_club
|
||||||
|
if club is not None:
|
||||||
|
if club.pk != self.club_id:
|
||||||
|
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||||
|
.format(role=role.name, club=club.name))
|
||||||
|
else:
|
||||||
|
if Membership.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
club=self.club,
|
||||||
|
date_start__lte=self.date_start,
|
||||||
|
date_end__gte=self.date_start,
|
||||||
|
).exists():
|
||||||
|
raise ValidationError(_('User is already a member of the club'))
|
||||||
|
|
||||||
|
if self.club.parent_club is not None:
|
||||||
|
# Check that the user is already a member of the parent club if the membership is created
|
||||||
|
if not Membership.objects.filter(
|
||||||
|
user=self.user,
|
||||||
|
club=self.club.parent_club,
|
||||||
|
date_start__gte=self.club.parent_club.membership_start,
|
||||||
|
).exists():
|
||||||
|
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||||
|
self.renew_parent()
|
||||||
|
else:
|
||||||
|
raise ValidationError(_('User is not a member of the parent club')
|
||||||
|
+ ' ' + self.club.parent_club.name)
|
||||||
|
|
||||||
|
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
||||||
|
|
||||||
|
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
||||||
|
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
||||||
|
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
||||||
|
self.date_end = self.club.membership_end
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
self.make_transaction()
|
||||||
|
|
||||||
def make_transaction(self):
|
def make_transaction(self):
|
||||||
"""
|
"""
|
||||||
Create Membership transaction associated to this membership.
|
Create Membership transaction associated to this membership.
|
||||||
@ -513,3 +499,11 @@ class Membership(models.Model):
|
|||||||
soge_credit.save()
|
soge_credit.save()
|
||||||
else:
|
else:
|
||||||
transaction.save(force_insert=True)
|
transaction.save(force_insert=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('membership')
|
||||||
|
verbose_name_plural = _('memberships')
|
||||||
|
indexes = [models.Index(fields=['user'])]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* On form submit, create a new friendship
|
* On form submit, create a new friendship
|
||||||
*/
|
*/
|
||||||
function form_create_trust (e) {
|
function create_trust (e) {
|
||||||
// Do not submit HTML form
|
// Do not submit HTML form
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
@ -14,35 +14,25 @@ function form_create_trust (e) {
|
|||||||
addMsg(gettext("You can't add yourself as a friend"), "danger")
|
addMsg(gettext("You can't add yourself as a friend"), "danger")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
create_trust(formData.get('trusting'), trusted_alias.note)
|
$.post('/api/note/trust/', {
|
||||||
|
csrfmiddlewaretoken: formData.get('csrfmiddlewaretoken'),
|
||||||
|
trusting: formData.get('trusting'),
|
||||||
|
trusted: trusted_alias.note
|
||||||
|
}).done(function () {
|
||||||
|
// Reload table
|
||||||
|
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||||
|
addMsg(gettext('Friendship successfully added'), 'success')
|
||||||
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
|
errMsg(xhr.responseJSON)
|
||||||
|
})
|
||||||
}).fail(function (xhr, _textStatus, _error) {
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
errMsg(xhr.responseJSON)
|
errMsg(xhr.responseJSON)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a trust between users
|
* On click of "delete", delete the alias
|
||||||
* @param trusting:Integer trusting note id
|
* @param button_id:Integer Alias id to remove
|
||||||
* @param trusted:Integer trusted note id
|
|
||||||
*/
|
|
||||||
function create_trust(trusting, trusted) {
|
|
||||||
$.post('/api/note/trust/', {
|
|
||||||
trusting: trusting,
|
|
||||||
trusted: trusted,
|
|
||||||
csrfmiddlewaretoken: CSRF_TOKEN
|
|
||||||
}).done(function () {
|
|
||||||
// Reload tables
|
|
||||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
|
||||||
$('#trusted_table').load(location.pathname + ' #trusted_table')
|
|
||||||
addMsg(gettext('Friendship successfully added'), 'success')
|
|
||||||
}).fail(function (xhr, _textStatus, _error) {
|
|
||||||
errMsg(xhr.responseJSON)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On click of "delete", delete the trust
|
|
||||||
* @param button_id:Integer Trust id to remove
|
|
||||||
*/
|
*/
|
||||||
function delete_button (button_id) {
|
function delete_button (button_id) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -52,7 +42,6 @@ function delete_button (button_id) {
|
|||||||
}).done(function () {
|
}).done(function () {
|
||||||
addMsg(gettext('Friendship successfully deleted'), 'success')
|
addMsg(gettext('Friendship successfully deleted'), 'success')
|
||||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||||
$('#trusted_table').load(location.pathname + ' #trusted_table')
|
|
||||||
}).fail(function (xhr, _textStatus, _error) {
|
}).fail(function (xhr, _textStatus, _error) {
|
||||||
errMsg(xhr.responseJSON)
|
errMsg(xhr.responseJSON)
|
||||||
})
|
})
|
||||||
@ -60,5 +49,5 @@ function delete_button (button_id) {
|
|||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
// Attach event
|
// Attach event
|
||||||
document.getElementById('form_trust').addEventListener('submit', form_create_trust)
|
document.getElementById('form_trust').addEventListener('submit', create_trust)
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% block profile_content %}
|
{% block profile_content %}
|
||||||
<div class="card bg-light mb-3">
|
<div class="card bg-light mb-3">
|
||||||
<h3 class="card-header text-center">
|
<h3 class="card-header text-center">
|
||||||
{% trans "Add friends" %}
|
{% trans "Note friendships" %}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if can_create %}
|
{% if can_create %}
|
||||||
@ -24,7 +24,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% render_table trusting %}
|
{% render_table trusting %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-warning card mb-3">
|
<div class="alert alert-warning card">
|
||||||
{% blocktrans trimmed %}
|
{% blocktrans trimmed %}
|
||||||
Adding someone as a friend enables them to initiate transactions coming
|
Adding someone as a friend enables them to initiate transactions coming
|
||||||
from your account (while keeping your balance positive). This is
|
from your account (while keeping your balance positive). This is
|
||||||
@ -33,13 +33,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
friends without needing additional rights among them.
|
friends without needing additional rights among them.
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card bg-light mb-3">
|
|
||||||
<h3 class="card-header text-center">
|
|
||||||
{% trans "People having you as a friend" %}
|
|
||||||
</h3>
|
|
||||||
{% render_table trusted_by %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
@ -183,7 +183,7 @@ class TestMemberships(TestCase):
|
|||||||
club = Club.objects.get(name="Kfet")
|
club = Club.objects.get(name="Kfet")
|
||||||
else:
|
else:
|
||||||
club = Club.objects.create(
|
club = Club.objects.create(
|
||||||
name="Second club without BDE",
|
name="Second club " + ("with BDE" if bde_parent else "without BDE"),
|
||||||
parent_club=None,
|
parent_club=None,
|
||||||
email="newclub@example.com",
|
email="newclub@example.com",
|
||||||
require_memberships=True,
|
require_memberships=True,
|
||||||
@ -335,7 +335,6 @@ class TestMemberships(TestCase):
|
|||||||
ml_sports_registration=True,
|
ml_sports_registration=True,
|
||||||
ml_art_registration=True,
|
ml_art_registration=True,
|
||||||
report_frequency=7,
|
report_frequency=7,
|
||||||
VSS_charter_read=True
|
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
self.assertTrue(User.objects.filter(username="toto changed").exists())
|
self.assertTrue(User.objects.filter(username="toto changed").exists())
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -8,6 +8,7 @@ from django.contrib.auth import logout
|
|||||||
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
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LoginView
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
@ -20,7 +21,7 @@ from django_tables2.views import SingleTableView
|
|||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
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
|
||||||
from note_kfet.middlewares import _set_current_request
|
from note_kfet.middlewares import _set_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.models import Role
|
from permission.models import Role
|
||||||
@ -257,18 +258,17 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
note = context['object'].note
|
note = context['object'].note
|
||||||
context["trusting"] = TrustTable(
|
context["trusting"] = TrustTable(
|
||||||
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||||
context["trusted_by"] = TrustedTable(
|
|
||||||
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
|
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
|
||||||
))
|
))
|
||||||
context["widget"] = {
|
context["widget"] = {
|
||||||
"name": "trusted",
|
"name": "trusted",
|
||||||
"resetable": True,
|
|
||||||
"attrs": {
|
"attrs": {
|
||||||
|
"model_pk": ContentType.objects.get_for_model(Alias).pk,
|
||||||
"class": "autocomplete form-control",
|
"class": "autocomplete form-control",
|
||||||
"id": "trusted",
|
"id": "trusted",
|
||||||
|
"resetable": True,
|
||||||
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
||||||
"name_field": "name",
|
"name_field": "name",
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
@ -753,10 +753,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
club = old_membership.club
|
club = old_membership.club
|
||||||
user = old_membership.user
|
user = old_membership.user
|
||||||
|
|
||||||
# Update club membership date
|
|
||||||
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
|
|
||||||
club.update_membership_dates()
|
|
||||||
|
|
||||||
form.instance.club = club
|
form.instance.club = club
|
||||||
|
|
||||||
# Get form data
|
# Get form data
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -7,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
|
|||||||
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
||||||
from note_kfet.admin import admin_site
|
from note_kfet.admin import admin_site
|
||||||
|
|
||||||
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
|
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
||||||
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
|
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
|
||||||
RecurrentTransaction, MembershipTransaction, SpecialTransaction
|
RecurrentTransaction, MembershipTransaction, SpecialTransaction
|
||||||
from .templatetags.pretty_money import pretty_money
|
from .templatetags.pretty_money import pretty_money
|
||||||
@ -21,16 +21,6 @@ class AliasInlines(admin.TabularInline):
|
|||||||
model = Alias
|
model = Alias
|
||||||
|
|
||||||
|
|
||||||
class TrustInlines(admin.TabularInline):
|
|
||||||
"""
|
|
||||||
Define trusts when editing the trusting note
|
|
||||||
"""
|
|
||||||
model = Trust
|
|
||||||
fk_name = "trusting"
|
|
||||||
extra = 0
|
|
||||||
readonly_fields = ("trusted",)
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Note, site=admin_site)
|
@admin.register(Note, site=admin_site)
|
||||||
class NoteAdmin(PolymorphicParentModelAdmin):
|
class NoteAdmin(PolymorphicParentModelAdmin):
|
||||||
"""
|
"""
|
||||||
@ -102,7 +92,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
|
|||||||
"""
|
"""
|
||||||
Child for an user note, see NoteAdmin
|
Child for an user note, see NoteAdmin
|
||||||
"""
|
"""
|
||||||
inlines = (AliasInlines, TrustInlines)
|
inlines = (AliasInlines,)
|
||||||
|
|
||||||
# We can't change user after creation or the balance
|
# We can't change user after creation or the balance
|
||||||
readonly_fields = ('user', 'balance')
|
readonly_fields = ('user', 'balance')
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -11,7 +11,6 @@ from member.models import Membership
|
|||||||
from note_kfet.middlewares import get_current_request
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from rest_framework.utils import model_meta
|
from rest_framework.utils import model_meta
|
||||||
from rest_framework.validators import UniqueTogetherValidator
|
|
||||||
|
|
||||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
|
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
|
||||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
||||||
@ -87,9 +86,11 @@ class TrustSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Trust
|
model = Trust
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
validators = [UniqueTogetherValidator(
|
|
||||||
queryset=Trust.objects.all(), fields=('trusting', 'trusted'),
|
def validate(self, attrs):
|
||||||
message=_("This friendship already exists"))]
|
instance = Trust(**attrs)
|
||||||
|
instance.clean()
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class AliasSerializer(serializers.ModelSerializer):
|
class AliasSerializer(serializers.ModelSerializer):
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
@ -293,11 +293,6 @@ class Alias(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.clean()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def normalize(string):
|
def normalize(string):
|
||||||
"""
|
"""
|
||||||
@ -326,6 +321,11 @@ class Alias(models.Model):
|
|||||||
pass
|
pass
|
||||||
self.normalized_name = normalized_name
|
self.normalized_name = normalized_name
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def delete(self, using=None, keep_parents=False):
|
def delete(self, using=None, keep_parents=False):
|
||||||
if self.name == str(self.note):
|
if self.name == str(self.note):
|
||||||
raise ValidationError(_("You can't delete your main alias."),
|
raise ValidationError(_("You can't delete your main alias."),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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 ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
@ -59,7 +59,6 @@ class TransactionTemplate(models.Model):
|
|||||||
amount = models.PositiveIntegerField(
|
amount = models.PositiveIntegerField(
|
||||||
verbose_name=_('amount'),
|
verbose_name=_('amount'),
|
||||||
)
|
)
|
||||||
|
|
||||||
category = models.ForeignKey(
|
category = models.ForeignKey(
|
||||||
TemplateCategory,
|
TemplateCategory,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -88,12 +87,12 @@ class TransactionTemplate(models.Model):
|
|||||||
verbose_name = _("transaction template")
|
verbose_name = _("transaction template")
|
||||||
verbose_name_plural = _("transaction templates")
|
verbose_name_plural = _("transaction templates")
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('note:template_update', args=(self.pk,))
|
return reverse('note:template_update', args=(self.pk,))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Transaction(PolymorphicModel):
|
class Transaction(PolymorphicModel):
|
||||||
"""
|
"""
|
||||||
@ -102,6 +101,7 @@ class Transaction(PolymorphicModel):
|
|||||||
amount is store in centimes of currency, making it a positive integer
|
amount is store in centimes of currency, making it a positive integer
|
||||||
value. (from someone to someone else)
|
value. (from someone to someone else)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
source = models.ForeignKey(
|
source = models.ForeignKey(
|
||||||
Note,
|
Note,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -166,50 +166,6 @@ class Transaction(PolymorphicModel):
|
|||||||
models.Index(fields=['destination']),
|
models.Index(fields=['destination']),
|
||||||
]
|
]
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\
|
|
||||||
+ pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid")
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
When saving, also transfer money between two notes
|
|
||||||
"""
|
|
||||||
if self.source.pk == self.destination.pk:
|
|
||||||
# When source == destination, no money is transferred and no transaction is created
|
|
||||||
return
|
|
||||||
|
|
||||||
self.source = Note.objects.select_for_update().get(pk=self.source_id)
|
|
||||||
self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
|
|
||||||
|
|
||||||
# Check that the amounts stay between big integer bounds
|
|
||||||
diff_source, diff_dest = self.validate()
|
|
||||||
|
|
||||||
if not (hasattr(self, '_force_save') and self._force_save) \
|
|
||||||
and (not self.source.is_active or not self.destination.is_active):
|
|
||||||
raise ValidationError(_("The transaction can't be saved since the source note "
|
|
||||||
"or the destination note is not active."))
|
|
||||||
|
|
||||||
# If the aliases are not entered, we assume that the used alias is the name of the note
|
|
||||||
if not self.source_alias:
|
|
||||||
self.source_alias = str(self.source)
|
|
||||||
|
|
||||||
if not self.destination_alias:
|
|
||||||
self.destination_alias = str(self.destination)
|
|
||||||
|
|
||||||
# We save first the transaction, in case of the user has no right to transfer money
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
# Save notes
|
|
||||||
self.source.refresh_from_db()
|
|
||||||
self.source.balance += diff_source
|
|
||||||
self.source._force_save = True
|
|
||||||
self.source.save()
|
|
||||||
self.destination.refresh_from_db()
|
|
||||||
self.destination.balance += diff_dest
|
|
||||||
self.destination._force_save = True
|
|
||||||
self.destination.save()
|
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
previous_source_balance = self.source.balance
|
previous_source_balance = self.source.balance
|
||||||
previous_dest_balance = self.destination.balance
|
previous_dest_balance = self.destination.balance
|
||||||
@ -252,6 +208,46 @@ class Transaction(PolymorphicModel):
|
|||||||
|
|
||||||
return source_balance - previous_source_balance, dest_balance - previous_dest_balance
|
return source_balance - previous_source_balance, dest_balance - previous_dest_balance
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
When saving, also transfer money between two notes
|
||||||
|
"""
|
||||||
|
if self.source.pk == self.destination.pk:
|
||||||
|
# When source == destination, no money is transferred and no transaction is created
|
||||||
|
return
|
||||||
|
|
||||||
|
self.source = Note.objects.select_for_update().get(pk=self.source_id)
|
||||||
|
self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
|
||||||
|
|
||||||
|
# Check that the amounts stay between big integer bounds
|
||||||
|
diff_source, diff_dest = self.validate()
|
||||||
|
|
||||||
|
if not (hasattr(self, '_force_save') and self._force_save) \
|
||||||
|
and (not self.source.is_active or not self.destination.is_active):
|
||||||
|
raise ValidationError(_("The transaction can't be saved since the source note "
|
||||||
|
"or the destination note is not active."))
|
||||||
|
|
||||||
|
# If the aliases are not entered, we assume that the used alias is the name of the note
|
||||||
|
if not self.source_alias:
|
||||||
|
self.source_alias = str(self.source)
|
||||||
|
|
||||||
|
if not self.destination_alias:
|
||||||
|
self.destination_alias = str(self.destination)
|
||||||
|
|
||||||
|
# We save first the transaction, in case of the user has no right to transfer money
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Save notes
|
||||||
|
self.source.refresh_from_db()
|
||||||
|
self.source.balance += diff_source
|
||||||
|
self.source._force_save = True
|
||||||
|
self.source.save()
|
||||||
|
self.destination.refresh_from_db()
|
||||||
|
self.destination.balance += diff_dest
|
||||||
|
self.destination._force_save = True
|
||||||
|
self.destination.save()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def total(self):
|
def total(self):
|
||||||
return self.amount * self.quantity
|
return self.amount * self.quantity
|
||||||
@ -260,40 +256,46 @@ class Transaction(PolymorphicModel):
|
|||||||
def type(self):
|
def type(self):
|
||||||
return _('Transfer')
|
return _('Transfer')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\
|
||||||
|
+ pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid")
|
||||||
|
|
||||||
|
|
||||||
class RecurrentTransaction(Transaction):
|
class RecurrentTransaction(Transaction):
|
||||||
"""
|
"""
|
||||||
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
|
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template = models.ForeignKey(
|
template = models.ForeignKey(
|
||||||
TransactionTemplate,
|
TransactionTemplate,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("recurrent transaction")
|
|
||||||
verbose_name_plural = _("recurrent transactions")
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.clean()
|
|
||||||
return super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save):
|
if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_("The destination of this transaction must equal to the destination of the template."))
|
_("The destination of this transaction must equal to the destination of the template."))
|
||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.clean()
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _('Template')
|
return _('Template')
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("recurrent transaction")
|
||||||
|
verbose_name_plural = _("recurrent transactions")
|
||||||
|
|
||||||
|
|
||||||
class SpecialTransaction(Transaction):
|
class SpecialTransaction(Transaction):
|
||||||
"""
|
"""
|
||||||
Special type of :model:`note.Transaction` associated to transactions with special notes
|
Special type of :model:`note.Transaction` associated to transactions with special notes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
last_name = models.CharField(
|
last_name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("name"),
|
verbose_name=_("name"),
|
||||||
@ -310,15 +312,6 @@ class SpecialTransaction(Transaction):
|
|||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("Special transaction")
|
|
||||||
verbose_name_plural = _("Special transactions")
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def save(self, *args, **kwargs):
|
|
||||||
self.clean()
|
|
||||||
super().save(*args, **kwargs)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
|
return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
|
||||||
@ -332,8 +325,13 @@ class SpecialTransaction(Transaction):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
# SpecialTransaction are only possible with NoteSpecial object
|
# SpecialTransaction are only possible with NoteSpecial object
|
||||||
if self.is_credit() == self.is_debit():
|
if self.is_credit() == self.is_debit():
|
||||||
raise ValidationError(_("A special transaction is only possible between a"
|
raise(ValidationError(_("A special transaction is only possible between a"
|
||||||
" Note associated to a payment method and a User or a Club"))
|
" Note associated to a payment method and a User or a Club")))
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
self.clean()
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def validate_payment_form(form):
|
def validate_payment_form(form):
|
||||||
@ -365,11 +363,17 @@ class SpecialTransaction(Transaction):
|
|||||||
|
|
||||||
return not error
|
return not error
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Special transaction")
|
||||||
|
verbose_name_plural = _("Special transactions")
|
||||||
|
|
||||||
|
|
||||||
class MembershipTransaction(Transaction):
|
class MembershipTransaction(Transaction):
|
||||||
"""
|
"""
|
||||||
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
|
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
membership = models.OneToOneField(
|
membership = models.OneToOneField(
|
||||||
'member.Membership',
|
'member.Membership',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
// Copyright (C) 2018-2021 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.
|
||||||
@ -221,7 +221,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
|||||||
.done(function () {
|
.done(function () {
|
||||||
if (!isNaN(source.balance)) {
|
if (!isNaN(source.balance)) {
|
||||||
const newBalance = source.balance - quantity * amount
|
const newBalance = source.balance - quantity * amount
|
||||||
if (newBalance <= -2000) {
|
if (newBalance <= -5000) {
|
||||||
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
||||||
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
|
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
|
||||||
} else if (newBalance < 0) {
|
} else if (newBalance < 0) {
|
||||||
@ -258,39 +258,3 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var searchbar = document.getElementById("search-input")
|
|
||||||
var search_results = document.getElementById("search-results")
|
|
||||||
|
|
||||||
var old_pattern = null;
|
|
||||||
var firstMatch = null;
|
|
||||||
/**
|
|
||||||
* Updates the button search tab
|
|
||||||
* @param force Forces the update even if the pattern didn't change
|
|
||||||
*/
|
|
||||||
function updateSearch(force = false) {
|
|
||||||
let pattern = searchbar.value
|
|
||||||
if (pattern === "")
|
|
||||||
firstMatch = null;
|
|
||||||
if ((pattern === old_pattern || pattern === "") && !force)
|
|
||||||
return;
|
|
||||||
firstMatch = null;
|
|
||||||
const re = new RegExp(pattern, "i");
|
|
||||||
Array.from(search_results.children).forEach(function(b) {
|
|
||||||
if (re.test(b.innerText)) {
|
|
||||||
b.hidden = false;
|
|
||||||
if (firstMatch === null) {
|
|
||||||
firstMatch = b;
|
|
||||||
}
|
|
||||||
} else
|
|
||||||
b.hidden = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
searchbar.addEventListener("input", function (e) {
|
|
||||||
debounce(updateSearch)()
|
|
||||||
});
|
|
||||||
searchbar.addEventListener("keyup", function (e) {
|
|
||||||
if (firstMatch && e.key === "Enter")
|
|
||||||
firstMatch.click()
|
|
||||||
});
|
|
||||||
|
@ -314,7 +314,7 @@ $('#btn_transfer').click(function () {
|
|||||||
|
|
||||||
if (!isNaN(source.note.balance)) {
|
if (!isNaN(source.note.balance)) {
|
||||||
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
|
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
|
||||||
if (newBalance <= -2000) {
|
if (newBalance <= -5000) {
|
||||||
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
|
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
|
||||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
|
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
|
||||||
reset()
|
reset()
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -159,11 +159,11 @@ class TrustTable(tables.Table):
|
|||||||
template_name = 'django_tables2/bootstrap4.html'
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
|
|
||||||
show_header = False
|
show_header = False
|
||||||
trusted = tables.Column(attrs={'td': {'class': 'text-center'}})
|
trusted = tables.Column(attrs={'td': {'class': 'text_center'}})
|
||||||
|
|
||||||
delete_col = tables.TemplateColumn(
|
delete_col = tables.TemplateColumn(
|
||||||
template_code=DELETE_TEMPLATE,
|
template_code=DELETE_TEMPLATE,
|
||||||
extra_context={"delete_trans": _('Delete')},
|
extra_context={"delete_trans": _('delete')},
|
||||||
attrs={
|
attrs={
|
||||||
'td': {
|
'td': {
|
||||||
'class': lambda record: 'col-sm-1'
|
'class': lambda record: 'col-sm-1'
|
||||||
@ -173,46 +173,6 @@ class TrustTable(tables.Table):
|
|||||||
verbose_name=_("Delete"),)
|
verbose_name=_("Delete"),)
|
||||||
|
|
||||||
|
|
||||||
class TrustedTable(tables.Table):
|
|
||||||
class Meta:
|
|
||||||
attrs = {
|
|
||||||
'class': 'table table condensed table-striped',
|
|
||||||
'id': 'trusted_table'
|
|
||||||
}
|
|
||||||
Model = Trust
|
|
||||||
fields = ("trusting",)
|
|
||||||
template_name = "django_tables2/bootstrap4.html"
|
|
||||||
|
|
||||||
show_header = False
|
|
||||||
trusting = tables.Column(attrs={
|
|
||||||
'td': {'class': 'text-center', 'width': '100%'}})
|
|
||||||
|
|
||||||
trust_back = tables.Column(
|
|
||||||
verbose_name=_("Trust back"),
|
|
||||||
accessor="pk",
|
|
||||||
attrs={
|
|
||||||
'td': {
|
|
||||||
'class': '',
|
|
||||||
'id': lambda record: "trust_back_" + str(record.pk),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def render_trust_back(self, record):
|
|
||||||
user_note = record.trusted
|
|
||||||
trusting_note = record.trusting
|
|
||||||
if Trust.objects.filter(trusted=trusting_note, trusting=user_note):
|
|
||||||
return ""
|
|
||||||
val = '<button id="'
|
|
||||||
val += str(record.pk)
|
|
||||||
val += '" class="btn btn-success btn-sm text-nowrap" \
|
|
||||||
onclick="create_trust(' + str(record.trusted.pk) + ',' + \
|
|
||||||
str(record.trusting.pk) + ')">'
|
|
||||||
val += str(_("Add back"))
|
|
||||||
val += '</button>'
|
|
||||||
return mark_safe(val)
|
|
||||||
|
|
||||||
|
|
||||||
class AliasTable(tables.Table):
|
class AliasTable(tables.Table):
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
|
@ -103,11 +103,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link font-weight-bold" data-toggle="tab" href="#search">
|
|
||||||
{% trans "Search" %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -128,20 +123,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="tab-pane" id="search">
|
|
||||||
<input class="form-control mx-auto d-block mb-3"
|
|
||||||
placeholder="{% trans "Search button..." %}" type="search" id="search-input"/>
|
|
||||||
<div class="d-inline-flex flex-wrap justify-content-center" id="search-results">
|
|
||||||
{% for button in all_buttons %}
|
|
||||||
{% if button.display %}
|
|
||||||
<button class="btn btn-outline-dark rounded-0 flex-fill" hidden
|
|
||||||
id="search_button{{ button.id }}" name="button" value="{{ button.name }}">
|
|
||||||
{{ button.name }} ({{ button.amount | pretty_money }})
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -182,7 +163,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
{% for button in highlighted %}
|
{% for button in highlighted %}
|
||||||
{% if button.display %}
|
{% if button.display %}
|
||||||
document.getElementById("highlighted_button{{ button.id }}").addEventListener("click", function() {
|
$("#highlighted_button{{ button.id }}").click(function() {
|
||||||
addConso({{ button.destination_id }}, {{ button.amount }},
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
{{ button.id }}, "{{ button.name|escapejs }}");
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
@ -193,7 +174,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
{% for button in category.templates_filtered %}
|
{% for button in category.templates_filtered %}
|
||||||
{% if button.display %}
|
{% if button.display %}
|
||||||
document.getElementById("button{{ button.id }}").addEventListener("click", function() {
|
$("#button{{ button.id }}").click(function() {
|
||||||
addConso({{ button.destination_id }}, {{ button.amount }},
|
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||||
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||||
{{ button.id }}, "{{ button.name|escapejs }}");
|
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||||
@ -201,15 +182,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% for button in all_buttons %}
|
|
||||||
{% if button.display %}
|
|
||||||
document.getElementById("search_button{{ button.id }}").addEventListener("click", function() {
|
|
||||||
addConso({{ button.destination_id }}, {{ button.amount }},
|
|
||||||
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
|
||||||
{{ button.id }}, "{{ button.name|escapejs }}");
|
|
||||||
});
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -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érent·e·s dont le solde
|
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
||||||
est inférieur à 0 € depuis plus de 24h.
|
est inférieur à 0 € 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
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -10,12 +10,12 @@ from django.core.exceptions import PermissionDenied
|
|||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, UpdateView, DetailView
|
from django.views.generic import CreateView, UpdateView, DetailView
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
|
from django.urls import reverse_lazy
|
||||||
from activity.models import Entry
|
from activity.models import Entry
|
||||||
|
from note_kfet.inputs import AmountInput
|
||||||
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 .forms import TransactionTemplateForm, SearchTransactionForm
|
from .forms import TransactionTemplateForm, SearchTransactionForm
|
||||||
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note
|
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note
|
||||||
@ -190,10 +190,6 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
).order_by('name').all()
|
).order_by('name').all()
|
||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
||||||
|
|
||||||
context['all_buttons'] = TransactionTemplate.objects.filter(
|
|
||||||
PermissionBackend.filter_queryset(self.request, TransactionTemplate, "view")
|
|
||||||
).filter(display=True).order_by('name').all()
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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 api.viewsets import ReadOnlyProtectedModelViewSet
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
@ -198,41 +198,6 @@ class PermissionBackend(ModelBackend):
|
|||||||
def has_module_perms(self, user_obj, app_label):
|
def has_module_perms(self, user_obj, app_label):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@memoize
|
|
||||||
def has_model_perm(request, model, type):
|
|
||||||
"""
|
|
||||||
Check is the given user has the permission over a given model for a given action.
|
|
||||||
The result is then memoized.
|
|
||||||
:param request: The current request
|
|
||||||
:param model: The model that the permissions shoud apply
|
|
||||||
:param type: The type of the permissions: view, change, add or delete
|
|
||||||
For view action, it is consider possible if user can view or change the model
|
|
||||||
"""
|
|
||||||
# Requested by a shell
|
|
||||||
if request is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
user_obj = request.user
|
|
||||||
sess = request.session
|
|
||||||
|
|
||||||
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
|
||||||
# OAuth2 Authentication
|
|
||||||
user_obj = request.auth.user
|
|
||||||
|
|
||||||
if user_obj is None or user_obj.is_anonymous:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
|
|
||||||
return True
|
|
||||||
|
|
||||||
ct = ContentType.objects.get_for_model(model)
|
|
||||||
if any(PermissionBackend.permissions(request, ct, type)):
|
|
||||||
return True
|
|
||||||
if type == "view" and any(PermissionBackend.permissions(request, ct, "change")):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_all_permissions(self, user_obj, obj=None):
|
def get_all_permissions(self, user_obj, obj=None):
|
||||||
ct = ContentType.objects.get_for_model(obj)
|
ct = ContentType.objects.get_for_model(obj)
|
||||||
return list(self.permissions(get_current_request(), ct, "view"))
|
return list(self.permissions(get_current_request(), ct, "view"))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,19 +0,0 @@
|
|||||||
# Generated by Django 2.2.28 on 2023-07-24 10:15
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('permission', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='role',
|
|
||||||
name='for_club',
|
|
||||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='for club'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
@ -26,15 +26,6 @@ class InstancedPermission:
|
|||||||
self.mask = mask
|
self.mask = mask
|
||||||
self.kwargs = kwargs
|
self.kwargs = kwargs
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if self.field:
|
|
||||||
return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
|
|
||||||
else:
|
|
||||||
return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
def applies(self, obj, permission_type, field_name=None):
|
def applies(self, obj, permission_type, field_name=None):
|
||||||
"""
|
"""
|
||||||
Returns True if the permission applies to
|
Returns True if the permission applies to
|
||||||
@ -93,11 +84,21 @@ class InstancedPermission:
|
|||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
self.query = Permission._about(self.raw_query, **self.kwargs)
|
self.query = Permission._about(self.raw_query, **self.kwargs)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.field:
|
||||||
|
return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query)
|
||||||
|
else:
|
||||||
|
return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__repr__()
|
||||||
|
|
||||||
|
|
||||||
class PermissionMask(models.Model):
|
class PermissionMask(models.Model):
|
||||||
"""
|
"""
|
||||||
Permissions that are hidden behind a mask
|
Permissions that are hidden behind a mask
|
||||||
"""
|
"""
|
||||||
|
|
||||||
rank = models.PositiveSmallIntegerField(
|
rank = models.PositiveSmallIntegerField(
|
||||||
unique=True,
|
unique=True,
|
||||||
verbose_name=_('rank'),
|
verbose_name=_('rank'),
|
||||||
@ -109,13 +110,13 @@ class PermissionMask(models.Model):
|
|||||||
verbose_name=_('description'),
|
verbose_name=_('description'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.description
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("permission mask")
|
verbose_name = _("permission mask")
|
||||||
verbose_name_plural = _("permission masks")
|
verbose_name_plural = _("permission masks")
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.description
|
|
||||||
|
|
||||||
|
|
||||||
class Permission(models.Model):
|
class Permission(models.Model):
|
||||||
|
|
||||||
@ -193,19 +194,16 @@ class Permission(models.Model):
|
|||||||
verbose_name = _("permission")
|
verbose_name = _("permission")
|
||||||
verbose_name_plural = _("permissions")
|
verbose_name_plural = _("permissions")
|
||||||
|
|
||||||
def __str__(self):
|
def clean(self):
|
||||||
return self.description
|
self.query = json.dumps(json.loads(self.query))
|
||||||
|
if self.field and self.type not in {'view', 'change'}:
|
||||||
|
raise ValidationError(_("Specifying field applies only to view and change permission types."))
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, **kwargs):
|
def save(self, **kwargs):
|
||||||
self.full_clean()
|
self.full_clean()
|
||||||
super().save()
|
super().save()
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
self.query = json.dumps(json.loads(self.query))
|
|
||||||
if self.field and self.type not in {'view', 'change'}:
|
|
||||||
raise ValidationError(_("Specifying field applies only to view and change permission types."))
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute_f(oper, **kwargs):
|
def compute_f(oper, **kwargs):
|
||||||
if isinstance(oper, list):
|
if isinstance(oper, list):
|
||||||
@ -319,6 +317,9 @@ class Permission(models.Model):
|
|||||||
# query = self._about(query, **kwargs)
|
# query = self._about(query, **kwargs)
|
||||||
return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs)
|
return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.description
|
||||||
|
|
||||||
|
|
||||||
class Role(models.Model):
|
class Role(models.Model):
|
||||||
"""
|
"""
|
||||||
@ -338,14 +339,13 @@ class Role(models.Model):
|
|||||||
"member.Club",
|
"member.Club",
|
||||||
verbose_name=_("for club"),
|
verbose_name=_("for club"),
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
blank=True,
|
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("role permissions")
|
verbose_name = _("role permissions")
|
||||||
verbose_name_plural = _("role permissions")
|
verbose_name_plural = _("role permissions")
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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-2024 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 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
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user