1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 18:08:21 +02:00

Compare commits

..

45 Commits

Author SHA1 Message Date
9dc11aa016 Merge branch 'main' into potvieux 2025-05-22 12:01:40 +02:00
ff4353d344 Merge branch 'update_invoice_template' into 'main'
Update invoice template

See merge request bde/nk20!307
2025-04-15 18:11:18 +02:00
a90f45bd8b Replace Diolistos.png 2025-04-15 17:38:45 +02:00
10c22ccc53 Replace Diolistos_bg.jpg 2025-04-15 17:38:26 +02:00
6969cee0f3 Merge branch 'time-display' into 'main'
Fixed some non timezone-aware displays

See merge request bde/nk20!303
2025-04-15 17:28:28 +02:00
ddeada200b Changement logo factures 2025-04-15 17:26:14 +02:00
8e2b24b2da Merge branch 'options_order' into 'main'
Options order

See merge request bde/nk20!306
2025-04-13 23:12:05 +02:00
bd76c280ec Update forms.py 2025-04-13 22:59:04 +02:00
ca0a95ba9e Update transaction_form.html 2025-04-13 22:32:49 +02:00
614f76e699 Add BDA email as an option to fix NL updates
Commit [1] removed default BDA email address from extract_ml_registrations script code. We should now add it to cron.

[1] 3dd5f6e3e0

See merge request bde/nk20!305
2025-04-13 20:17:44 +02:00
a5815f0bc7 Add BDA email as an option to fix NL updates
Commit [1] removed default BDA email address from extract_ml_registrations script code. We should now add it to cron.

[1] 3dd5f6e3e0
2025-04-13 19:56:20 +02:00
84e9fea15f linters 2025-04-04 14:46:43 +02:00
b7a660ee40 bootstrap: fix minor issues with profile picture cropping
* Add required [1] "display: block;" style property to img element
* Fix image overflow in modal. As cropper size inherits from img's parent element [2] (including padding according to my research), we need to wrap modal body into another div that has the padding we want.
* Remove ability [3] to click away to dismiss the modal as it often interfered with user interaction when cropping.

[1] https://github.com/fengyuanchen/cropperjs/tree/v1?tab=readme-ov-file#example
[2] https://github.com/fengyuanchen/cropperjs/tree/v1?tab=readme-ov-file#notes
[3] https://getbootstrap.com/docs/4.0/components/modal/#options

See merge request bde/nk20!301
2025-04-04 01:14:46 +02:00
b9ebb1718a Fixed some non timezone-aware displays 2025-04-04 00:29:22 +02:00
7ba5c76a89 Merge branch 'guests_schools' into 'main'
add school field to guest

See merge request bde/nk20!302
2025-03-25 18:59:46 +01:00
702ddb5679 add school field to guest 2025-03-25 17:39:31 +01:00
93aed87265 bootstrap: fix minor issues with profile picture cropping
* Add required [1] "display: block;" style property to img element
* Fix image overflow in modal. As cropper size inherits from img's parent element [2] (including padding according to my research), we need to wrap modal body into another div that has the padding we want.
* Remove ability [3] to click away to dismiss the modal as it often interfered with user interaction when cropping.

[1] https://github.com/fengyuanchen/cropperjs/tree/v1?tab=readme-ov-file#example
[2] https://github.com/fengyuanchen/cropperjs/tree/v1?tab=readme-ov-file#notes
[3] https://getbootstrap.com/docs/4.0/components/modal/#options

Signed-off-by: Alexis Mercier des Rochettes <apernouille@gmail.com>
2025-03-24 17:36:30 +01:00
60355196ce Merge branch 'openid-connect' into 'main'
Openid connect

See merge request bde/nk20!293
2025-03-20 18:42:51 +01:00
9bffb32a5e documentation 2025-03-20 17:36:38 +01:00
5ef019c5c2 Merge branch 'notekfet_wrapped' into 'main'
Rewrite script and add test

See merge request bde/nk20!300
2025-03-18 16:11:33 +01:00
8da62e62fb Rewrite script and add test 2025-03-18 15:53:02 +01:00
56a43396d4 Merge branch 'Add_some_permissions' into 'main'
Add some permissions

See merge request bde/nk20!296
2025-03-17 13:16:01 +01:00
7966d6f397 Update file initial.json 2025-03-17 13:15:07 +01:00
562dcfb908 Update file initial.json 2025-03-11 19:34:56 +01:00
12ef258ff0 Update file initial.json 2025-03-11 19:27:02 +01:00
2ae32ee3b6 Update file initial.json 2025-03-11 19:26:49 +01:00
ec1bd45481 Update file initial.json 2025-03-11 19:14:09 +01:00
6c63c6417c Typesetting 2025-03-08 16:08:40 +01:00
4563b2b640 Added configusation for OpenID support, along with installation information 2025-03-08 16:04:25 +01:00
2ec5a0d9ca Les couleurs des surviva[list]s 2025-01-21 20:11:29 +01:00
3faf611816 Merge main into potvieux 2025-01-20 17:43:25 +01:00
2807b6ef44 Note en mode vieux pour la Kokarde de remise des diplomes 2024-05-30 22:06:51 +02:00
d6645900d3 PC Kfet can see kfet memberships to accurately display non-Kfet members in blue 2024-01-11 21:01:55 +01:00
e35847ebd8 Show old people with PC Kfet 2024-01-11 20:37:35 +01:00
57268bc9c2 Styling and catching late registrations as old 2024-01-11 19:48:41 +01:00
ab6c943126 linters 2024-01-11 14:58:41 +01:00
5dc5f56ae4 guests cannot be old 2024-01-11 14:38:43 +01:00
81017fc393 Couleurs Splpp 2024-01-11 11:22:58 +01:00
90e3871934 Merge branch 'main' into potvieux 2024-01-11 02:19:51 +01:00
95e07f3148 New scripts 2024-01-10 18:09:53 +01:00
770c748bd9 couleurs finalist pour le pot vieux 2023-01-29 21:18:13 +01:00
06fa096405 Fina[list] colors 2023-01-27 20:42:00 +01:00
182f680507 Merge branch 'main' into potvieux 2023-01-27 19:00:02 +01:00
5ceda66ded use the right colors. 2022-04-01 08:54:57 +02:00
6443d64b69 View and Highlight Vieux people. 2022-04-01 08:54:05 +02:00
35 changed files with 435 additions and 595 deletions

View File

@ -58,7 +58,13 @@ Bien que cela permette de créer une instance sur toutes les distributions,
(env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial (env)$ ./manage.py createsuperuser # Création d'un⋅e utilisateur⋅rice initial
``` ```
6. Enjoy : 6. (Optionnel) **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
7. Enjoy :
```bash ```bash
(env)$ ./manage.py runserver 0.0.0.0:8000 (env)$ ./manage.py runserver 0.0.0.0:8000
@ -228,7 +234,13 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
(env)$ ./manage.py check # pas de bêtise qui traine (env)$ ./manage.py check # pas de bêtise qui traine
(env)$ ./manage.py migrate (env)$ ./manage.py migrate
7. *Enjoy \o/* 7. **Création d'une clé privée OpenID Connect**
Pour activer le support d'OpenID Connect, il faut générer une clé privée, par
exemple avec openssl (`openssl genrsa -out oidc.key 4096`), et renseigner son
emplacement dans `OIDC_RSA_PRIVATE_KEY` (par défaut `/var/secrets/oidc.key`).
8. *Enjoy \o/*
### Installation avec Docker ### Installation avec Docker

View File

@ -35,7 +35,7 @@ class GuestAdmin(admin.ModelAdmin):
""" """
Admin customisation for Guest Admin customisation for Guest
""" """
list_display = ('last_name', 'first_name', 'activity', 'inviter') list_display = ('last_name', 'first_name', 'school', 'activity', 'inviter')
form = GuestForm form = GuestForm

View File

@ -51,9 +51,9 @@ class GuestViewSet(ReadProtectedModelViewSet):
queryset = Guest.objects.order_by('id') queryset = Guest.objects.order_by('id')
serializer_class = GuestSerializer serializer_class = GuestSerializer
filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter] filter_backends = [DjangoFilterBackend, RegexSafeSearchFilter]
filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'inviter', 'inviter__alias__name', filterset_fields = ['activity', 'activity__name', 'last_name', 'first_name', 'school', 'inviter', 'inviter__alias__name',
'inviter__alias__normalized_name', ] 'inviter__alias__normalized_name', ]
search_fields = ['$activity__name', '$last_name', '$first_name', '$inviter__user__email', '$inviter__alias__name', search_fields = ['$activity__name', '$last_name', '$first_name', '$school', '$inviter__user__email', '$inviter__alias__name',
'$inviter__alias__normalized_name', ] '$inviter__alias__normalized_name', ]

View File

@ -107,7 +107,7 @@ class GuestForm(forms.ModelForm):
class Meta: class Meta:
model = Guest model = Guest
fields = ('last_name', 'first_name', 'inviter', ) fields = ('last_name', 'first_name', 'school', 'inviter', )
widgets = { widgets = {
"inviter": Autocomplete( "inviter": Autocomplete(
NoteUser, NoteUser,

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.20 on 2025-03-25 09:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("activity", "0005_alter_opener_options_alter_opener_opener"),
]
operations = [
migrations.AddField(
model_name="guest",
name="school",
field=models.CharField(default="", max_length=255, verbose_name="school"),
preserve_default=False,
),
]

View File

@ -201,7 +201,8 @@ class Entry(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest)
if qs.exists(): if qs.exists():
raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, )) raise ValidationError(_("Already entered on ")
+ _("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(qs.get().time), ))
if self.guest: if self.guest:
self.note = self.guest.inviter self.note = self.guest.inviter
@ -247,6 +248,11 @@ class Guest(models.Model):
verbose_name=_("first name"), verbose_name=_("first name"),
) )
school = models.CharField(
max_length=255,
verbose_name=_("school"),
)
inviter = models.ForeignKey( inviter = models.ForeignKey(
NoteUser, NoteUser,
on_delete=models.PROTECT, on_delete=models.PROTECT,

View File

@ -1,6 +1,8 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay # Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import datetime
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
@ -51,11 +53,11 @@ class GuestTable(tables.Table):
} }
model = Guest model = Guest
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ("last_name", "first_name", "inviter", ) fields = ("last_name", "first_name", "inviter", "school")
def render_entry(self, record): def render_entry(self, record):
if record.has_entry: if record.has_entry:
return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, ))) return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(timezone.localtime(record.entry.time))))
return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> ' return mark_safe('<button id="{id}" class="btn btn-danger btn-sm" onclick="remove_guest(this.id)"> '
'{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize())) '{delete_trans}</button>'.format(id=record.id, delete_trans=_("remove").capitalize()))
@ -77,6 +79,9 @@ def get_row_class(record):
c += " table-info" c += " table-info"
elif record.note.balance < 0: elif record.note.balance < 0:
c += " table-danger" c += " table-danger"
# MODE VIEUXCON=ON
if (datetime.datetime.utcnow().timestamp() - record.note.created_at.timestamp()) > 3600 * 24 * 365 * 2.5:
c += " font-weight-bold underline"
return c return c

View File

@ -50,6 +50,7 @@ class TestActivities(TestCase):
inviter=self.user.note, inviter=self.user.note,
last_name="GUEST", last_name="GUEST",
first_name="Guest", first_name="Guest",
school="School",
) )
def test_activity_list(self): def test_activity_list(self):
@ -156,6 +157,7 @@ class TestActivities(TestCase):
inviter=self.user.note.id, inviter=self.user.note.id,
last_name="GUEST2", last_name="GUEST2",
first_name="Guest", first_name="Guest",
school="School",
)) ))
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@ -167,6 +169,7 @@ class TestActivities(TestCase):
inviter=self.user.note.id, inviter=self.user.note.id,
last_name="GUEST2", last_name="GUEST2",
first_name="Guest", first_name="Guest",
school="School",
)) ))
self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200) self.assertRedirects(response, reverse("activity:activity_detail", args=(self.activity.pk,)), 302, 200)
@ -200,6 +203,7 @@ class TestActivityAPI(TestAPI):
inviter=self.user.note, inviter=self.user.note,
last_name="GUEST", last_name="GUEST",
first_name="Guest", first_name="Guest",
school="School",
) )
self.entry = Entry.objects.create( self.entry = Entry.objects.create(

View File

@ -168,6 +168,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
activity=activity, activity=activity,
first_name="", first_name="",
last_name="", last_name="",
school="",
inviter=self.request.user.note, inviter=self.request.user.note,
) )
@ -263,13 +264,22 @@ class ActivityEntryView(LoginRequiredMixin, SingleTableMixin, TemplateView):
balance=F("note__balance")) balance=F("note__balance"))
# Keep only users that have a note # Keep only users that have a note
note_qs = note_qs.filter(note__noteuser__isnull=False) note_qs = note_qs.filter(note__noteuser__isnull=False).exclude(note__inactivity_reason='forced')
if activity.activity_type.name != "Pot Vieux":
# Keep only members
note_qs = note_qs.filter(
note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(),
note__noteuser__user__memberships__date_end__gte=timezone.now(),
)
# Keep only valid members # Keep only valid members
note_qs = note_qs.filter( # note_qs = note_qs.filter(
note__noteuser__user__memberships__club=activity.attendees_club, # note__noteuser__user__memberships__club=activity.attendees_club,
note__noteuser__user__memberships__date_start__lte=timezone.now(), # note__noteuser__user__memberships__date_start__lte=timezone.now(),
note__noteuser__user__memberships__date_end__gte=timezone.now()).exclude(note__inactivity_reason='forced') # note__noteuser__user__memberships__date_end__gte=timezone.now(),
# )
# Filter with permission backend # Filter with permission backend
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")) note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))

View File

@ -23,7 +23,7 @@ from .models import Profile, Club, Membership
class CustomAuthenticationForm(AuthenticationForm): class CustomAuthenticationForm(AuthenticationForm):
permission_mask = forms.ModelChoiceField( permission_mask = forms.ModelChoiceField(
label=_("Permission mask"), label=_("Permission mask"),
queryset=PermissionMask.objects.order_by("rank"), queryset=PermissionMask.objects.order_by("-rank"),
empty_label=None, empty_label=None,
) )

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-07 12:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0012_club_add_registration_form'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2024, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -20,12 +20,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
</form> </form>
</div> </div>
<!-- MODAL TO CROP THE IMAGE --> <!-- MODAL TO CROP THE IMAGE -->
<div class="modal fade" id="modalCrop"> <div class="modal fade" id="modalCrop" data-backdrop="static">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body-wrapper" style="width: 500px; height: 500px; padding: 16px;">
<img src="" id="modal-image" style="max-width: 100%;"> <div class="modal-body" style="width: 100%; height: 100%; padding: 0">
</div> <img src="" id="modal-image" style="display: block; max-width: 100%;">
</div>
</div>
<div class="modal-footer"> <div class="modal-footer">
<div class="btn-group pull-left" role="group"> <div class="btn-group pull-left" role="group">
<button type="button" class="btn btn-default" id="js-zoom-in"> <button type="button" class="btn btn-default" id="js-zoom-in">

View File

@ -1,65 +0,0 @@
{% load pretty_money %}
{% load i18n %}
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>[Note Kfet] Récapitulatif de trésorerie</title>
</head>
<body>
<h1>
Récapitulatif de trésorerie au {{ summary.date|date:"d/m/Y" }} à {{ summary.date|date:"H:i:s" }} :
</h1>
<h2>
Tous les utilisateur⋅rices :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_user }} soit {{ summary.balance_positive_user / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_user }}</li>
<li>Négatifs : {{ summary.total_negative_user }} soit {{ summary.balance_negative_user / 100 }} €</li>
</ul>
<h2>
Les {{ summary.total_positive_user_bde + summary.total_zero_user_bde + summary.total_negative_user_bde }} adhérent⋅es BDE :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_user_bde }} soit {{ summary.balance_positive_user_bde / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_user_bde }}</li>
<li>Négatifs : {{ summary.total_negative_user_bde }} soit {{ summary.balance_negative_user_bde / 100 }} €</li>
</ul>
<h2>
Clubs :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_club }} soit {{ summary.balance_positive_club / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_club }}</li>
<li>Négatifs : {{ summary.total_negative_club }} soit {{ summary.balance_negative_club / 100 }} €</li>
</ul>
<h2>
Clubs hors BDE / Kfet et club dont le nom fini par "- BDE" :
</h2>
<ul>
<li>Positifs : {{ summary.total_positive_club_nbde }} soit {{ summary.balance_positive_club_nbde / 100 }} €</li>
<li>Neutres : {{ summary.total_zero_club_nbde }}</li>
<li>Négatifs : {{ summary.total_negative_club_nbde }} soit {{ summary.balance_negative_club_nbde / 100 }} €</li>
</ul>
<h2>
Progression :
</h2>
<ul>
<li>Ceci correspond à une différence de {{ balance_difference_user / 100 }} € pour les utilisateur⋅rices</li>
<li>Ceci correspond à une différence de {{ balance_difference_club / 100 }} € pour les clubs</li>
</ul>
--
<p>
Le BDE<br>
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}
</p>
</body>
</html>

View File

@ -1,33 +0,0 @@
{% load pretty_money %}
{% load i18n %}
Récapitulatif de trésorerie au {{ summary.date|date:"d/m/Y" }} à {{ summary.date|date:"H:i:s" }} :
Tous les utilisateur⋅rices :
- Positifs : {{ summary.total_positive_user }} soit {{ summary.balance_positive_user / 100 }} €
- Neutres : {{ summary.total_zero_user }}
- Négatifs : {{ summary.total_negative_user }} soit {{ summary.balance_negative_user / 100 }} €
Les {{ summary.total_positive_user_bde + summary.total_zero_user_bde + summary.total_negative_user_bde }} adhérent⋅es BDE :
- Positifs : {{ summary.total_positive_user_bde }} soit {{ summary.balance_positive_user_bde / 100 }} €
- Neutres : {{ summary.total_zero_user_bde }}
- Négatifs : {{ summary.total_negative_user_bde }} soit {{ summary.balance_negative_user_bde /100 }} €
Clubs :
- Positifs : {{ summary.total_positive_club }} soit {{ summary.balance_positive_club / 100 }} €
- Neutres : {{ summary.total_zero_club }}
- Négatifs : {{ summary.total_negative_club }} soit {{ summary.balance_negative_club / 100 }} €
Clubs hors BDE / Kfet et club dont le nom fini par "- BDE" :
- Positifs : {{ summary.total_positive_club_nbde }} soit {{ summary.balance_positive_club_nbde / 100 }} €
- Neutres : {{ summary.total_zero_club_nbde }}
- Négatifs : {{ summary.total_negative_club_nbde }} soit {{ summary.balance_negative_club_nbde / 100 }} €
Progression :
- Ceci correspond à une différence de {{ balance_difference_user / 100 }} € pour les utilisateur⋅rices
- Ceci correspond à une différence de {{ balance_difference_club / 100 }} € pour les clubs
--
Le BDE
{% trans "Mail generated by the Note Kfet on the" %} {% now "j F Y à H:i:s" %}

View File

@ -89,7 +89,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</ul> </ul>
<div class="card-body"> <div class="card-body">
<select id="debit_type" class="form-control custom-select d-none"> <select id="debit_type" class="form-control custom-select d-none">
{% for special_type in special_types %} {% for special_type in special_types|slice:"::-1" %}
<option value="{{ special_type.id }}">{{ special_type.special_type }}</option> <option value="{{ special_type.id }}">{{ special_type.special_type }}</option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -324,7 +324,7 @@
"mask": 2, "mask": 2,
"field": "", "field": "",
"permanent": false, "permanent": false,
"description": "Créer une transaction de ou vers la note d'un club" "description": "Créer une transaction de ou vers la note d'un club tant que la source reste au dessus de -20 €"
} }
}, },
{ {
@ -3816,7 +3816,7 @@
"field": "", "field": "",
"permanent": false, "permanent": false,
"description": "Créer une transaction vers la note d'un club" "description": "Créer une transaction vers la note d'un club"
} }
}, },
{ {
"model": "permission.permission", "model": "permission.permission",
@ -4186,6 +4186,86 @@
"description": "Voir la note d'un club enfant" "description": "Voir la note d'un club enfant"
} }
}, },
{
"model": "permission.permission",
"pk": 266,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source_alias\": \"Carte bancaire\"}, {\"source_alias\": \"Espèces\"}, {\"source_alias\": \"Chèque\"}, {\"source_alias\": \"Virement bancaire\"}]",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir les transactions de rechargement"
}
},
{
"model": "permission.permission",
"pk": 267,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source_alias\": \"Carte bancaire\"}, {\"source_alias\": \"Espèces\"}, {\"source_alias\": \"Chèque\"}, {\"source_alias\": \"Virement bancaire\"}]",
"type": "change",
"mask": 2,
"field": "valid",
"permanent": false,
"description": "Mettre à jour le statut de validation d'une transaction de rechargement"
}
},
{
"model": "permission.permission",
"pk": 268,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source_alias\": \"Carte bancaire\"}, {\"source_alias\": \"Espèces\"}, {\"source_alias\": \"Chèque\"}, {\"source_alias\": \"Virement bancaire\"}]",
"type": "change",
"mask": 2,
"field": "invalidity_reason",
"permanent": false,
"description": "Modifier la raison d'invalidité d'une transaction de rechargement"
}
},
{
"model": "permission.permission",
"pk": 269,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"OR\", {\"source_alias\": \"Carte bancaire\"}, {\"source_alias\": \"Espèces\"}, {\"source_alias\": \"Chèque\"}, {\"source_alias\": \"Virement bancaire\"}]",
"type": "add",
"mask": 2,
"field": "",
"permanent": false,
"description": "Créer une transaction de rechargement"
}
},
{
"model": "permission.permission",
"pk": 270,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 2,
"field": "",
"permanent": false,
"description": "Créer une transaction de ou vers la note d'un club tant que la source reste au dessus de -50 €"
}
},
{ {
"model": "permission.role", "model": "permission.role",
"pk": 1, "pk": 1,
@ -4732,8 +4812,10 @@
168, 168,
176, 176,
177, 177,
178,
197, 197,
211 211,
244
] ]
} }
}, },

View File

@ -5,13 +5,13 @@ from django.contrib import admin
from note_kfet.admin import admin_site from note_kfet.admin import admin_site
from .forms import ProductForm from .forms import ProductForm
from .models import Invoice, NoteSummary, Product, RemittanceType, Remittance, SogeCredit from .models import RemittanceType, Remittance, SogeCredit, Invoice, Product
@admin.register(RemittanceType, site=admin_site) @admin.register(RemittanceType, site=admin_site)
class RemittanceTypeAdmin(admin.ModelAdmin): class RemittanceTypeAdmin(admin.ModelAdmin):
""" """
Admin customisation for RemittanceType Admin customisation for RemiitanceType
""" """
list_display = ('note', ) list_display = ('note', )
@ -55,19 +55,3 @@ class InvoiceAdmin(admin.ModelAdmin):
""" """
list_display = ('object', 'id', 'bde', 'name', 'date', 'acquitted',) list_display = ('object', 'id', 'bde', 'name', 'date', 'acquitted',)
inlines = (ProductInline,) inlines = (ProductInline,)
@admin.register(NoteSummary, site=admin_site)
class NoteSummaryAdmin(admin.ModelAdmin):
"""
Admin customisation for NoteSummary
"""
list_display = (
'date', 'total_positive_user', 'balance_positive_user', 'total_positive_user_bde',
'balance_positive_user_bde', 'total_zero_user', 'total_zero_user_bde', 'total_negative_user',
'balance_negative_user', 'total_negative_user_bde', 'balance_negative_user_bde',
'total_vnegative_user', 'balance_vnegative_user', 'total_vnegative_user_bde',
'balance_vnegative_user_bde', 'total_positive_club', 'balance_positive_club',
'total_positive_club_nbde', 'balance_positive_club_nbde', 'total_zero_club', 'total_zero_club_nbde',
'total_negative_club', 'balance_negative_club', 'total_negative_club_nbde', 'balance_negative_club_nbde',
)

View File

@ -1,49 +0,0 @@
# Generated by Django 2.2.28 on 2024-08-07 12:09
import datetime
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0008_auto_20240322_0045'),
]
operations = [
migrations.CreateModel(
name='NoteSummary',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(default=datetime.date.today, verbose_name='Date')),
('total_positive_user', models.PositiveIntegerField(verbose_name='Total positive user')),
('balance_positive_user', models.PositiveIntegerField(verbose_name='Balance positive user')),
('total_positive_user_bde', models.PositiveIntegerField(verbose_name='Total positive user BDE')),
('balance_positive_user_bde', models.PositiveIntegerField(verbose_name='Balance positive user BDE')),
('total_zero_user', models.PositiveIntegerField(verbose_name='Total zero user')),
('total_zero_user_bde', models.PositiveIntegerField(verbose_name='Total zero user BDE')),
('total_negative_user', models.PositiveIntegerField(verbose_name='Total negative user')),
('balance_negative_user', models.PositiveIntegerField(verbose_name='Balance negative user')),
('total_negative_user_bde', models.PositiveIntegerField(verbose_name='Total negative user BDE')),
('balance_negative_user_bde', models.PositiveIntegerField(verbose_name='Balance negative user BDE')),
('total_vnegative_user', models.PositiveIntegerField(verbose_name='Total very negative user')),
('balance_vnegative_user', models.PositiveIntegerField(verbose_name='Balance very negative user')),
('total_vnegative_user_bde', models.PositiveIntegerField(verbose_name='Total very negative user BDE')),
('balance_vnegative_user_bde', models.PositiveIntegerField(verbose_name='Balance very negative user BDE')),
('total_positive_club', models.PositiveIntegerField(verbose_name='Total positive club')),
('balance_positive_club', models.PositiveIntegerField(verbose_name='Balance positive club')),
('total_positive_club_nbde', models.PositiveIntegerField(verbose_name='Total positive club nbde')),
('balance_positive_club_nbde', models.PositiveIntegerField(verbose_name='Balance positive club nbde')),
('total_zero_club', models.PositiveIntegerField(verbose_name='Total zero club')),
('total_zero_club_nbde', models.PositiveIntegerField(verbose_name='Total zero club nbde')),
('total_negative_club', models.PositiveIntegerField(verbose_name='Total negative club')),
('balance_negative_club', models.PositiveIntegerField(verbose_name='Balance negative club')),
('total_negative_club_nbde', models.PositiveIntegerField(verbose_name='Total negative club nbde')),
('balance_negative_club_nbde', models.PositiveIntegerField(verbose_name='Balance negative club nbde')),
],
options={
'verbose_name': 'Summary',
'verbose_name_plural': 'Summaries',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.20 on 2025-04-14 20:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0009_alter_sogecredit_transactions'),
]
operations = [
migrations.AlterField(
model_name='invoice',
name='bde',
field=models.CharField(choices=[('Diolistos', 'Diol[list]os'), ('RavePartlist', 'RavePart[list]'), ('SecretStorlist', 'SecretStor[list]'), ('TotalistSpies', 'Tota[list]Spies'), ('Saperlistpopette', 'Saper[list]popette'), ('Finalist', 'Fina[list]'), ('Listorique', '[List]orique'), ('Satellist', 'Satel[list]'), ('Monopolist', 'Monopo[list]'), ('Kataclist', 'Katac[list]')], default='Diolistos', max_length=32, verbose_name='BDE'),
),
]

View File

@ -27,8 +27,9 @@ class Invoice(models.Model):
bde = models.CharField( bde = models.CharField(
max_length=32, max_length=32,
default='RavePartlist', default='Diolistos',
choices=( choices=(
('Diolistos', 'Diol[list]os'),
('RavePartlist', 'RavePart[list]'), ('RavePartlist', 'RavePart[list]'),
('SecretStorlist', 'SecretStor[list]'), ('SecretStorlist', 'SecretStor[list]'),
('TotalistSpies', 'Tota[list]Spies'), ('TotalistSpies', 'Tota[list]Spies'),
@ -460,117 +461,3 @@ class SogeCredit(models.Model):
self.credit_transaction._force_save = True self.credit_transaction._force_save = True
self.credit_transaction.save() self.credit_transaction.save()
super().delete(**kwargs) super().delete(**kwargs)
class NoteSummary(models.Model):
"""
Summary of every notes
"""
date = models.DateField(
default=date.today,
verbose_name=_("Date"),
)
total_positive_user = models.PositiveIntegerField(
verbose_name=_("Total positive user"),
)
balance_positive_user = models.PositiveIntegerField(
verbose_name=_("Balance positive user"),
)
total_positive_user_bde = models.PositiveIntegerField(
verbose_name=_("Total positive user BDE"),
)
balance_positive_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance positive user BDE"),
)
total_zero_user = models.PositiveIntegerField(
verbose_name=_("Total zero user"),
)
total_zero_user_bde = models.PositiveIntegerField(
verbose_name=_("Total zero user BDE"),
)
total_negative_user = models.PositiveIntegerField(
verbose_name=_("Total negative user"),
)
balance_negative_user = models.PositiveIntegerField(
verbose_name=_("Balance negative user"),
)
total_negative_user_bde = models.PositiveIntegerField(
verbose_name=_("Total negative user BDE"),
)
balance_negative_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance negative user BDE"),
)
total_vnegative_user = models.PositiveIntegerField(
verbose_name=_("Total very negative user"),
)
balance_vnegative_user = models.PositiveIntegerField(
verbose_name=_("Balance very negative user"),
)
total_vnegative_user_bde = models.PositiveIntegerField(
verbose_name=_("Total very negative user BDE"),
)
balance_vnegative_user_bde = models.PositiveIntegerField(
verbose_name=_("Balance very negative user BDE"),
)
total_positive_club = models.PositiveIntegerField(
verbose_name=_("Total positive club"),
)
balance_positive_club = models.PositiveIntegerField(
verbose_name=_("Balance positive club"),
)
total_positive_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total positive club nbde"),
)
balance_positive_club_nbde = models.PositiveIntegerField(
verbose_name=_("Balance positive club nbde"),
)
total_zero_club = models.PositiveIntegerField(
verbose_name=_("Total zero club"),
)
total_zero_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total zero club nbde"),
)
total_negative_club = models.PositiveIntegerField(
verbose_name=_("Total negative club"),
)
balance_negative_club = models.PositiveIntegerField(
verbose_name=_("Balance negative club"),
)
total_negative_club_nbde = models.PositiveIntegerField(
verbose_name=_("Total negative club nbde"),
)
balance_negative_club_nbde = models.PositiveIntegerField(
verbose_name=_("Balance negative club nbde"),
)
class Meta:
verbose_name = _("Summary")
verbose_name_plural = _("Summaries")
def __str__(self):
return "Note summary of {date}".format(date=self.date)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

View File

@ -38,7 +38,7 @@ class Command(BaseCommand):
required=False, required=False,
help="""User will have their(s) wrapped generated, help="""User will have their(s) wrapped generated,
all = all users all = all users
adh = all users who have a valid memberships to BDE during the BDE considered adh = all users who have a valid cd memberships to BDE during the BDE considered
supersuser = all superusers supersuser = all superusers
custom user1,user2,... = a list of username, custom user1,user2,... = a list of username,
custom_id id1,id2,... = a list of user id""", custom_id id1,id2,... = a list of user id""",
@ -70,15 +70,7 @@ class Command(BaseCommand):
dest='create', dest='create',
) )
def handle(self, *args, **options): def handle(self, *args, **options): # NOQA
# useful string for output
red = '\033[31;1m'
yellow = '\033[33;1m'
green = '\033[32;1m'
abort = red + 'ABORT'
warning = yellow + 'WARNING'
success = green + 'SUCCESS'
# Traitement des paramètres # Traitement des paramètres
verb = options['verbosity'] verb = options['verbosity']
bde = [] bde = []
@ -89,11 +81,11 @@ class Command(BaseCommand):
if options['bde_id']: if options['bde_id']:
if bde: if bde:
if verb >= 1: if verb >= 1:
print(warning) self.stdout.write(self.style.WARNING(
print(yellow + 'You already defined bde with their name !') "WARNING\nYou already defined bde with their name !"))
if verb >= 0: if verb >= 0:
print(abort) self.stdout.write(self.style.ERROR("ABORT"))
return exit(1)
bde_id = options['bde_id'].split(',') bde_id = options['bde_id'].split(',')
bde = [Bde.objects.get(pk=i) for i in bde_id] bde = [Bde.objects.get(pk=i) for i in bde_id]
@ -113,11 +105,11 @@ class Command(BaseCommand):
user = ['custom_id', [User.objects.get(pk=u) for u in user_id]] user = ['custom_id', [User.objects.get(pk=u) for u in user_id]]
else: else:
if verb >= 1: if verb >= 1:
print(warning) self.sdtout.write(self.style.WARNING(
print(yellow + 'You user option is not recognized') "WARNING\nYou user option is not recognized"))
if verb >= 0: if verb >= 0:
print(abort) self.stdout.write(self.style.ERROR("ABORT"))
return exit(1)
club = [] club = []
if options['club']: if options['club']:
@ -133,11 +125,11 @@ class Command(BaseCommand):
club = ['custom_id', [Club.objects.get(pk=c) for c in club_id]] club = ['custom_id', [Club.objects.get(pk=c) for c in club_id]]
else: else:
if verb >= 1: if verb >= 1:
print(warning) self.stdout.write(self.style.WARNING(
print(yellow + 'You club option is not recognized') "WARNING\nYou club option is not recognized"))
if verb >= 0: if verb >= 0:
print(abort) self.stdout.write(self.style.ERROR("ABORT"))
return exit(1)
change = options['change'] change = options['change']
create = options['create'] create = options['create']
@ -145,72 +137,75 @@ class Command(BaseCommand):
# check if parameters are sufficient for generate wrapped with the desired option # check if parameters are sufficient for generate wrapped with the desired option
if not bde: if not bde:
if verb >= 1: if verb >= 1:
print(warning) self.stdout.write(self.style.WARNING(
print(yellow + 'You have not selectionned a BDE !') "WARNING\nYou have not selectionned a BDE !"))
if verb >= 0: if verb >= 0:
print(abort) self.stdout.write(self.style.ERROR("ABORT"))
return exit(1)
if not (user or club): if not (user or club):
if verb >= 1: if verb >= 1:
print(warning) self.stdout.write(self.style.WARNING(
print(yellow + 'No club or user selected !') "WARNING\nNo club or user selected !"))
if verb >= 0: if verb >= 0:
print(abort) self.stdout.write(self.style.ERROR("ABORT"))
return exit(1)
if verb >= 3: if verb >= 3:
print('\033[1mOptions:\033[m') self.stdout.write("Options:")
bde_str = '' bde_str = ''
for b in bde: for b in bde:
bde_str += str(b) bde_str += str(b) + '\n'
print('BDE: ' + bde_str) self.stdout.write("BDE: " + bde_str)
if user: if user:
print('User: ' + user[0]) self.stdout.write('User: ' + user[0])
if club: if club:
print('Club: ' + club[0]) self.stdout.write('Club: ' + club[0])
print('change: ' + str(change)) self.stdout.write('change: ' + str(change))
print('create: ' + str(create)) self.stdout.write('create: ' + str(create) + '\n')
print('')
if not (change or create): if not (change or create):
if verb >= 1: if verb >= 1:
print(warning) self.stdout.write(self.style.WARNING(
print(yellow + 'change and create is set to false, none wrapped will be created') "WARNING\nchange and create is set to false, none wrapped will be created"))
if verb >= 0: if verb >= 0:
print(abort) self.stdout.write(self.style.ERROR("ABORT"))
return exit(1)
if verb >= 1 and change: if verb >= 1 and change:
print(warning) self.stdout.write(self.style.WARNING(
print(yellow + 'change is set to true, some wrapped may be replaced !') "WARNING\nchange is set to true, some wrapped may be replaced !"))
if verb >= 1 and not create: if verb >= 1 and not create:
print(warning) self.stdout.write(self.style.WARNING(
print(yellow + 'create is set to false, wrapped will not be created !') "WARNING\ncreate is set to false, wrapped will not be created !"))
if verb >= 3 or change or not create: if verb >= 3 or change or not create:
a = str(input('\033[mContinue ? (y/n) ')).lower() a = str(input('\033[mContinue ? (y/n) ')).lower()
if a in ['n', 'no', 'non', '0']: if a in ['n', 'no', 'non', '0']:
if verb >= 0: if verb >= 0:
print(abort) self.stdout.write(self.style.ERROR("ABORT"))
return exit(1)
note = self.convert_to_note(change, create, bde=bde, user=user, club=club, verb=verb) note = self.convert_to_note(change, create, bde=bde, user=user, club=club, verb=verb)
if verb >= 1: if verb >= 1:
print("\033[32mUser and/or Club given has successfully convert in their note\033[m") self.stdout.write(self.style.SUCCESS(
"User and/or Club given has successfully convert in their note"))
global_data = self.global_data(bde, verb=verb) global_data = self.global_data(bde, verb=verb)
if verb >= 1: if verb >= 1:
print("\033[32mGlobal data has been successfully generated\033[m") self.stdout.write(self.style.SUCCESS(
"Global data has been successfully generated"))
unique_data = self.unique_data(bde, note, global_data=global_data, verb=verb) unique_data = self.unique_data(bde, note, global_data=global_data, verb=verb)
if verb >= 1: if verb >= 1:
print("\033[32mUnique data has been successfully generated\033[m") self.stdout.write(self.style.SUCCESS(
"Unique data has been successfully generated"))
self.make_wrapped(unique_data, note, bde, change, create, verb=verb) self.make_wrapped(unique_data, note, bde, change, create, verb=verb)
if verb >= 1: if verb >= 1:
print(green + "The wrapped has been generated !") self.stdout.write(self.style.SUCCESS(
"The wrapped has been generated !"))
if verb >= 0: if verb >= 0:
print(success) self.stdout.write(self.style.SUCCESS("SUCCESS"))
exit(0)
return def convert_to_note(self, change, create, bde=None, user=None, club=None, verb=1): # NOQA
def convert_to_note(self, change, create, bde=None, user=None, club=None, verb=1):
notes = [] notes = []
for b in bde: for b in bde:
note_for_bde = Note.objects.filter(pk__lte=-1) note_for_bde = Note.objects.filter(pk__lte=-1)
@ -253,17 +248,17 @@ class Command(BaseCommand):
note_for_bde = self.filter_note(b, note_for_bde, change, create, verb=verb) note_for_bde = self.filter_note(b, note_for_bde, change, create, verb=verb)
notes.append(note_for_bde) notes.append(note_for_bde)
if verb >= 2: if verb >= 2:
print("\033[m{nb} note selectionned for bde {bde}".format(nb=len(note_for_bde), bde=b.name)) self.stdout.write(f"{len(note_for_bde)} note selectionned for bde {b.name}")
return notes return notes
def global_data(self, bde, verb=1): def global_data(self, bde, verb=1): # NOQA
data = {} data = {}
for b in bde: for b in bde:
if b.name == 'Rave Part[list]': if b.name == 'Rave Part[list]':
if verb >= 2: if verb >= 2:
print("Begin to make global data") self.stdout.write("Begin to make global data")
if verb >= 3: if verb >= 3:
print('nb_transaction') self.stdout.write("nb_transaction")
# nb total de transactions # nb total de transactions
data['nb_transaction'] = Transaction.objects.filter( data['nb_transaction'] = Transaction.objects.filter(
created_at__gte=b.date_start, created_at__gte=b.date_start,
@ -271,7 +266,7 @@ class Command(BaseCommand):
valid=True).count() valid=True).count()
if verb >= 3: if verb >= 3:
print('nb_vieux_con') self.stdout.write("nb_vieux_con")
# nb total de vielleux con·ne·s derrière le bar # nb total de vielleux con·ne·s derrière le bar
button_id = [2884, 2585] button_id = [2884, 2585]
transactions = Transaction.objects.filter( transactions = Transaction.objects.filter(
@ -286,7 +281,7 @@ class Command(BaseCommand):
data['nb_vieux_con'] = q data['nb_vieux_con'] = q
if verb >= 3: if verb >= 3:
print('nb_soiree') self.stdout.write("nb_soiree")
# nb total de soirée # nb total de soirée
a_type_id = [1, 2, 4, 5, 7, 10] a_type_id = [1, 2, 4, 5, 7, 10]
data['nb_soiree'] = Activity.objects.filter( data['nb_soiree'] = Activity.objects.filter(
@ -296,7 +291,7 @@ class Command(BaseCommand):
activity_type__pk__in=a_type_id).count() activity_type__pk__in=a_type_id).count()
if verb >= 3: if verb >= 3:
print('pots, nb_entree_pot') self.stdout.write('pots, nb_entree_pot')
# nb d'entrée totale aux pots # nb d'entrée totale aux pots
pot_id = [1, 4, 10] pot_id = [1, 4, 10]
pots = Activity.objects.filter( pots = Activity.objects.filter(
@ -310,7 +305,7 @@ class Command(BaseCommand):
data['nb_entree_pot'] += Entry.objects.filter(activity=pot).count() data['nb_entree_pot'] += Entry.objects.filter(activity=pot).count()
if verb >= 3: if verb >= 3:
print('top3_buttons') self.stdout.write('top3_buttons')
# top 3 des boutons les plus cliqués # top 3 des boutons les plus cliqués
transactions = Transaction.objects.filter( transactions = Transaction.objects.filter(
created_at__gte=b.date_start, created_at__gte=b.date_start,
@ -329,7 +324,7 @@ class Command(BaseCommand):
data['top3_buttons'] = list(sorted(d.items(), key=lambda item: item[1], reverse=True))[:3] data['top3_buttons'] = list(sorted(d.items(), key=lambda item: item[1], reverse=True))[:3]
if verb >= 3: if verb >= 3:
print('class_conso_all') self.stdout.write('class_conso_all')
# le classement des plus gros consommateurs (BDE + club) # le classement des plus gros consommateurs (BDE + club)
transactions = Transaction.objects.filter( transactions = Transaction.objects.filter(
created_at__gte=b.date_start, created_at__gte=b.date_start,
@ -348,7 +343,7 @@ class Command(BaseCommand):
data['class_conso_all'] = dict(sorted(d.items(), key=lambda item: item[1], reverse=True)) data['class_conso_all'] = dict(sorted(d.items(), key=lambda item: item[1], reverse=True))
if verb >= 3: if verb >= 3:
print('class_conso_bde') self.stdout.write('class_conso_bde')
# le classement des plus gros consommateurs BDE # le classement des plus gros consommateurs BDE
transactions = Transaction.objects.filter( transactions = Transaction.objects.filter(
created_at__gte=b.date_start, created_at__gte=b.date_start,
@ -368,11 +363,10 @@ class Command(BaseCommand):
else: else:
# make your wrapped or reuse previous wrapped # make your wrapped or reuse previous wrapped
raise NotImplementedError("The BDE: {bde_name} has not personalized wrapped, make it !" raise NotImplementedError(f"The BDE: {b.name} has not personalized wrapped, make it !")
.format(bde_name=b.name))
return data return data
def unique_data(self, bde, note, global_data=None, verb=1): def unique_data(self, bde, note, global_data=None, verb=1): # NOQA
data = [] data = []
for i in range(len(bde)): for i in range(len(bde)):
data_bde = [] data_bde = []
@ -380,8 +374,7 @@ class Command(BaseCommand):
if verb >= 3: if verb >= 3:
total = len(note[i]) total = len(note[i])
current = 0 current = 0
print('Make {nb} data for wrapped sponsored by {bde}' self.stdout.write(f"Make {total} data for wrapped sponsored by {bde[i].name}")
.format(nb=total, bde=bde[i].name))
for n in note[i]: for n in note[i]:
d = {} d = {}
if 'user' in n.__dir__(): if 'user' in n.__dir__():
@ -542,12 +535,11 @@ class Command(BaseCommand):
data_bde.append(json.dumps(d)) data_bde.append(json.dumps(d))
if verb >= 3: if verb >= 3:
current += 1 current += 1
print('\033[2K' + '({c}/{t})'.format(c=current, t=total) + '\033[1A') self.stdout.write("\033[2K" + f"({current}/{total})" + "\033[1A")
else: else:
# make your wrapped or reuse previous wrapped # make your wrapped or reuse previous wrapped
raise NotImplementedError("The BDE: {bde_name} has not personalized wrapped, make it !" raise NotImplementedError(f"The BDE: {bde[i].name} has not personalized wrapped, make it !")
.format(bde_name=bde[i].name))
data.append(data_bde) data.append(data_bde)
return data return data
@ -557,7 +549,7 @@ class Command(BaseCommand):
total = 0 total = 0
for n in note: for n in note:
total += len(n) total += len(n)
print('\033[mMake {nb} wrapped'.format(nb=total)) self.stdout.write(f"Make {total} wrapped")
for i in range(len(bde)): for i in range(len(bde)):
for j in range(len(note[i])): for j in range(len(note[i])):
if create and not Wrapped.objects.filter(bde=bde[i], note=note[i][j]): if create and not Wrapped.objects.filter(bde=bde[i], note=note[i][j]):
@ -572,7 +564,7 @@ class Command(BaseCommand):
w.save() w.save()
if verb >= 3: if verb >= 3:
current += 1 current += 1
print('\033[2K' + '({c}/{t})'.format(c=current, t=total) + '\033[1A') self.stdout.write("\033[2K" + f"({current}/{total})" + "\033[1A")
return return
def filter_note(self, bde, note, change, create, verb=1): def filter_note(self, bde, note, change, create, verb=1):

View File

View File

@ -0,0 +1,91 @@
# Copyright (C) 2018-2025 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import timedelta
from api.tests import TestAPI
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from ..api.views import WrappedViewSet, BdeViewSet
from ..models import Bde, Wrapped
class TestWrapped(TestCase):
"""
Test activities
"""
fixtures = ('initial',)
def setUp(self):
self.user = User.objects.create_superuser(
username="admintoto",
password="tototototo",
email="toto@example.com"
)
self.client.force_login(self.user)
sess = self.client.session
sess["permission_mask"] = 42
sess.save()
self.bde = Bde.objects.create(
name="The best BDE",
date_start=timezone.now() - timedelta(days=365),
date_end=timezone.now(),
)
self.wrapped = Wrapped.objects.create(
generated=True,
public=False,
bde=self.bde,
note=self.user.note,
data_json="{}",
)
def test_wrapped_list(self):
"""
Display the list of all wrapped
"""
response = self.client.get(reverse("wrapped:wrapped_list"))
self.assertEqual(response.status_code, 200)
def test_wrapped_detail(self):
"""
Display the detail of an wrapped
"""
response = self.client.get(reverse("wrapped:wrapped_detail", args=(self.wrapped.pk,)))
self.assertEqual(response.status_code, 200)
class TestWrappedAPI(TestAPI):
def setUp(self) -> None:
super().setUp()
self.bde = Bde.objects.create(
name="The best BDE",
date_start=timezone.now() - timedelta(days=365),
date_end=timezone.now(),
)
self.wrapped = Wrapped.objects.create(
generated=True,
public=False,
bde=self.bde,
note=self.user.note,
data_json="{}",
)
def test_bde_api(self):
"""
Load Bde API page and test all filters and permissions
"""
self.check_viewset(BdeViewSet, "/api/wrapped/bde/")
def test_wrapped_api(self):
"""
Load Wrapped API page and test all filters and permissions
"""
self.check_viewset(WrappedViewSet, "/api/wrapped/wrapped/")

View File

@ -55,6 +55,7 @@ Les adhérent⋅es ont la possibilité d'inviter des ami⋅es. Pour cela, les di
* Activité concernée (clé étrangère) * Activité concernée (clé étrangère)
* Nom de famille * Nom de famille
* Prénom * Prénom
* École
* Note de la personne ayant invité * Note de la personne ayant invité
Certaines contraintes s'appliquent : Certaines contraintes s'appliquent :

View File

@ -43,6 +43,11 @@ On a ensuite besoin de définir nos propres scopes afin d'avoir des permissions
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes', 'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator", 'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14), 'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
'PKCE_REQUIRED': False,
'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY':
os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'),
'SCOPES': { 'openid': "OpenID Connect scope" },
} }
Cela a pour effet d'avoir des scopes sous la forme ``PERMISSION_CLUB``, Cela a pour effet d'avoir des scopes sous la forme ``PERMISSION_CLUB``,
@ -57,6 +62,14 @@ On ajoute enfin les routes dans ``urls.py`` :
path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')) path('o/', include('oauth2_provider.urls', namespace='oauth2_provider'))
) )
Enfin pour utiliser OIDC, il faut générer une clé privé que l'on va, par défaut,
mettre dans `/var/secrets/oidc.key` :
.. code:: bash
cd /var/secrets/
openssl genrsa -out oidc.key 4096
L'OAuth2 est désormais prêt à être utilisé. L'OAuth2 est désormais prêt à être utilisé.

View File

@ -227,6 +227,22 @@ En production, ce fichier contient :
) )
Génération d'une clé privé pour OIDC
------------------------------------
Pour pouvoir proposer le service de connexion Openid Connect (OIDC) par OAuth2, il y a
besoin d'une clé privé. Par défaut, elle est cherché dans le fichier `/var/secrets/oidc.key`
(sinon, il faut modifier l'emplacement dans les fichiers de configurations).
Pour générer la clé, il faut aller dans le dossier `/var/secrets` (à créer, si nécessaire) puis
utiliser la commande de génération :
.. code:: bash
cd /var/secrets
openssl genrsa -out oidc.key 4096
Configuration des tâches récurrentes Configuration des tâches récurrentes
------------------------------------ ------------------------------------

View File

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-03-13 21:08+0100\n" "POT-Creation-Date: 2025-03-25 11:16+0100\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n" "PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: bleizi <bleizi@crans.org>\n" "Last-Translator: bleizi <bleizi@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n" "Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
@ -25,7 +25,7 @@ msgid "This opener already exists"
msgstr "Cette amitié existe déjà" msgstr "Cette amitié existe déjà"
#: apps/activity/apps.py:10 apps/activity/models.py:129 #: apps/activity/apps.py:10 apps/activity/models.py:129
#: apps/activity/models.py:169 apps/activity/models.py:323 #: apps/activity/models.py:169 apps/activity/models.py:328
msgid "activity" msgid "activity"
msgstr "activité" msgstr "activité"
@ -37,24 +37,24 @@ msgstr "La note du club est inactive."
msgid "The end date must be after the start date." msgid "The end date must be after the start date."
msgstr "La date de fin doit être après celle de début." msgstr "La date de fin doit être après celle de début."
#: apps/activity/forms.py:83 apps/activity/models.py:271 #: apps/activity/forms.py:83 apps/activity/models.py:276
msgid "You can't invite someone once the activity is started." msgid "You can't invite someone once the activity is started."
msgstr "" msgstr ""
"Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré." "Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré."
#: apps/activity/forms.py:86 apps/activity/models.py:274 #: apps/activity/forms.py:86 apps/activity/models.py:279
msgid "This activity is not validated yet." msgid "This activity is not validated yet."
msgstr "Cette activité n'est pas encore validée." msgstr "Cette activité n'est pas encore validée."
#: apps/activity/forms.py:96 apps/activity/models.py:282 #: apps/activity/forms.py:96 apps/activity/models.py:287
msgid "This person has been already invited 5 times this year." msgid "This person has been already invited 5 times this year."
msgstr "Cette personne a déjà été invitée 5 fois cette année." msgstr "Cette personne a déjà été invitée 5 fois cette année."
#: apps/activity/forms.py:100 apps/activity/models.py:286 #: apps/activity/forms.py:100 apps/activity/models.py:291
msgid "This person is already invited." msgid "This person is already invited."
msgstr "Cette personne est déjà invitée." msgstr "Cette personne est déjà invitée."
#: apps/activity/forms.py:104 apps/activity/models.py:290 #: apps/activity/forms.py:104 apps/activity/models.py:295
msgid "You can't invite more than 3 people to this activity." msgid "You can't invite more than 3 people to this activity."
msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
@ -228,32 +228,36 @@ msgstr "nom de famille"
msgid "first name" msgid "first name"
msgstr "prénom" msgstr "prénom"
#: apps/activity/models.py:254 #: apps/activity/models.py:252
msgid "school"
msgstr "école"
#: apps/activity/models.py:259
msgid "inviter" msgid "inviter"
msgstr "hôte" msgstr "hôte"
#: apps/activity/models.py:258 #: apps/activity/models.py:263
msgid "guest" msgid "guest"
msgstr "invité·e" msgstr "invité·e"
#: apps/activity/models.py:259 #: apps/activity/models.py:264
msgid "guests" msgid "guests"
msgstr "invité·e·s" msgstr "invité·e·s"
#: apps/activity/models.py:312 #: apps/activity/models.py:317
msgid "Invitation" msgid "Invitation"
msgstr "Invitation" msgstr "Invitation"
#: apps/activity/models.py:330 apps/activity/models.py:334 #: apps/activity/models.py:335 apps/activity/models.py:339
msgid "Opener" msgid "Opener"
msgstr "Ouvreur⋅se" msgstr "Ouvreur⋅se"
#: apps/activity/models.py:335 #: apps/activity/models.py:340
#: apps/activity/templates/activity/activity_detail.html:16 #: apps/activity/templates/activity/activity_detail.html:16
msgid "Openers" msgid "Openers"
msgstr "Ouvreur⋅ses" msgstr "Ouvreur⋅ses"
#: apps/activity/models.py:339 #: apps/activity/models.py:344
#, fuzzy, python-brace-format #, fuzzy, python-brace-format
#| msgid "Entry for {note} to the activity {activity}" #| msgid "Entry for {note} to the activity {activity}"
msgid "{opener} is opener of activity {acivity}" msgid "{opener} is opener of activity {acivity}"
@ -463,25 +467,25 @@ msgstr "Détails de l'activité"
msgid "Update activity" msgid "Update activity"
msgstr "Modifier l'activité" msgstr "Modifier l'activité"
#: apps/activity/views.py:177 #: apps/activity/views.py:178
msgid "Invite guest to the activity \"{}\"" msgid "Invite guest to the activity \"{}\""
msgstr "Invitation pour l'activité « {} »" msgstr "Invitation pour l'activité « {} »"
#: apps/activity/views.py:217 #: apps/activity/views.py:218
msgid "You are not allowed to display the entry interface for this activity." msgid "You are not allowed to display the entry interface for this activity."
msgstr "" msgstr ""
"Vous n'êtes pas autorisé·e à afficher l'interface des entrées pour cette " "Vous n'êtes pas autorisé·e à afficher l'interface des entrées pour cette "
"activité." "activité."
#: apps/activity/views.py:220 #: apps/activity/views.py:221
msgid "This activity does not support activity entries." msgid "This activity does not support activity entries."
msgstr "Cette activité ne requiert pas d'entrées." msgstr "Cette activité ne requiert pas d'entrées."
#: apps/activity/views.py:223 #: apps/activity/views.py:224
msgid "This activity is closed." msgid "This activity is closed."
msgstr "Cette activité est fermée." msgstr "Cette activité est fermée."
#: apps/activity/views.py:328 #: apps/activity/views.py:329
msgid "Entry for activity \"{}\"" msgid "Entry for activity \"{}\""
msgstr "Entrées pour l'activité « {} »" msgstr "Entrées pour l'activité « {} »"
@ -1985,16 +1989,10 @@ msgstr "Historique des transactions récentes"
#: apps/note/templates/note/mails/negative_balance.txt:25 #: apps/note/templates/note/mails/negative_balance.txt:25
#: apps/note/templates/note/mails/negative_notes_report.html:46 #: apps/note/templates/note/mails/negative_notes_report.html:46
#: apps/note/templates/note/mails/negative_notes_report.txt:13 #: apps/note/templates/note/mails/negative_notes_report.txt:13
#: apps/note/templates/note/mails/summary_notes_report.html:62
#: apps/note/templates/note/mails/summary_ntoes_report.txt:33
#: apps/note/templates/note/mails/weekly_report.html:51 #: apps/note/templates/note/mails/weekly_report.html:51
#: apps/note/templates/note/mails/weekly_report.txt:32 #: apps/note/templates/note/mails/weekly_report.txt:32
#: apps/registration/templates/registration/mails/email_validation_email.html:40 #: apps/registration/templates/registration/mails/email_validation_email.html:40
#: apps/registration/templates/registration/mails/email_validation_email.txt:16 #: apps/registration/templates/registration/mails/email_validation_email.txt:16
#: apps/scripts/templates/scripts/horaires.html:35
#: apps/scripts/templates/scripts/horaires.txt:17
#: apps/scripts/templates/scripts/intro_mail.html:49
#: apps/scripts/templates/scripts/intro_mail.txt:25
msgid "Mail generated by the Note Kfet on the" msgid "Mail generated by the Note Kfet on the"
msgstr "Mail généré par la Note Kfet le" msgstr "Mail généré par la Note Kfet le"
@ -2565,7 +2563,6 @@ msgid "Address"
msgstr "Adresse" msgstr "Adresse"
#: apps/treasury/models.py:69 apps/treasury/models.py:202 #: apps/treasury/models.py:69 apps/treasury/models.py:202
#: apps/treasury/models.py:472
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
@ -2685,102 +2682,6 @@ msgstr ""
"sa note. Merci de lui demander de recharger sa note avant d'invalider ce " "sa note. Merci de lui demander de recharger sa note avant d'invalider ce "
"crédit." "crédit."
#: apps/treasury/models.py:476
msgid "Total positive user"
msgstr "Nombre d'utilisateur⋅rices en positif"
#: apps/treasury/models.py:480
msgid "Balance positive user"
msgstr "Solde des utilisateur⋅rices en positif"
#: apps/treasury/models.py:484
msgid "Total positive user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en positif"
#: apps/treasury/models.py:488
msgid "Balance positive user BDE"
msgstr "Solde des adhérent⋅es au BDE en positif"
#: apps/treasury/models.py:492
msgid "Total zero user"
msgstr "Nombre d'utilisateur⋅rices à zéro"
#: apps/treasury/models.py:496
msgid "Total zero user BDE"
msgstr "Nombre d'adhérent⋅es au BDE à zéro"
#: apps/treasury/models.py:500
msgid "Total negative user"
msgstr "Nombre d'utilisateur⋅rices en négatif"
#: apps/treasury/models.py:504
msgid "Balance negative user"
msgstr "Solde des utilisateur⋅rices en négatif"
#: apps/treasury/models.py:508
msgid "Total negative user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en négatif"
#: apps/treasury/models.py:512
msgid "Balance negative user BDE"
msgstr "Solde des adhérent⋅es au BDE en négatif"
#: apps/treasury/models.py:516
msgid "Total very negative user"
msgstr "Nombre d'utilisateur⋅rices en négatif sévère"
#: apps/treasury/models.py:520
msgid "Balance very negative user"
msgstr "Solde des utilisateur⋅rices en négatif sévère"
#: apps/treasury/models.py:524
msgid "Total very negative user BDE"
msgstr "Nombre d'adhérent⋅es au BDE en négatif sévère"
#: apps/treasury/models.py:528
msgid "Balance very negative user BDE"
msgstr "Solde des adhérent⋅es au BDE en négatif sévère"
#: apps/treasury/models.py:532
msgid "Total positive club"
msgstr "Nombre de clubs en positif"
#: apps/treasury/models.py:536
msgid "Balance positive club"
msgstr "Solde des clubs en positif"
#: apps/treasury/models.py:540
msgid "Total positive club nbde"
msgstr "Nombre de clubs non-BDE en positif"
#: apps/treasury/models.py:544
msgid "Balance positive club nbde"
msgstr "Solde des clubs non-BDE en positif"
#: apps/treasury/models.py:548
msgid "Total zero club"
msgstr "Nombre de clubs à zéro"
#: apps/treasury/models.py:552
msgid "Total zero club nbde"
msgstr "Nombre de clubs non-BDE à zéro"
#: apps/treasury/models.py:556
msgid "Total negative club"
msgstr "Nombre de clubs en négatif"
#: apps/treasury/models.py:560
msgid "Balance negative club"
msgstr "Solde des clubs en négatif"
#: apps/treasury/models.py:564
msgid "Total negative club nbde"
msgstr "Nombre de clubs non-BDE en négatif"
#: apps/treasury/models.py:568
msgid "Balance negative club nbde"
msgstr "Solde des clubs non-BDE en négatif"
#: apps/treasury/tables.py:20 #: apps/treasury/tables.py:20
msgid "Invoice #{:d}" msgid "Invoice #{:d}"
msgstr "Facture n°{:d}" msgstr "Facture n°{:d}"

View File

@ -20,8 +20,6 @@ MAILTO=notekfet2020@lists.crans.org
00 5 * * 2 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --spam --negative-amount 1 -v 0 00 5 * * 2 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --spam --negative-amount 1 -v 0
# Envoyer le rapport mensuel aux trésoriers et respos info # Envoyer le rapport mensuel aux trésoriers et respos info
00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --report --add-years 1 -v 0 00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --report --add-years 1 -v 0
# Envoyer le recap de tresorerie
00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_summary_notes_report --negative-amount 2000
# Envoyer les rapports aux gens # Envoyer les rapports aux gens
55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0 55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0
# Mettre à jour les boutons mis en avant # Mettre à jour les boutons mis en avant
@ -29,5 +27,5 @@ MAILTO=notekfet2020@lists.crans.org
# Vider les tokens Oauth2 # Vider les tokens Oauth2
00 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py cleartokens -v 0 00 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py cleartokens -v 0
# Envoyer la liste des abonnés à la NL BDA # Envoyer la liste des abonnés à la NL BDA
00 10 * * 0 root cd /var/www/note_kfet && env/bin/python manage.py extract_ml_registrations -t art 00 10 * * 0 root cd /var/www/note_kfet && env/bin/python manage.py extract_ml_registrations -t art -e "bda.ensparissaclay@gmail.com"

View File

@ -268,6 +268,10 @@ OAUTH2_PROVIDER = {
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator", 'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14), 'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0) 'PKCE_REQUIRED': False, # PKCE (fix a breaking change of django-oauth-toolkit 2.0.0)
'OIDC_ENABLED': True,
'OIDC_RSA_PRIVATE_KEY':
os.getenv('OIDC_RSA_PRIVATE_KEY', '/var/secrets/oidc.key'),
'SCOPES': { 'openid': "OpenID Connect scope" },
} }
# Take control on how widget templates are sourced # Take control on how widget templates are sourced

96
note_kfet/static/css/custom.css Executable file → Normal file
View File

@ -61,20 +61,16 @@ mark {
/* Make navbar more readable */ /* Make navbar more readable */
.navbar-dark .navbar-nav .nav-link { .navbar-dark .navbar-nav .nav-link {
color: rgba(255, 255, 255, .75); color: rgba(255, 255, 255, .75);
text-shadow: 2px 2px 15px #ffeb40; /* text-shadow: 2px 2px 15px #ffeb40; */
} }
.navbar-brand { /* .navbar-brand { */
text-shadow: 2px 2px 15px #ffeb40; /* text-shadow: 2px 2px 15px #ffeb40; */
} /* }
/* Last BDE colors */ /* Last BDE colors */
.bg-primary { .bg-primary {
/* background-color: rgb(18, 67, 4) !important; */ background-color: rgb(102, 83, 105) !important;
/* MODE VIEUXCON=ON */
/* background-color: rgb(166, 0, 2) !important; */
background-color: rgb(0, 0, 0);
background-image: url('/static/img/rp_bg.png');
} }
html { html {
@ -89,94 +85,52 @@ body {
.btn-outline-primary:hover, .btn-outline-primary:hover,
.btn-outline-primary:not(:disabled):not(.disabled).active, .btn-outline-primary:not(:disabled):not(.disabled).active,
.btn-outline-primary:not(:disabled):not(.disabled):active { .btn-outline-primary:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0); color: #fff;
background-color: rgb(255, 0, 101); background-color: rgb(102, 83, 105);
border-color: rgb(255, 203, 32); border-color: rgb(102, 83, 105);
} }
.btn-outline-primary { .btn-outline-primary {
color: #000; color: rgb(102, 83, 105);
background-color: #ffcb20; background-color: rgba(248, 249, 250, 0.9);
border-color: #000; border-color: rgb(102, 83, 105);
} }
.turbolinks-progress-bar { .turbolinks-progress-bar {
background-color: #ffffff; background-color: #12432E;
} }
.btn-primary:hover, .btn-primary:hover,
.btn-primary:not(:disabled):not(.disabled).active, .btn-primary:not(:disabled):not(.disabled).active,
.btn-primary:not(:disabled):not(.disabled):active { .btn-primary:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0); color: #fff;
background-color: rgb(255, 0, 101); background-color: rgb(102, 83, 105);
border-color: rgb(255, 203, 32); border-color: rgb(102, 83, 105);
} }
.btn-primary { .btn-primary {
color: #ffcb20; color: rgba(248, 249, 250, 0.9);
background-color: #000000; background-color: rgb(102, 83, 105);
border-color: #ffcd20; border-color: rgb(102, 83, 105);
} }
.border-primary { .border-primary {
border-color: rgb(255, 255, 255) !important; border-color: rgb(115, 15, 115) !important;
}
.btn-secondary {
color: #ff0065;
background-color: #000000;
border-color: #ff0065;
}
.btn-secondary:hover,
.btn-secondary:not(:disabled):not(.disabled).active,
.btn-secondary:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0);
background-color: rgb(255, 203, 32);
border-color: rgb(255, 0, 101);
}
.btn-outline-dark-shiny {
background-color: #222;
border-color: #61605b;
color: rgba(255, 0, 101, 75%);
}
.btn-outline-dark-shiny:hover,
.btn-outline-dark-shiny:not(:disabled):not(.disabled).active,
.btn-outline-dark-shiny:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0);
background-color: rgb(255, 203, 32);
border-color: rgb(255, 0, 101);
}
.btn-outline-dark {
background-color: #222;
border-color: #61605b;
color: rgba(255, 203, 32, 75%);
}
.btn-outline-dark:hover,
.btn-outline-dark:not(:disabled):not(.disabled).active,
.btn-outline-dark:not(:disabled):not(.disabled):active {
color: rgb(0, 0, 0);
background-color: rgb(255, 0, 101);
border-color: rgb(255, 203, 32);
} }
a { a {
color: rgb(255, 0, 101); color: rgb(102, 83, 105);
} }
a:hover { a:hover {
color: rgb(255, 203, 32); color: rgb(200, 30, 200);
} }
.form-control:focus { .form-control:focus {
box-shadow: 0 0 0 0.25rem rgb(255 0 101 / 50%); box-shadow: 0 0 0 0.25rem rgba(200, 30, 200, 0.25);
border-color: rgb(255, 0, 101); border-color: rgb(200, 30, 200);
} }
.btn-outline-primary.focus { .btn-outline-primary.focus {
box-shadow: 0 0 0 0.25rem rgb(255 203 32 / 22%); box-shadow: 0 0 0 0.25rem rgba(200, 30, 200, 0.5);
}

View File

@ -96,11 +96,13 @@ function displayStyle (note) {
if (!note) { return '' } if (!note) { return '' }
const balance = note.balance const balance = note.balance
var css = '' var css = ''
var ms_per_year = 31536000000 // 365 * 24 * 3600 * 1000
if (balance < -2000) { css += ' text-danger bg-dark' } if (balance < -2000) { css += ' text-danger bg-dark' }
else if (balance < -1000) { css += ' text-danger' } else if (balance < -1000) { css += ' text-danger' }
else if (balance < 0) { css += ' text-warning' } else if (balance < 0) { css += ' text-warning' }
if (!note.email_confirmed) { css += ' bg-primary' } if (!note.email_confirmed) { css += ' bg-primary' }
else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString())) { css += ' bg-info' } else if (!note.is_active || (note.membership && note.membership.date_end < new Date().toISOString())) { css += ' bg-info' }
if (((Date.now() - Date.parse(note.created_at))/ms_per_year) > 2.5) { css += ' font-weight-bold underline' }
return css return css
} }

View File

@ -164,7 +164,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div> </div>
</nav> </nav>
<div class="{% block containertype %}container{% endblock %} my-3"> <div class="{% block containertype %}container{% endblock %} my-3">
<div id="messages"> <div id="messages">
{% if user.is_authenticated %}
<div class="alert alert-info">
Bravo pour votre diplomation les survis !
</div>
{% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if not user|is_member:"BDE" %} {% if not user|is_member:"BDE" %}
<div class="alert alert-danger"> <div class="alert alert-danger">