mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 01:48:21 +02:00
Compare commits
8 Commits
a3073ba5a5
...
inclusive
Author | SHA1 | Date | |
---|---|---|---|
5c77b6ebfa
|
|||
92610c5ffd
|
|||
ddbdd5b5d0
|
|||
e3ee39ca81
|
|||
0c1c845f72
|
|||
edcf42beb6
|
|||
3306aed6dc
|
|||
a69573ccdb
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,7 +42,6 @@ map.json
|
||||
backups/
|
||||
/static/
|
||||
/media/
|
||||
/tmp/
|
||||
|
||||
# Virtualenv
|
||||
env/
|
||||
|
@ -55,7 +55,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
|
||||
(env)$ ./manage.py makemigrations
|
||||
(env)$ ./manage.py migrate
|
||||
(env)$ ./manage.py loaddata initial
|
||||
(env)$ ./manage.py createsuperuser # Création d'un utilisateur initial
|
||||
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
|
||||
```
|
||||
|
||||
6. Enjoy :
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -6,7 +6,7 @@
|
||||
"name": "Pot",
|
||||
"manage_entries": true,
|
||||
"can_invite": true,
|
||||
"guest_entry_fee": 1000
|
||||
"guest_entry_fee": 500
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -28,25 +28,5 @@
|
||||
"can_invite": false,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "activity.activitytype",
|
||||
"pk": 5,
|
||||
"fields": {
|
||||
"name": "Soir\u00e9e avec entrées",
|
||||
"manage_entries": true,
|
||||
"can_invite": false,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "activity.activitytype",
|
||||
"pk": 7,
|
||||
"fields": {
|
||||
"name": "Soir\u00e9e avec invitations",
|
||||
"manage_entries": true,
|
||||
"can_invite": true,
|
||||
"guest_entry_fee": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -1,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
|
||||
|
||||
from datetime import timedelta
|
||||
@ -26,7 +26,7 @@ class ActivityForm(forms.ModelForm):
|
||||
clubs = list(Club.objects.filter(PermissionBackend
|
||||
.filter_queryset(get_current_request(), Club, "view")).all())
|
||||
shuffle(clubs)
|
||||
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ",…"
|
||||
|
||||
def clean_organizer(self):
|
||||
organizer = self.cleaned_data['organizer']
|
||||
@ -53,7 +53,7 @@ class ActivityForm(forms.ModelForm):
|
||||
model=Note,
|
||||
attrs={
|
||||
"api_url": "/api/note/note/",
|
||||
'placeholder': 'Note de l\'événement sur laquelle envoyer les crédits d\'invitation ...'
|
||||
'placeholder': 'Note de l\'événement sur laquelle envoyer les crédits d\'invitation…'
|
||||
},
|
||||
),
|
||||
"attendees_club": Autocomplete(
|
||||
@ -115,7 +115,7 @@ class GuestForm(forms.ModelForm):
|
||||
# We don't evaluate the content type at launch because the DB might be not initialized
|
||||
'api_url_suffix':
|
||||
lambda: '&polymorphic_ctype=' + str(ContentType.objects.get_for_model(NoteUser).pk),
|
||||
'placeholder': 'Note ...',
|
||||
'placeholder': 'Note…',
|
||||
},
|
||||
),
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
import os
|
||||
@ -66,8 +66,6 @@ class Activity(models.Model):
|
||||
|
||||
description = models.TextField(
|
||||
verbose_name=_('description'),
|
||||
blank=True,
|
||||
default="",
|
||||
)
|
||||
|
||||
location = models.CharField(
|
||||
@ -125,18 +123,10 @@ class Activity(models.Model):
|
||||
verbose_name=_('open'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("activity")
|
||||
verbose_name_plural = _("activities")
|
||||
unique_together = ("name", "date_start", "date_end",)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Update the activity wiki page each time the activity is updated (validation, change description, ...)
|
||||
Update the activity wiki page each time the activity is updated (validation, change description,…)
|
||||
"""
|
||||
if self.date_end < self.date_start:
|
||||
raise ValidationError(_("The end date must be after the start date."))
|
||||
@ -154,6 +144,14 @@ class Activity(models.Model):
|
||||
if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else refresh_activities()
|
||||
return ret
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("activity")
|
||||
verbose_name_plural = _("activities")
|
||||
unique_together = ("name", "date_start", "date_end",)
|
||||
|
||||
|
||||
class Entry(models.Model):
|
||||
"""
|
||||
@ -254,13 +252,14 @@ class Guest(models.Model):
|
||||
verbose_name=_("inviter"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("guest")
|
||||
verbose_name_plural = _("guests")
|
||||
unique_together = ("activity", "last_name", "first_name", )
|
||||
|
||||
def __str__(self):
|
||||
return self.first_name + " " + self.last_name
|
||||
@property
|
||||
def has_entry(self):
|
||||
try:
|
||||
if self.entry:
|
||||
return True
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
@ -291,14 +290,13 @@ class Guest(models.Model):
|
||||
|
||||
return super().save(force_insert, force_update, using, update_fields)
|
||||
|
||||
@property
|
||||
def has_entry(self):
|
||||
try:
|
||||
if self.entry:
|
||||
return True
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
def __str__(self):
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("guest")
|
||||
verbose_name_plural = _("guests")
|
||||
unique_together = ("activity", "last_name", "first_name", )
|
||||
|
||||
|
||||
class GuestTransaction(Transaction):
|
||||
|
@ -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
|
||||
|
||||
from django.utils import timezone
|
||||
|
@ -37,7 +37,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<button class="btn btn-light">{% trans "Return to activity page" %}</button>
|
||||
</a>
|
||||
|
||||
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
|
||||
<input id="alias" type="text" class="form-control" placeholder="Nom/note…">
|
||||
|
||||
<hr>
|
||||
|
||||
|
@ -17,24 +17,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var date_end = document.getElementById("id_date_end");
|
||||
var date_start = document.getElementById("id_date_start");
|
||||
|
||||
function update_date_end (){
|
||||
if(date_end.value=="" || date_end.value<date_start.value){
|
||||
date_end.value = date_start.value;
|
||||
};
|
||||
};
|
||||
|
||||
function update_date_start (){
|
||||
if(date_start.value=="" || date_end.value<date_start.value){
|
||||
date_start.value = date_end.value;
|
||||
};
|
||||
};
|
||||
|
||||
date_start.addEventListener('focusout', update_date_end);
|
||||
date_end.addEventListener('focusout', update_date_start);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
from hashlib import md5
|
||||
@ -76,7 +76,6 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
||||
context['upcoming'] = ActivityTable(
|
||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
||||
prefix='upcoming-',
|
||||
order_by='date_start',
|
||||
)
|
||||
|
||||
started_activities = self.get_queryset().filter(open=True, valid=True).distinct().all()
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
@ -1,5 +0,0 @@
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
|
||||
|
||||
class CustomPagination(PageNumberPagination):
|
||||
page_size_query_param = 'page_size'
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
from django.conf import settings
|
||||
@ -76,6 +76,9 @@ class Changelog(models.Model):
|
||||
verbose_name=_('timestamp'),
|
||||
)
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
raise ValidationError(_("Logs cannot be destroyed."))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("changelog")
|
||||
verbose_name_plural = _("changelogs")
|
||||
@ -83,6 +86,3 @@ class Changelog(models.Model):
|
||||
def __str__(self):
|
||||
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
|
||||
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
raise ValidationError(_("Logs cannot be destroyed."))
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -56,13 +56,13 @@ def save_object(sender, instance, **kwargs):
|
||||
# noinspection PyProtectedMember
|
||||
previous = instance._previous
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
|
||||
request = get_current_request()
|
||||
|
||||
if request is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
|
||||
ip = "127.0.0.1"
|
||||
username = Alias.normalize(getpass.getuser())
|
||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||
@ -134,13 +134,13 @@ def delete_object(sender, instance, **kwargs):
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||
return
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
# Si un⋅e utilisateur⋅rice est connecté⋅e, on récupère l'utilisateur⋅rice courant⋅e ainsi que son adresse IP
|
||||
request = get_current_request()
|
||||
|
||||
if request is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||
# IMPORTANT : l'utilisateur⋅rice dans la VM doit être un des alias note du respo info
|
||||
ip = "127.0.0.1"
|
||||
username = Alias.normalize(getpass.getuser())
|
||||
note = NoteUser.objects.filter(alias__normalized_name=username)
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
import io
|
||||
@ -47,13 +47,6 @@ class ProfileForm(forms.ModelForm):
|
||||
|
||||
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
|
||||
|
||||
VSS_charter_read = forms.BooleanField(
|
||||
required=True,
|
||||
label=_("Anti-VSS (<em>Violences Sexistes et Sexuelles</em>) charter read and approved"),
|
||||
help_text=_("Tick after having read and accepted the anti-VSS charter \
|
||||
<a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available here in pdf</a>")
|
||||
)
|
||||
|
||||
def clean_promotion(self):
|
||||
promotion = self.cleaned_data["promotion"]
|
||||
if promotion > timezone.now().year:
|
||||
@ -121,7 +114,7 @@ class ImageForm(forms.Form):
|
||||
frame = frame.crop((x, y, x + w, y + h))
|
||||
frame = frame.resize(
|
||||
(settings.PIC_WIDTH, settings.PIC_RATIO * settings.PIC_WIDTH),
|
||||
Image.LANCZOS,
|
||||
Image.ANTIALIAS,
|
||||
)
|
||||
frames.append(frame)
|
||||
|
||||
@ -207,9 +200,9 @@ class MembershipForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ('user', 'date_start')
|
||||
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
||||
# Le champ d'utilisateur⋅rice est remplacé par un champ d'auto-complétion.
|
||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||
# et récupère les noms d'utilisateur valides
|
||||
# et récupère les noms d'utilisateur⋅rices valides
|
||||
widgets = {
|
||||
'user':
|
||||
Autocomplete(
|
||||
@ -217,7 +210,7 @@ class MembershipForm(forms.ModelForm):
|
||||
attrs={
|
||||
'api_url': '/api/user/',
|
||||
'name_field': 'username',
|
||||
'placeholder': 'Nom ...',
|
||||
'placeholder': 'Nom…',
|
||||
},
|
||||
),
|
||||
'date_start': DatePickerInput(),
|
||||
@ -234,7 +227,7 @@ class MembershipRolesForm(forms.ModelForm):
|
||||
attrs={
|
||||
'api_url': '/api/user/',
|
||||
'name_field': 'username',
|
||||
'placeholder': 'Nom ...',
|
||||
'placeholder': 'Nom…',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
import hashlib
|
||||
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.26 on 2022-09-04 21:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0008_auto_20211005_1544'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='promotion',
|
||||
field=models.PositiveSmallIntegerField(default=2022, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.28 on 2023-08-23 21:29
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0009_auto_20220904_2325'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='profile',
|
||||
name='promotion',
|
||||
field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
||||
),
|
||||
]
|
@ -1,18 +0,0 @@
|
||||
# Generated by Django 2.2.28 on 2023-08-31 09:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('member', '0010_new_default_year'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='profile',
|
||||
name='VSS_charter_read',
|
||||
field=models.BooleanField(default=False, verbose_name='VSS charter read'),
|
||||
),
|
||||
]
|
@ -1,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
|
||||
|
||||
import datetime
|
||||
@ -28,6 +28,7 @@ class Profile(models.Model):
|
||||
We do not want to patch the Django Contrib :model:`auth.User`model;
|
||||
so this model add an user profile with additional information.
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
@ -133,22 +134,6 @@ class Profile(models.Model):
|
||||
default=False,
|
||||
)
|
||||
|
||||
VSS_charter_read = models.BooleanField(
|
||||
verbose_name=_("VSS charter read"),
|
||||
default=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user profile')
|
||||
verbose_name_plural = _('user profile')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('member:user_detail', args=(self.user_id,))
|
||||
|
||||
@property
|
||||
def ens_year(self):
|
||||
"""
|
||||
@ -173,6 +158,17 @@ class Profile(models.Model):
|
||||
return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists()
|
||||
return False
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user profile')
|
||||
verbose_name_plural = _('user profile')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('member:user_detail', args=(self.user_id,))
|
||||
|
||||
def __str__(self):
|
||||
return str(self.user)
|
||||
|
||||
def send_email_validation_link(self):
|
||||
subject = "[Note Kfet] " + str(_("Activate your Note Kfet account"))
|
||||
token = email_validation_token.make_token(self.user)
|
||||
@ -204,11 +200,9 @@ class Club(models.Model):
|
||||
max_length=255,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
email = models.EmailField(
|
||||
verbose_name=_('email'),
|
||||
)
|
||||
|
||||
parent_club = models.ForeignKey(
|
||||
'self',
|
||||
null=True,
|
||||
@ -259,12 +253,25 @@ class Club(models.Model):
|
||||
help_text=_('Maximal date of a membership, after which members must renew it.'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("club")
|
||||
verbose_name_plural = _("clubs")
|
||||
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
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
today = datetime.date.today()
|
||||
|
||||
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
|
||||
def save(self, force_insert=False, force_update=False, using=None,
|
||||
@ -277,29 +284,16 @@ class Club(models.Model):
|
||||
self.membership_end = None
|
||||
super().save(force_insert, force_update, update_fields)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("club")
|
||||
verbose_name_plural = _("clubs")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy('member:club_detail', args=(self.pk,))
|
||||
|
||||
def update_membership_dates(self):
|
||||
"""
|
||||
This function is called each time the club detail view is displayed.
|
||||
Update the year of the membership dates.
|
||||
"""
|
||||
if not self.membership_start or not self.membership_end:
|
||||
return
|
||||
|
||||
today = datetime.date.today()
|
||||
|
||||
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):
|
||||
"""
|
||||
@ -339,66 +333,6 @@ class Membership(models.Model):
|
||||
verbose_name=_('fee'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('membership')
|
||||
verbose_name_plural = _('memberships')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
||||
def __str__(self):
|
||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||
"""
|
||||
# Ensure that club membership dates are valid
|
||||
old_membership_start = self.club.membership_start
|
||||
self.club.update_membership_dates()
|
||||
if self.club.membership_start != old_membership_start:
|
||||
self.club.save()
|
||||
|
||||
created = not self.pk
|
||||
if not created:
|
||||
for role in self.roles.all():
|
||||
club = role.for_club
|
||||
if club is not None:
|
||||
if club.pk != self.club_id:
|
||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||
.format(role=role.name, club=club.name))
|
||||
else:
|
||||
if Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club,
|
||||
date_start__lte=self.date_start,
|
||||
date_end__gte=self.date_start,
|
||||
).exists():
|
||||
raise ValidationError(_('User is already a member of the club'))
|
||||
|
||||
if self.club.parent_club is not None:
|
||||
# Check that the user is already a member of the parent club if the membership is created
|
||||
if not Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club.parent_club,
|
||||
date_start__gte=self.club.parent_club.membership_start,
|
||||
).exists():
|
||||
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||
self.renew_parent()
|
||||
else:
|
||||
raise ValidationError(_('User is not a member of the parent club')
|
||||
+ ' ' + self.club.parent_club.name)
|
||||
|
||||
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
||||
|
||||
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
||||
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
||||
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
||||
self.date_end = self.club.membership_end
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
self.make_transaction()
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
"""
|
||||
@ -468,14 +402,66 @@ class Membership(models.Model):
|
||||
|
||||
if self.club.parent_club.name == "BDE":
|
||||
parent_membership.roles.set(
|
||||
Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all())
|
||||
Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all())
|
||||
elif self.club.parent_club.name == "Kfet":
|
||||
parent_membership.roles.set(
|
||||
Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
||||
Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
|
||||
else:
|
||||
parent_membership.roles.set(Role.objects.filter(name="Membre de club").all())
|
||||
parent_membership.save()
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
Calculate fee and end date before saving the membership and creating the transaction if needed.
|
||||
"""
|
||||
# Ensure that club membership dates are valid
|
||||
old_membership_start = self.club.membership_start
|
||||
self.club.update_membership_dates()
|
||||
if self.club.membership_start != old_membership_start:
|
||||
self.club.save()
|
||||
|
||||
created = not self.pk
|
||||
if not created:
|
||||
for role in self.roles.all():
|
||||
club = role.for_club
|
||||
if club is not None:
|
||||
if club.pk != self.club_id:
|
||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||
.format(role=role.name, club=club.name))
|
||||
else:
|
||||
if Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club,
|
||||
date_start__lte=self.date_start,
|
||||
date_end__gte=self.date_start,
|
||||
).exists():
|
||||
raise ValidationError(_('User is already a member of the club'))
|
||||
|
||||
if self.club.parent_club is not None:
|
||||
# Check that the user is already a member of the parent club if the membership is created
|
||||
if not Membership.objects.filter(
|
||||
user=self.user,
|
||||
club=self.club.parent_club,
|
||||
date_start__gte=self.club.parent_club.membership_start,
|
||||
).exists():
|
||||
if hasattr(self, '_force_renew_parent') and self._force_renew_parent:
|
||||
self.renew_parent()
|
||||
else:
|
||||
raise ValidationError(_('User is not a member of the parent club')
|
||||
+ ' ' + self.club.parent_club.name)
|
||||
|
||||
self.fee = self.club.membership_fee_paid if self.user.profile.paid else self.club.membership_fee_unpaid
|
||||
|
||||
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) \
|
||||
if self.club.membership_duration is not None else self.date_start + datetime.timedelta(days=424242)
|
||||
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
|
||||
self.date_end = self.club.membership_end
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
self.make_transaction()
|
||||
|
||||
def make_transaction(self):
|
||||
"""
|
||||
Create Membership transaction associated to this membership.
|
||||
@ -513,3 +499,11 @@ class Membership(models.Model):
|
||||
soge_credit.save()
|
||||
else:
|
||||
transaction.save(force_insert=True)
|
||||
|
||||
def __str__(self):
|
||||
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('membership')
|
||||
verbose_name_plural = _('memberships')
|
||||
indexes = [models.Index(fields=['user'])]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2024 by BDE ENS Paris-Saclay
|
||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/**
|
||||
* On form submit, create a new friendship
|
||||
*/
|
||||
function form_create_trust (e) {
|
||||
function create_trust (e) {
|
||||
// Do not submit HTML form
|
||||
e.preventDefault()
|
||||
|
||||
@ -14,35 +14,25 @@ function form_create_trust (e) {
|
||||
addMsg(gettext("You can't add yourself as a friend"), "danger")
|
||||
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) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a trust between users
|
||||
* @param trusting:Integer trusting note id
|
||||
* @param trusted:Integer trusted note id
|
||||
*/
|
||||
function create_trust(trusting, trusted) {
|
||||
$.post('/api/note/trust/', {
|
||||
trusting: trusting,
|
||||
trusted: trusted,
|
||||
csrfmiddlewaretoken: CSRF_TOKEN
|
||||
}).done(function () {
|
||||
// Reload tables
|
||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||
$('#trusted_table').load(location.pathname + ' #trusted_table')
|
||||
addMsg(gettext('Friendship successfully added'), 'success')
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* On click of "delete", delete the trust
|
||||
* @param button_id:Integer Trust id to remove
|
||||
* On click of "delete", delete the alias
|
||||
* @param button_id:Integer Alias id to remove
|
||||
*/
|
||||
function delete_button (button_id) {
|
||||
$.ajax({
|
||||
@ -52,7 +42,6 @@ function delete_button (button_id) {
|
||||
}).done(function () {
|
||||
addMsg(gettext('Friendship successfully deleted'), 'success')
|
||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
||||
$('#trusted_table').load(location.pathname + ' #trusted_table')
|
||||
}).fail(function (xhr, _textStatus, _error) {
|
||||
errMsg(xhr.responseJSON)
|
||||
})
|
||||
@ -60,5 +49,5 @@ function delete_button (button_id) {
|
||||
|
||||
$(document).ready(function () {
|
||||
// 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
|
||||
|
||||
from datetime import date
|
||||
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% block profile_content %}
|
||||
<div class="card bg-light mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "Add friends" %}
|
||||
{% trans "Note friendships" %}
|
||||
</h3>
|
||||
<div class="card-body">
|
||||
{% if can_create %}
|
||||
@ -24,7 +24,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% render_table trusting %}
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning card mb-3">
|
||||
<div class="alert alert-warning card">
|
||||
{% blocktrans trimmed %}
|
||||
Adding someone as a friend enables them to initiate transactions coming
|
||||
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.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<h3 class="card-header text-center">
|
||||
{% trans "People having you as a friend" %}
|
||||
</h3>
|
||||
{% render_table trusted_by %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
|
@ -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
|
||||
|
||||
from datetime import date
|
||||
|
@ -183,7 +183,7 @@ class TestMemberships(TestCase):
|
||||
club = Club.objects.get(name="Kfet")
|
||||
else:
|
||||
club = Club.objects.create(
|
||||
name="Second club without BDE",
|
||||
name="Second club " + ("with BDE" if bde_parent else "without BDE"),
|
||||
parent_club=None,
|
||||
email="newclub@example.com",
|
||||
require_memberships=True,
|
||||
@ -291,7 +291,7 @@ class TestMemberships(TestCase):
|
||||
|
||||
response = self.client.post(reverse("member:club_manage_roles", args=(self.membership.pk,)), data=dict(
|
||||
roles=[role.id for role in Role.objects.filter(
|
||||
Q(name="Membre de club") | Q(name="Trésorier·ère de club") | Q(name="Bureau de club")).all()],
|
||||
Q(name="Membre de club") | Q(name="Trésorièr⋅e de club") | Q(name="Bureau de club")).all()],
|
||||
))
|
||||
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||
self.membership.refresh_from_db()
|
||||
@ -335,7 +335,6 @@ class TestMemberships(TestCase):
|
||||
ml_sports_registration=True,
|
||||
ml_art_registration=True,
|
||||
report_frequency=7,
|
||||
VSS_charter_read=True
|
||||
))
|
||||
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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.models import User
|
||||
from django.contrib.auth.views import LoginView
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import transaction
|
||||
from django.db.models import Q, F
|
||||
from django.shortcuts import redirect
|
||||
@ -20,7 +21,7 @@ from django_tables2.views import SingleTableView
|
||||
from rest_framework.authtoken.models import Token
|
||||
from note.models import Alias, NoteClub, NoteUser, Trust
|
||||
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 permission.backends import PermissionBackend
|
||||
from permission.models import Role
|
||||
@ -257,18 +258,17 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
note = context['object'].note
|
||||
context["trusting"] = TrustTable(
|
||||
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||
context["trusted_by"] = TrustedTable(
|
||||
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
|
||||
trusting=context["object"].note,
|
||||
trusted=context["object"].note
|
||||
))
|
||||
context["widget"] = {
|
||||
"name": "trusted",
|
||||
"resetable": True,
|
||||
"attrs": {
|
||||
"model_pk": ContentType.objects.get_for_model(Alias).pk,
|
||||
"class": "autocomplete form-control",
|
||||
"id": "trusted",
|
||||
"resetable": True,
|
||||
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
||||
"name_field": "name",
|
||||
"placeholder": ""
|
||||
@ -753,10 +753,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
club = old_membership.club
|
||||
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
|
||||
|
||||
# Get form data
|
||||
@ -824,8 +820,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
|
||||
ret = super().form_valid(form)
|
||||
|
||||
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
|
||||
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
|
||||
member_role = Role.objects.filter(Q(name="Adhérent⋅e BDE") | Q(name="Membre de club")).all() \
|
||||
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all() \
|
||||
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
|
||||
# Set the same roles as before
|
||||
if old_membership:
|
||||
@ -861,7 +857,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
||||
membership.refresh_from_db()
|
||||
if old_membership.exists():
|
||||
membership.roles.set(old_membership.get().roles.all())
|
||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all())
|
||||
membership.roles.set(Role.objects.filter(Q(name="Adhérent⋅e Kfet") | Q(name="Membre de club")).all())
|
||||
membership.save()
|
||||
|
||||
return ret
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
from django.contrib import admin
|
||||
@ -7,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
|
||||
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
||||
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, \
|
||||
RecurrentTransaction, MembershipTransaction, SpecialTransaction
|
||||
from .templatetags.pretty_money import pretty_money
|
||||
@ -21,16 +21,6 @@ class AliasInlines(admin.TabularInline):
|
||||
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)
|
||||
class NoteAdmin(PolymorphicParentModelAdmin):
|
||||
"""
|
||||
@ -102,7 +92,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Child for an user note, see NoteAdmin
|
||||
"""
|
||||
inlines = (AliasInlines, TrustInlines)
|
||||
inlines = (AliasInlines,)
|
||||
|
||||
# We can't change user after creation or the 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
|
||||
|
||||
from django.conf import settings
|
||||
@ -11,7 +11,6 @@ from member.models import Membership
|
||||
from note_kfet.middlewares import get_current_request
|
||||
from permission.backends import PermissionBackend
|
||||
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.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
||||
@ -87,9 +86,11 @@ class TrustSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Trust
|
||||
fields = '__all__'
|
||||
validators = [UniqueTogetherValidator(
|
||||
queryset=Trust.objects.all(), fields=('trusting', 'trusted'),
|
||||
message=_("This friendship already exists"))]
|
||||
|
||||
def validate(self, attrs):
|
||||
instance = Trust(**attrs)
|
||||
instance.clean()
|
||||
return attrs
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
from datetime import datetime
|
||||
|
||||
@ -26,7 +26,7 @@ class TransactionTemplateForm(forms.ModelForm):
|
||||
# We don't evaluate the content type at launch because the DB might be not initialized
|
||||
'api_url_suffix':
|
||||
lambda: '&polymorphic_ctype=' + str(ContentType.objects.get_for_model(NoteClub).pk),
|
||||
'placeholder': 'Note ...',
|
||||
'placeholder': 'Note…',
|
||||
},
|
||||
),
|
||||
'amount': AmountInput(),
|
||||
@ -43,7 +43,7 @@ class SearchTransactionForm(forms.Form):
|
||||
resetable=True,
|
||||
attrs={
|
||||
'api_url': '/api/note/alias/',
|
||||
'placeholder': 'Note ...',
|
||||
'placeholder': 'Note…',
|
||||
},
|
||||
),
|
||||
)
|
||||
@ -57,7 +57,7 @@ class SearchTransactionForm(forms.Form):
|
||||
resetable=True,
|
||||
attrs={
|
||||
'api_url': '/api/note/alias/',
|
||||
'placeholder': 'Note ...',
|
||||
'placeholder': 'Note…',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
@ -19,7 +19,7 @@ class Migration(migrations.Migration):
|
||||
('trusting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusting', to='note.Note', verbose_name='trusting')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'frienship',
|
||||
'verbose_name': 'friendship',
|
||||
'verbose_name_plural': 'friendships',
|
||||
'unique_together': {('trusting', 'trusted')},
|
||||
},
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
import unicodedata
|
||||
@ -240,7 +240,7 @@ class Trust(models.Model):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("frienship")
|
||||
verbose_name = _("friendship")
|
||||
verbose_name_plural = _("friendships")
|
||||
unique_together = ("trusting", "trusted")
|
||||
|
||||
@ -293,11 +293,6 @@ class Alias(models.Model):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def normalize(string):
|
||||
"""
|
||||
@ -326,6 +321,11 @@ class Alias(models.Model):
|
||||
pass
|
||||
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):
|
||||
if self.name == str(self.note):
|
||||
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
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
@ -20,7 +20,7 @@ class TemplateCategory(models.Model):
|
||||
"""
|
||||
Defined a recurrent transaction category
|
||||
|
||||
Example: food, softs, ...
|
||||
Example: food, softs,…
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_("name"),
|
||||
@ -40,7 +40,7 @@ class TransactionTemplate(models.Model):
|
||||
"""
|
||||
Defined a recurrent transaction
|
||||
|
||||
associated to selling something (a burger, a beer, ...)
|
||||
associated to selling something (a burger, a beer,…)
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
@ -59,7 +59,6 @@ class TransactionTemplate(models.Model):
|
||||
amount = models.PositiveIntegerField(
|
||||
verbose_name=_('amount'),
|
||||
)
|
||||
|
||||
category = models.ForeignKey(
|
||||
TemplateCategory,
|
||||
on_delete=models.PROTECT,
|
||||
@ -88,12 +87,12 @@ class TransactionTemplate(models.Model):
|
||||
verbose_name = _("transaction template")
|
||||
verbose_name_plural = _("transaction templates")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('note:template_update', args=(self.pk,))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Transaction(PolymorphicModel):
|
||||
"""
|
||||
@ -102,6 +101,7 @@ class Transaction(PolymorphicModel):
|
||||
amount is store in centimes of currency, making it a positive integer
|
||||
value. (from someone to someone else)
|
||||
"""
|
||||
|
||||
source = models.ForeignKey(
|
||||
Note,
|
||||
on_delete=models.PROTECT,
|
||||
@ -166,50 +166,6 @@ class Transaction(PolymorphicModel):
|
||||
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):
|
||||
previous_source_balance = self.source.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
|
||||
|
||||
@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
|
||||
def total(self):
|
||||
return self.amount * self.quantity
|
||||
@ -260,40 +256,46 @@ class Transaction(PolymorphicModel):
|
||||
def type(self):
|
||||
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):
|
||||
"""
|
||||
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
|
||||
"""
|
||||
|
||||
template = models.ForeignKey(
|
||||
TransactionTemplate,
|
||||
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):
|
||||
if self.template.destination != self.destination and not (hasattr(self, '_force_save') and self._force_save):
|
||||
raise ValidationError(
|
||||
_("The destination of this transaction must equal to the destination of the template."))
|
||||
return super().clean()
|
||||
|
||||
@transaction.atomic
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return _('Template')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("recurrent transaction")
|
||||
verbose_name_plural = _("recurrent transactions")
|
||||
|
||||
|
||||
class SpecialTransaction(Transaction):
|
||||
"""
|
||||
Special type of :model:`note.Transaction` associated to transactions with special notes
|
||||
"""
|
||||
|
||||
last_name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
@ -310,15 +312,6 @@ class SpecialTransaction(Transaction):
|
||||
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
|
||||
def type(self):
|
||||
return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit")
|
||||
@ -332,8 +325,13 @@ class SpecialTransaction(Transaction):
|
||||
def clean(self):
|
||||
# SpecialTransaction are only possible with NoteSpecial object
|
||||
if self.is_credit() == self.is_debit():
|
||||
raise ValidationError(_("A special transaction is only possible between a"
|
||||
" Note associated to a payment method and a User or a Club"))
|
||||
raise(ValidationError(_("A special transaction is only possible between a"
|
||||
" 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
|
||||
def validate_payment_form(form):
|
||||
@ -365,11 +363,17 @@ class SpecialTransaction(Transaction):
|
||||
|
||||
return not error
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Special transaction")
|
||||
verbose_name_plural = _("Special transactions")
|
||||
|
||||
|
||||
class MembershipTransaction(Transaction):
|
||||
"""
|
||||
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
|
||||
|
||||
"""
|
||||
|
||||
membership = models.OneToOneField(
|
||||
'member.Membership',
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
// 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 () {
|
||||
if (!isNaN(source.balance)) {
|
||||
const newBalance = source.balance - quantity * amount
|
||||
if (newBalance <= -2000) {
|
||||
if (newBalance <= -5000) {
|
||||
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)
|
||||
} 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)) {
|
||||
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.'),
|
||||
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
|
||||
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
|
||||
|
||||
import html
|
||||
@ -159,11 +159,11 @@ class TrustTable(tables.Table):
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
show_header = False
|
||||
trusted = tables.Column(attrs={'td': {'class': 'text-center'}})
|
||||
trusted = tables.Column(attrs={'td': {'class': 'text_center'}})
|
||||
|
||||
delete_col = tables.TemplateColumn(
|
||||
template_code=DELETE_TEMPLATE,
|
||||
extra_context={"delete_trans": _('Delete')},
|
||||
extra_context={"delete_trans": _('delete')},
|
||||
attrs={
|
||||
'td': {
|
||||
'class': lambda record: 'col-sm-1'
|
||||
@ -173,46 +173,6 @@ class TrustTable(tables.Table):
|
||||
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 Meta:
|
||||
attrs = {
|
||||
|
@ -40,7 +40,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{# User search with autocompletion #}
|
||||
<div class="card-footer">
|
||||
<input class="form-control mx-auto d-block"
|
||||
placeholder="{% trans "Name or alias..." %}" type="text" id="note" autofocus />
|
||||
placeholder="{% trans "Name or alias…" %}" type="text" id="note" autofocus />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -103,11 +103,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link font-weight-bold" data-toggle="tab" href="#search">
|
||||
{% trans "Search" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -128,20 +123,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
||||
|
||||
@ -182,7 +163,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<script type="text/javascript">
|
||||
{% for button in highlighted %}
|
||||
{% if button.display %}
|
||||
document.getElementById("highlighted_button{{ button.id }}").addEventListener("click", function() {
|
||||
$("#highlighted_button{{ button.id }}").click(function() {
|
||||
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||
@ -193,7 +174,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% for category in categories %}
|
||||
{% for button in category.templates_filtered %}
|
||||
{% if button.display %}
|
||||
document.getElementById("button{{ button.id }}").addEventListener("click", function() {
|
||||
$("#button{{ button.id }}").click(function() {
|
||||
addConso({{ button.destination_id }}, {{ button.amount }},
|
||||
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
|
||||
{{ button.id }}, "{{ button.name|escapejs }}");
|
||||
@ -201,15 +182,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
{% endif %}
|
||||
{% 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>
|
||||
{% endblock %}
|
||||
|
@ -22,7 +22,7 @@
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérents dont le solde
|
||||
Par ailleurs, le BDE ne sert pas d'alcool aux adhérent⋅es dont le solde
|
||||
est inférieur à 0 € depuis plus de 24h.
|
||||
</p>
|
||||
|
||||
|
@ -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 }}.
|
||||
|
||||
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érent⋅es dont le solde
|
||||
est inférieur à 0 € depuis plus de 24h.
|
||||
|
||||
Si tu ne comprends pas ton solde, tu peux consulter ton historique
|
||||
|
@ -66,7 +66,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input class="form-control mx-auto" type="text" id="source_note" placeholder="{% trans "Name or alias..." %}" />
|
||||
<input class="form-control mx-auto" type="text" id="source_note" placeholder="{% trans "Name or alias…" %}" />
|
||||
<div id="source_me_div">
|
||||
<hr>
|
||||
<a class="btn-block btn btn-secondary" href="#" id="source_me" data-turbolinks="false">
|
||||
@ -93,14 +93,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input class="form-control mx-auto" type="text" id="dest_note" placeholder="{% trans "Name or alias..." %}" />
|
||||
<input class="form-control mx-auto" type="text" id="dest_note" placeholder="{% trans "Name or alias…" %}" />
|
||||
<ul class="list-group list-group-flush" id="dest_alias_matched">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Information on transaction (amount, reason, name,...) #}
|
||||
{# Information on transaction (amount, reason, name,…) #}
|
||||
<div class="col-md" id="external_div">
|
||||
<div class="card bg-light mb-4">
|
||||
<div class="card-header">
|
||||
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="row justify-content-center mb-4">
|
||||
<div class="col-md-10 text-center">
|
||||
{# Search field , see js #}
|
||||
<input class="form-control mx-auto w-25" type="text" id="search_field" placeholder="{% trans "Name of the button..." %}" value="{{ request.GET.search }}">
|
||||
<input class="form-control mx-auto w-25" type="text" id="search_field" placeholder="{% trans "Name of the button…" %}" value="{{ request.GET.search }}">
|
||||
<hr>
|
||||
<a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}" data-turbolinks="false">{% trans "New button" %}</a>
|
||||
</div>
|
||||
@ -19,7 +19,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
<div class="col-md-12">
|
||||
<div class="card card-border shadow">
|
||||
<div class="card-header text-center">
|
||||
<h5> {% trans "buttons listing "%}</h5>
|
||||
<h5>{% trans "buttons listing"%}</h5>
|
||||
</div>
|
||||
<div class="card-body px-0 py-0" id="buttons_table">
|
||||
{% render_table table %}
|
||||
@ -70,11 +70,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
|
||||
})
|
||||
.done(function() {
|
||||
addMsg('{% trans "button successfully deleted "%}','success');
|
||||
addMsg('{% trans "button successfully deleted"%}','success');
|
||||
$("#buttons_table").load(location.pathname + "?search=" + $("#search_field").val().replace(" ", "%20") + " #buttons_table");
|
||||
})
|
||||
.fail(function() {
|
||||
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
|
||||
addMsg('{% trans "Unable to delete button"%} #' + button_id, 'danger')
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
import json
|
||||
@ -10,12 +10,12 @@ from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q, F
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, UpdateView, DetailView
|
||||
from django.urls import reverse_lazy
|
||||
from django_tables2 import SingleTableView
|
||||
from django.urls import reverse_lazy
|
||||
from activity.models import Entry
|
||||
from note_kfet.inputs import AmountInput
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
from note_kfet.inputs import AmountInput
|
||||
|
||||
from .forms import TransactionTemplateForm, SearchTransactionForm
|
||||
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note
|
||||
@ -190,10 +190,6 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
).order_by('name').all()
|
||||
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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
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é
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
from datetime import date
|
||||
@ -198,41 +198,6 @@ class PermissionBackend(ModelBackend):
|
||||
def has_module_perms(self, user_obj, app_label):
|
||||
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):
|
||||
ct = ContentType.objects.get_for_model(obj)
|
||||
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
|
||||
import sys
|
||||
from functools import lru_cache
|
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user