1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-30 13:11:09 +02:00

Compare commits

...

53 Commits

Author SHA1 Message Date
3f99fcd5d8 Merge branch 'nix-shell' into 'main'
Nix shell

See merge request bde/nk20!201
2023-07-06 23:22:16 +02:00
11223430fd Merge branch 'WEI2023' into 'main'
Préparation WEI 2023

See merge request bde/nk20!212
2023-07-04 19:17:17 +02:00
7aeb977e72 Oubli dans le fichier test_wei_registration_.py d'un 2022 en 2023 2023-07-04 18:33:54 +02:00
52fef1df42 Préparation WEI 2023 2023-07-04 18:23:43 +02:00
d5819ac562 Merge branch 'FAQ' into 'main'
Ajout d'un lien vers la FAQ de la note.

See merge request bde/nk20!209
2023-04-18 15:51:38 +02:00
a79df8f1f6 Merge branch 'invoice_bg_storlist' into 'main'
changement du fond des factures

See merge request bde/nk20!211
2023-04-14 19:29:26 +02:00
364b18e188 migrations 2023-04-14 16:52:46 +02:00
10a883b2e5 new treasury phone number 2023-04-14 16:00:48 +02:00
1410ab6c4f Almost on time, the SIRET number is now changed 2023-04-14 15:35:18 +02:00
623dd61be6 Remove phone number 2023-04-14 14:56:34 +02:00
48a0a87e7c changement du fond des factures 2023-04-14 00:25:26 +02:00
563f525b11 Merge branch 'cron' into 'main'
fréquence des mails de négatif aux trez : 1 mois -> 1 semaine, et les notes liées au BDE n'apparaissent plus

See merge request bde/nk20!210
2023-04-08 13:04:59 +02:00
63c1d74f1a Ignore notes containing '- BDE-' in the list of negative balances 2023-04-07 15:47:06 +02:00
c42fb380a6 frequence des mails de négatif aux trez : 1 mois -> 1 semiane 2023-04-06 09:04:27 +02:00
c636d52a73 traduction (allemand et espagnol probablement pas optimal) 2023-03-31 17:21:58 +02:00
6a9021ec14 Merge branch 'couleur_totalist_spies' into 'main'
Couleur totalist spies

See merge request bde/nk20!208
2023-03-31 12:37:24 +02:00
9c9149b53a Ajout d'un lien vers la FAQ de la note. 2023-03-31 12:34:14 +02:00
cb74311e7b Commit migration, j'étais triggered 2023-03-30 19:14:52 +02:00
9d7dd566c9 Ignore /tmp/ 2023-03-30 17:26:06 +02:00
9944ebcaad changement des couleurs de la note vers les couleurs totalist spies 2023-03-25 02:13:16 +01:00
8537f043f7 changement des couleurs de la note vers les couleurs totalist spies 2023-03-25 00:57:19 +01:00
c89a95f8d2 Merge branch 'invoice-logo-totalist' into 'main'
changement du fond des factures

See merge request bde/nk20!207
2023-01-30 13:06:39 +01:00
73640b1dfa changement du fond des factures 2023-01-30 00:06:45 +01:00
84b16ab603 Merge branch 'SogeCreditDate' into 'main'
link SogeCredit to WEI by creation date instead of civil year

See merge request bde/nk20!206
2023-01-17 15:58:52 +01:00
6a1b51dbbf Merge branch 'api_pagination' into 'main'
Add custom pagination size as an API parameter

See merge request bde/nk20!205
2023-01-11 22:46:13 +01:00
c441a43a8b link SogeCredit to WEI by creation date instead of civil year 2023-01-10 21:40:03 +01:00
87f3b51b04 Add custom pagination size as an API parameter 2022-12-14 18:37:13 +01:00
0a853fd3e6 Merge branch 'permission_trez' into 'main'
fix trez perm

See merge request bde/nk20!204
2022-12-10 14:41:57 +01:00
c429734810 fix bug 2022-11-12 14:51:22 +01:00
5d759111b6 Merge branch 'weiWords' into 'main'
change wei words

See merge request bde/nk20!203
2022-09-05 13:24:24 +02:00
70baf7566c change wei words 2022-09-05 13:20:00 +02:00
eb355f547c Merge branch 'SogeNotForMembership' into 'main'
Soge not for membership

See merge request bde/nk20!202
2022-09-04 22:56:07 +02:00
7068170f18 fixing grammar in comments 2022-09-04 13:24:39 +02:00
45ee9a8941 Soge only payd WEI (not bde/kfet membership) 2022-09-04 12:52:40 +02:00
454ea19603 hide Soge during registration 2022-09-04 12:31:08 +02:00
dde1baa25c typo 2022-08-21 19:50:53 +02:00
7a7ee47e0b Add two shell.nix to enable easier development on nixos. 2022-08-21 19:46:11 +02:00
5a77a66391 Merge branch 'beta' into 'main'
Friendships

See merge request bde/nk20!200
2022-04-13 12:45:06 +02:00
761fc170eb Update Spanish translation 2022-04-13 12:30:22 +02:00
ac23d7eb54 Generated translation files for de/es (but didn't translate anything) 2022-04-13 12:30:22 +02:00
40e7415062 Added translations for friendships 2022-04-13 12:30:22 +02:00
319405d2b1 Added a message to explain what frendships do
Signed-off-by: Nicolas Margulies <nicomarg@crans.org>
2022-04-13 12:30:22 +02:00
633ab88b04 Linting 2022-04-13 12:30:22 +02:00
e29b42eecc Add permissions related to trusting 2022-04-13 12:30:22 +02:00
dc69faaf1d Better user search to add friendships 2022-04-13 12:30:22 +02:00
442a5c5e36 First proro of trusting, with models and front, but no additional permissions 2022-04-13 12:30:22 +02:00
7ab0fec3bc Added trust model 2022-04-13 12:30:22 +02:00
bd4fb23351 Merge branch 'color_survi' into 'main'
switching to survivalist color

See merge request bde/nk20!199
2022-04-12 20:16:55 +02:00
ee22e9b3b6 fixing color to follow the proper theme 2022-04-12 18:33:22 +02:00
19ae616fb4 switching to survivalist color 2022-04-12 17:40:52 +02:00
b7657ec362 Merge branch 'color_ttlsp' into 'main'
Passage des couleur vers ttlsp

See merge request bde/nk20!197
2022-04-05 15:05:41 +02:00
4d03d9460d Passage des couleurs ttlsp 2022-04-05 14:45:41 +02:00
3633f66a87 Merge branch 'beta' into 'main'
Corrections de bugs

See merge request bde/nk20!195
2022-03-09 15:10:37 +01:00
44 changed files with 2621 additions and 886 deletions

2
.gitignore vendored
View File

@ -42,12 +42,12 @@ map.json
backups/
/static/
/media/
/tmp/
# Virtualenv
env/
venv/
db.sqlite3
shell.nix
# ansibles customs host
ansible/host_vars/*.yaml

5
apps/api/pagination.py Normal file
View File

@ -0,0 +1,5 @@
from rest_framework.pagination import PageNumberPagination
class CustomPagination(PageNumberPagination):
page_size_query_param = 'page_size'

View File

@ -0,0 +1,18 @@
# 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'),
),
]

View File

@ -0,0 +1,53 @@
/**
* On form submit, create a new friendship
*/
function create_trust (e) {
// Do not submit HTML form
e.preventDefault()
// Get data and send to API
const formData = new FormData(e.target)
$.getJSON('/api/note/alias/'+formData.get('trusted') + '/',
function (trusted_alias) {
if ((trusted_alias.note == formData.get('trusting')))
{
addMsg(gettext("You can't add yourself as a friend"), "danger")
return
}
$.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)
})
}
/**
* On click of "delete", delete the alias
* @param button_id:Integer Alias id to remove
*/
function delete_button (button_id) {
$.ajax({
url: '/api/note/trust/' + button_id + '/',
method: 'DELETE',
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
}).done(function () {
addMsg(gettext('Friendship successfully deleted'), 'success')
$('#trust_table').load(location.pathname + ' #trust_table')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
$(document).ready(function () {
// Attach event
document.getElementById('form_trust').addEventListener('submit', create_trust)
})

View File

@ -25,6 +25,14 @@
</a>
</dd>
<dt class="col-xl-6">{% trans 'friendships'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:user_trust' user_object.pk %}">
<i class="fa fa-edit"></i>
{% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
</a>
</dd>
{% if "member.view_profile"|has_perm:user_object.profile %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>

View File

@ -0,0 +1,41 @@
{% extends "member/base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load static django_tables2 i18n %}
{% block profile_content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Note friendships" %}
</h3>
<div class="card-body">
{% if can_create %}
<form class="input-group" method="POST" id="form_trust">
{% csrf_token %}
<input type="hidden" name="trusting" value="{{ object.note.pk }}">
{%include "autocomplete_model.html" %}
<div class="input-group-append">
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
</div>
</form>
{% endif %}
</div>
{% render_table trusting %}
</div>
<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
designed to simplify using note kfet transfers to transfer money between
users. The intent is that one person can make all transfers for a group of
friends without needing additional rights among them.
{% endblocktrans %}
</div>
{% endblock %}
{% block extrajavascript %}
<script src="{% static "member/js/trust.js" %}"></script>
<script src="{% static "js/autocomplete_model.js" %}"></script>
{% endblock%}

View File

@ -23,5 +23,6 @@ urlpatterns = [
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
]

View File

@ -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
@ -18,9 +19,9 @@ from django.views.generic import DetailView, UpdateView, TemplateView
from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from note.models import Alias, NoteUser, NoteClub
from note.models import Alias, NoteClub, NoteUser, Trust
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable
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
@ -243,6 +244,39 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return context
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user trust relationships
"""
model = User
template_name = 'member/profile_trust.html'
context_object_name = 'user_object'
extra_context = {"title": _("Note friendships")}
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
context["trusting"] = TrustTable(
note.trusting.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",
"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": ""
}
}
return context
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
View and manage user aliases.

View File

@ -12,7 +12,7 @@ from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from rest_framework.utils import model_meta
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
RecurrentTransaction, SpecialTransaction
@ -77,6 +77,22 @@ class NoteUserSerializer(serializers.ModelSerializer):
return str(obj)
class TrustSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Trusts.
The djangorestframework plugin will analyse the model `Trust` and parse all fields in the API.
"""
class Meta:
model = Trust
fields = '__all__'
def validate(self, attrs):
instance = Trust(**attrs)
instance.clean()
return attrs
class AliasSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Aliases.

View File

@ -2,7 +2,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, \
TrustViewSet
def register_note_urls(router, path):
@ -11,6 +12,7 @@ def register_note_urls(router, path):
"""
router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet)
router.register(path + '/trust', TrustViewSet)
router.register(path + '/consumer', ConsumerViewSet)
router.register(path + '/transaction/category', TemplateCategoryViewSet)

View File

@ -14,8 +14,9 @@ from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSe
from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \
TrustSerializer
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial, Trust
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
@ -56,11 +57,41 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
return queryset.order_by("id")
class TrustViewSet(ReadProtectedModelViewSet):
"""
REST Trust View set.
The djangorestframework plugin will get all `Trust` objects, serialize it to JSON with the given serializer,
then render it on /api/note/trust/
"""
queryset = Trust.objects
serializer_class = TrustSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
'$trusted__alias__name', '$trusted__alias__normalized_name']
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
ordering_fields = ['trusting', 'trusted', ]
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# trust relationship can't change people involved
serializer_class.Meta.read_only_fields = ('trusting', 'trusting',)
return serializer_class
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
try:
self.perform_destroy(instance)
except ValidationError as e:
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
class AliasViewSet(ReadProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
then render it on /api/aliases/
then render it on /api/note/aliases/
"""
queryset = Alias.objects
serializer_class = AliasSerializer

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.24 on 2021-09-05 19:16
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('note', '0005_auto_20210313_1235'),
]
operations = [
migrations.CreateModel(
name='Trust',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('trusted', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusted', to='note.Note', verbose_name='trusted')),
('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_plural': 'friendships',
'unique_together': {('trusting', 'trusted')},
},
),
]

View File

@ -1,13 +1,13 @@
# 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
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
from .transactions import MembershipTransaction, Transaction, \
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
__all__ = [
# Notes
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
'Alias', 'Trust', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
'RecurrentTransaction', 'SpecialTransaction',

View File

@ -217,6 +217,38 @@ class NoteSpecial(Note):
return self.special_type
class Trust(models.Model):
"""
A one-sided trust relationship bertween two users
If another user considers you as your friend, you can transfer money from
them
"""
trusting = models.ForeignKey(
Note,
on_delete=models.CASCADE,
related_name='trusting',
verbose_name=_('trusting')
)
trusted = models.ForeignKey(
Note,
on_delete=models.CASCADE,
related_name='trusted',
verbose_name=_('trusted')
)
class Meta:
verbose_name = _("frienship")
verbose_name_plural = _("friendships")
unique_together = ("trusting", "trusted")
def __str__(self):
return _("Friendship between {trusting} and {trusted}").format(
trusting=str(self.trusting), trusted=str(self.trusted))
class Alias(models.Model):
"""
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.

View File

@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from .models.notes import Alias
from .models.notes import Alias, Trust
from .models.transactions import Transaction, TransactionTemplate
from .templatetags.pretty_money import pretty_money
@ -148,6 +148,31 @@ DELETE_TEMPLATE = """
"""
class TrustTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped',
'id': "trust_table"
}
model = Trust
fields = ("trusted",)
template_name = 'django_tables2/bootstrap4.html'
show_header = False
trusted = tables.Column(attrs={'td': {'class': 'text_center'}})
delete_col = tables.TemplateColumn(
template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')},
attrs={
'td': {
'class': lambda record: 'col-sm-1'
+ (' d-none' if not PermissionBackend.check_perm(
get_current_request(), "note.delete_trust", record)
else '')}},
verbose_name=_("Delete"),)
class AliasTable(tables.Table):
class Meta:
attrs = {

View File

@ -1967,7 +1967,7 @@
"note",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]]",
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}]]",
"type": "change",
"mask": 2,
"field": "valid",
@ -2607,7 +2607,7 @@
"note",
"transaction"
],
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]",
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}]",
"type": "change",
"mask": 2,
"field": "valid",
@ -2623,7 +2623,7 @@
"note",
"transaction"
],
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}]",
"query": "[\"OR\", {\"source__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": false}, {\"destination__balance__gte\": {\"F\": [\"SUB\", [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]], 5000]}, \"valid\": true}]",
"type": "change",
"mask": 2,
"field": "invalidity_reason",
@ -2967,6 +2967,118 @@
"description": "Supprimer une application OAuth2"
}
},
{
"model": "permission.permission",
"pk": 190,
"fields": {
"model": [
"note",
"trust"
],
"query": "{\"trusting\": [\"user\", \"note\"]}",
"type": "delete",
"mask": 1,
"field": "",
"permanent": false,
"description": "Supprimer une amitié à sa note"
}
},
{
"model": "permission.permission",
"pk": 191,
"fields": {
"model": [
"note",
"trust"
],
"query": "{\"trusting\": [\"user\", \"note\"]}",
"type": "add",
"mask": 1,
"field": "",
"permanent": false,
"description": "Ajouter une amitié à sa note"
}
},
{
"model": "permission.permission",
"pk": 192,
"fields": {
"model": [
"note",
"trust"
],
"query": "{\"trusting__is_active\": true}",
"type": "add",
"mask": 1,
"field": "",
"permanent": false,
"description": "Ajouter une amitié à une note non bloquée"
}
},
{
"model": "permission.permission",
"pk": 193,
"fields": {
"model": [
"note",
"trust"
],
"query": "{\"trusting__is_active\": true}",
"type": "delete",
"mask": 3,
"field": "",
"permanent": false,
"description": "Supprimer une amitié à une note non bloquée"
}
},
{
"model": "permission.permission",
"pk": 194,
"fields": {
"model": [
"note",
"trust"
],
"query": "{}",
"type": "view",
"mask": 3,
"field": "",
"permanent": false,
"description": "Voir toutes les amitiés, y compris celles des non adhérents"
}
},
{
"model": "permission.permission",
"pk": 195,
"fields": {
"model": [
"note",
"trust"
],
"query": "{\"trusting__noteuser__user\": [\"user\"]}",
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"description": "Voir ses propres amitiés, pour toujours"
}
},
{
"model": "permission.permission",
"pk": 196,
"fields": {
"model": [
"note",
"transaction"
],
"query": "[\"AND\", {\"source__trusting__trusted\": [\"user\", \"note\"]}, [\"OR\", {\"source__balance__gte\": {\"F\": [\"MUL\", [\"F\", \"amount\"], [\"F\", \"quantity\"]]}}, {\"valid\": false}]]",
"type": "add",
"mask": 1,
"field": "",
"permanent": false,
"description": "Transférer de l'argent depuis une note amie en restant positif"
}
},
{
"model": "permission.role",
"pk": 1,
@ -3001,7 +3113,11 @@
186,
187,
188,
189
189,
190,
191,
195,
196
]
}
},
@ -3042,7 +3158,9 @@
158,
159,
160,
179
179,
189,
190
]
}
},
@ -3192,7 +3310,10 @@
176,
177,
178,
183
188,
183,
186,
187
]
}
},
@ -3386,7 +3507,14 @@
186,
187,
188,
189
189,
190,
191,
192,
193,
194,
195,
196
]
}
},

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2023-01-29 22:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0004_auto_20211005_1544'),
]
operations = [
migrations.AlterField(
model_name='invoice',
name='bde',
field=models.CharField(choices=[('TotalistSpies', 'Tota[list]Spies'), ('Saperlistpopette', 'Saper[list]popette'), ('Finalist', 'Fina[list]'), ('Listorique', '[List]orique'), ('Satellist', 'Satel[list]'), ('Monopolist', 'Monopo[list]'), ('Kataclist', 'Katac[list]')], default='TotalistSpies', max_length=32, verbose_name='BDE'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2023-04-14 14:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0005_auto_20230129_2348'),
]
operations = [
migrations.AlterField(
model_name='invoice',
name='bde',
field=models.CharField(choices=[('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='SecretStorlist', max_length=32, verbose_name='BDE'),
),
]

View File

@ -28,8 +28,10 @@ class Invoice(models.Model):
bde = models.CharField(
max_length=32,
default='Saperlistpopette',
default='SecretStorlist',
choices=(
('SecretStorlist', 'SecretStor[list]'),
('TotalistSpies', 'Tota[list]Spies'),
('Saperlistpopette', 'Saper[list]popette'),
('Finalist', 'Fina[list]'),
('Listorique', '[List]orique'),
@ -95,7 +97,7 @@ class Invoice(models.Model):
products = self.products.all()
self.place = "Gif-sur-Yvette"
self.my_name = "BDE ENS Cachan"
self.my_name = "BDE ENS Paris Saclay"
self.my_address_street = "4 avenue des Sciences"
self.my_city = "91190 Gif-sur-Yvette"
self.bank_code = 30003
@ -310,8 +312,8 @@ class SogeCredit(models.Model):
amount = sum(transaction.total for transaction in self.transactions.all())
if 'wei' in settings.INSTALLED_APPS:
from wei.models import WEIMembership
if not WEIMembership.objects.filter(club__weiclub__year=datetime.date.today().year, user=self.user)\
.exists():
if not WEIMembership.objects\
.filter(club__weiclub__year=self.credit_transaction.created_at.year, user=self.user).exists():
# 80 € for people that don't go to WEI
amount += 8000
return amount
@ -329,17 +331,18 @@ class SogeCredit(models.Model):
bde_qs = Membership.objects.filter(user=self.user, club=bde, date_start__gte=bde.membership_start)
kfet_qs = Membership.objects.filter(user=self.user, club=kfet, date_start__gte=kfet.membership_start)
if bde_qs.exists():
m = bde_qs.get()
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction)
if kfet_qs.exists():
m = kfet_qs.get()
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
if m.transaction not in self.transactions.all():
self.transactions.add(m.transaction)
## Soge do not pay BDE and kfet memberships this year (2022-2023)
# if bde_qs.exists():
# m = bde_qs.get()
# if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
# if m.transaction not in self.transactions.all():
# self.transactions.add(m.transaction)
#
# if kfet_qs.exists():
# m = kfet_qs.get()
# if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
# if m.transaction not in self.transactions.all():
# self.transactions.add(m.transaction)
if 'wei' in settings.INSTALLED_APPS:
from wei.models import WEIClub

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -105,8 +105,8 @@
\renewcommand{\headrulewidth}{0pt}
\cfoot{
\small{\MonNom ~--~ \MonAdresseRue ~ \MonAdresseVille ~--~ Téléphone : +33(0)6 89 88 56 50\newline
Site web : bde.ens-cachan.fr ~--~ E-mail : tresorerie.bde@lists.crans.org \newline Numéro SIRET : 399 485 838 00011
\small{\MonNom ~--~ \MonAdresseRue ~ \MonAdresseVille ~--~ Téléphone : +33(0)7 78 17 22 34\newline
Site web : bde.ens-cachan.fr ~--~ E-mail : tresorerie.bde@lists.crans.org \newline Numéro SIRET : 399 485 838 00029
}
}

View File

@ -2,11 +2,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
from .wei2022 import WEISurvey2022
from .wei2023 import WEISurvey2023
__all__ = [
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
]
CurrentSurvey = WEISurvey2022
CurrentSurvey = WEISurvey2023

View File

@ -14,14 +14,17 @@ from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInf
from ...models import WEIMembership
WORDS = [
'13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant',
'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill',
'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial',
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit',
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic',
'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi',
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
'ABBA', 'After', 'Alcoolique anonyme', 'Ambiance festive', 'Années 2000', 'Apéro', 'Art',
'Baby foot billard biere pong', 'BBQ', 'Before', 'Bière pong', 'Bon enfant', 'Calme', 'Canapé',
'Chanson paillarde', 'Chanson populaire', 'Chartreuse', 'Cheerleader', 'Chill', 'Choré',
'Cinéma', 'Cocktail', 'Comédie musicle', 'Commercial', 'Copaing', 'Danse', 'Dancefloor',
'Electro', 'Fanfare', 'Gin tonic', 'Inclusif', 'Jazz', "Jeux d'alcool", 'Jeux de carte',
'Jeux de rôle', 'Jeux de société', 'JUL', 'Jus de fruit', 'Kfet', 'Kleptomanie assurée',
'LGBTQ+', 'Livre', 'Morning beer', 'Musique', 'NAPS', 'Paillettes', 'Pastis', 'Paté Hénaff',
'Peluche', 'Pena baiona', "Peu d'alcool", 'Pilier de bar', 'PMU', 'Poulpe', 'Punch', 'Rap',
'Réveil', 'Rock', 'Rugby', 'Sandwich', 'Serge', 'Shot', 'Sociable', 'Spectacle', 'Techno',
'Techno house', 'Thérapie Taxi', 'Tradition kchanaises', 'Troisième mi-temps', 'Turn up',
'Vodka', 'Vodka pomme', 'Volley', 'Vomi stratégique'
]

View File

@ -0,0 +1,296 @@
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import time
from functools import lru_cache
from random import Random
from django import forms
from django.db import transaction
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
from ...models import WEIMembership
WORDS = [
'ABBA', 'After', 'Alcoolique anonyme', 'Ambiance festive', 'Années 2000', 'Apéro', 'Art',
'Baby foot billard biere pong', 'BBQ', 'Before', 'Bière pong', 'Bon enfant', 'Calme', 'Canapé',
'Chanson paillarde', 'Chanson populaire', 'Chartreuse', 'Cheerleader', 'Chill', 'Choré',
'Cinéma', 'Cocktail', 'Comédie musicle', 'Commercial', 'Copaing', 'Danse', 'Dancefloor',
'Electro', 'Fanfare', 'Gin tonic', 'Inclusif', 'Jazz', "Jeux d'alcool", 'Jeux de carte',
'Jeux de rôle', 'Jeux de société', 'JUL', 'Jus de fruit', 'Kfet', 'Kleptomanie assurée',
'LGBTQ+', 'Livre', 'Morning beer', 'Musique', 'NAPS', 'Paillettes', 'Pastis', 'Paté Hénaff',
'Peluche', 'Pena baiona', "Peu d'alcool", 'Pilier de bar', 'PMU', 'Poulpe', 'Punch', 'Rap',
'Réveil', 'Rock', 'Rugby', 'Sandwich', 'Serge', 'Shot', 'Sociable', 'Spectacle', 'Techno',
'Techno house', 'Thérapie Taxi', 'Tradition kchanaises', 'Troisième mi-temps', 'Turn up',
'Vodka', 'Vodka pomme', 'Volley', 'Vomi stratégique'
]
class WEISurveyForm2023(forms.Form):
"""
Survey form for the year 2023.
Members choose 20 words, from which we calculate the best associated bus.
"""
word = forms.ChoiceField(
label=_("Choose a word:"),
widget=forms.RadioSelect(),
)
def set_registration(self, registration):
"""
Filter the bus selector with the buses of the current WEI.
"""
information = WEISurveyInformation2023(registration)
if not information.seed:
information.seed = int(1000 * time.time())
information.save(registration)
registration._force_save = True
registration.save()
if self.data:
self.fields["word"].choices = [(w, w) for w in WORDS]
if self.is_valid():
return
rng = Random((information.step + 1) * information.seed)
words = None
buses = WEISurveyAlgorithm2023.get_buses()
informations = {bus: WEIBusInformation2023(bus) for bus in buses}
scores = sum((list(informations[bus].scores.values()) for bus in buses), [])
average_score = sum(scores) / len(scores)
preferred_words = {bus: [word for word in WORDS
if informations[bus].scores[word] >= average_score]
for bus in buses}
while words is None or len(set(words)) != len(words):
# Ensure that there is no the same word 2 times
words = [rng.choice(words) for _ignored2, words in preferred_words.items()]
rng.shuffle(words)
words = [(w, w) for w in words]
self.fields["word"].choices = words
class WEIBusInformation2023(WEIBusInformation):
"""
For each word, the bus has a score
"""
scores: dict
def __init__(self, bus):
self.scores = {}
for word in WORDS:
self.scores[word] = 0.0
super().__init__(bus)
class WEISurveyInformation2023(WEISurveyInformation):
"""
We store the id of the selected bus. We store only the name, but is not used in the selection:
that's only for humans that try to read data.
"""
# Random seed that is stored at the first time to ensure that words are generated only once
seed = 0
step = 0
def __init__(self, registration):
for i in range(1, 21):
setattr(self, "word" + str(i), None)
super().__init__(registration)
class WEISurvey2023(WEISurvey):
"""
Survey for the year 2023.
"""
@classmethod
def get_year(cls):
return 2023
@classmethod
def get_survey_information_class(cls):
return WEISurveyInformation2023
def get_form_class(self):
return WEISurveyForm2023
def update_form(self, form):
"""
Filter the bus selector with the buses of the WEI.
"""
form.set_registration(self.registration)
@transaction.atomic
def form_valid(self, form):
word = form.cleaned_data["word"]
self.information.step += 1
setattr(self.information, "word" + str(self.information.step), word)
self.save()
@classmethod
def get_algorithm_class(cls):
return WEISurveyAlgorithm2023
def is_complete(self) -> bool:
"""
The survey is complete once the bus is chosen.
"""
return self.information.step == 20
@classmethod
@lru_cache()
def word_mean(cls, word):
"""
Calculate the mid-score given by all buses.
"""
buses = cls.get_algorithm_class().get_buses()
return sum([cls.get_algorithm_class().get_bus_information(bus).scores[word] for bus in buses]) / buses.count()
@lru_cache()
def score(self, bus):
if not self.is_complete():
raise ValueError("Survey is not ended, can't calculate score")
bus_info = self.get_algorithm_class().get_bus_information(bus)
# Score is the given score by the bus subtracted to the mid-score of the buses.
s = sum(bus_info.scores[getattr(self.information, 'word' + str(i))]
- self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 21)) / 20
return s
@lru_cache()
def scores_per_bus(self):
return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()}
@lru_cache()
def ordered_buses(self):
values = list(self.scores_per_bus().items())
values.sort(key=lambda item: -item[1])
return values
@classmethod
def clear_cache(cls):
cls.word_mean.cache_clear()
return super().clear_cache()
class WEISurveyAlgorithm2023(WEISurveyAlgorithm):
"""
The algorithm class for the year 2023.
We use Gale-Shapley algorithm to attribute 1y students into buses.
"""
@classmethod
def get_survey_class(cls):
return WEISurvey2023
@classmethod
def get_bus_information_class(cls):
return WEIBusInformation2023
def run_algorithm(self, display_tqdm=False):
"""
Gale-Shapley algorithm implementation.
We modify it to allow buses to have multiple "weddings".
"""
surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys
surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys
# Don't manage hardcoded people
surveys = [s for s in surveys if not hasattr(s.information, 'hardcoded') or not s.information.hardcoded]
# Reset previous algorithm run
for survey in surveys:
survey.free()
survey.save()
non_men = [s for s in surveys if s.registration.gender != 'male']
men = [s for s in surveys if s.registration.gender == 'male']
quotas = {}
registrations = self.get_registrations()
non_men_total = registrations.filter(~Q(gender='male')).count()
for bus in self.get_buses():
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
# Remove hardcoded people
free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
registration__information_json__icontains="hardcoded").count()
quotas[bus] = 4 + int(non_men_total / registrations.count() * free_seats)
tqdm_obj = None
if display_tqdm:
from tqdm import tqdm
tqdm_obj = tqdm(total=len(non_men), desc="Non-hommes")
# Repartition for non men people first
self.make_repartition(non_men, quotas, tqdm_obj=tqdm_obj)
quotas = {}
for bus in self.get_buses():
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
# Remove hardcoded people
free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
registration__information_json__icontains="hardcoded").count()
quotas[bus] = free_seats
if display_tqdm:
tqdm_obj.close()
from tqdm import tqdm
tqdm_obj = tqdm(total=len(men), desc="Hommes")
self.make_repartition(men, quotas, tqdm_obj=tqdm_obj)
if display_tqdm:
tqdm_obj.close()
# Clear cache information after running algorithm
WEISurvey2023.clear_cache()
def make_repartition(self, surveys, quotas=None, tqdm_obj=None):
free_surveys = surveys.copy() # Remaining surveys
while free_surveys: # Some students are not affected
survey = free_surveys[0]
buses = survey.ordered_buses() # Preferences of the student
for bus, current_score in buses:
if self.get_bus_information(bus).has_free_seats(surveys, quotas):
# Selected bus has free places. Put student in the bus
survey.select_bus(bus)
survey.save()
free_surveys.remove(survey)
break
else:
# Current bus has not enough places. Remove the least preferred student from the bus if existing
least_preferred_survey = None
least_score = -1
# Find the least student in the bus that has a lower score than the current student
for survey2 in surveys:
if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
continue
score2 = survey2.score(bus)
if current_score <= score2: # Ignore better students
continue
if least_preferred_survey is None or score2 < least_score:
least_preferred_survey = survey2
least_score = score2
if least_preferred_survey is not None:
# Remove the least student from the bus and put the current student in.
# If it does not exist, choose the next bus.
least_preferred_survey.free()
least_preferred_survey.save()
free_surveys.append(least_preferred_survey)
survey.select_bus(bus)
survey.save()
free_surveys.remove(survey)
break
else:
raise ValueError(f"User {survey.registration.user} has no free seat")
if tqdm_obj is not None:
tqdm_obj.n = len(surveys) - len(free_surveys)
tqdm_obj.refresh()

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.26 on 2022-09-04 21:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0003_bus_size'),
]
operations = [
migrations.AlterField(
model_name='weiclub',
name='year',
field=models.PositiveIntegerField(default=2022, unique=True, verbose_name='year'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2023-01-28 17:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0004_auto_20220904_2325'),
]
operations = [
migrations.AlterField(
model_name='weiclub',
name='year',
field=models.PositiveIntegerField(default=2023, unique=True, verbose_name='year'),
),
]

View File

@ -0,0 +1,110 @@
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import random
from django.contrib.auth.models import User
from django.test import TestCase
from ..forms.surveys.wei2023 import WEIBusInformation2023, WEISurvey2023, WORDS, WEISurveyInformation2023
from ..models import Bus, WEIClub, WEIRegistration
class TestWEIAlgorithm(TestCase):
"""
Run some tests to ensure that the WEI algorithm is working well.
"""
fixtures = ('initial',)
def setUp(self):
"""
Create some test data, with one WEI and 10 buses with random score attributions.
"""
self.wei = WEIClub.objects.create(
name="WEI 2023",
email="wei2023@example.com",
date_start='2023-09-16',
date_end='2023-09-18',
year=2023,
)
self.buses = []
for i in range(10):
bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10)
self.buses.append(bus)
information = WEIBusInformation2023(bus)
for word in WORDS:
information.scores[word] = random.randint(0, 101)
information.save()
bus.save()
def test_survey_algorithm_small(self):
"""
There are only a few people in each bus, ensure that each person has its best bus
"""
# Add a few users
for i in range(10):
user = User.objects.create(username=f"user{i}")
registration = WEIRegistration.objects.create(
user=user,
wei=self.wei,
first_year=True,
birth_date='2000-01-01',
)
information = WEISurveyInformation2023(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2023.get_algorithm_class()().run_algorithm()
# Ensure that everyone has its first choice
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2023(r)
preferred_bus = survey.ordered_buses()[0][0]
chosen_bus = survey.information.get_selected_bus()
self.assertEqual(preferred_bus, chosen_bus)
def test_survey_algorithm_full(self):
"""
Buses are full of first year people, ensure that they are happy
"""
# Add a lot of users
for i in range(95):
user = User.objects.create(username=f"user{i}")
registration = WEIRegistration.objects.create(
user=user,
wei=self.wei,
first_year=True,
birth_date='2000-01-01',
)
information = WEISurveyInformation2023(registration)
for j in range(1, 21):
setattr(information, f'word{j}', random.choice(WORDS))
information.step = 20
information.save(registration)
registration.save()
# Run algorithm
WEISurvey2023.get_algorithm_class()().run_algorithm()
penalty = 0
# Ensure that everyone seems to be happy
# We attribute a penalty for each user that didn't have its first choice
# The penalty is the square of the distance between the score of the preferred bus
# and the score of the attributed bus
# We consider it acceptable if the mean of this distance is lower than 5 %
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2023(r)
chosen_bus = survey.information.get_selected_bus()
buses = survey.ordered_buses()
score = min(v for bus, v in buses if bus == chosen_bus)
max_score = buses[0][1]
penalty += (max_score - score) ** 2
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %

View File

@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import subprocess
@ -782,7 +782,7 @@ class TestDefaultWEISurvey(TestCase):
WEISurvey.update_form(None, None)
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
self.assertEqual(CurrentSurvey.get_year(), 2022)
self.assertEqual(CurrentSurvey.get_year(), 2023)
class TestWeiAPI(TestAPI):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,16 +7,16 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-07 22:55+0200\n"
"PO-Revision-Date: 2020-11-16 20:02+0000\n"
"Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n"
"POT-Creation-Date: 2023-03-31 17:08+0200\n"
"PO-Revision-Date: 2022-04-11 22:05+0200\n"
"Last-Translator: bleizi <bleizi@crans.org>\n"
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 4.3.2\n"
"X-Generator: Poedit 3.0\n"
#: apps/activity/apps.py:10 apps/activity/models.py:151
#: apps/activity/models.py:167
@ -56,7 +56,7 @@ msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
#: apps/member/models.py:199
#: apps/member/templates/member/includes/club_info.html:4
#: apps/member/templates/member/includes/profile_info.html:4
#: apps/note/models/notes.py:231 apps/note/models/transactions.py:26
#: apps/note/models/notes.py:263 apps/note/models/transactions.py:26
#: apps/note/models/transactions.py:46 apps/note/models/transactions.py:301
#: apps/permission/models.py:330
#: apps/registration/templates/registration/future_profile_detail.html:16
@ -114,8 +114,8 @@ msgstr "Lieu où l'activité est organisée, par exemple la Kfet."
msgid "type"
msgstr "type"
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:305
#: apps/note/models/notes.py:148 apps/treasury/models.py:285
#: apps/activity/models.py:89 apps/logs/models.py:22 apps/member/models.py:307
#: apps/note/models/notes.py:148 apps/treasury/models.py:286
#: apps/wei/models.py:173 apps/wei/templates/wei/attribute_bus_1A.html:13
#: apps/wei/templates/wei/survey.html:15
msgid "user"
@ -258,7 +258,7 @@ msgstr "Entré le "
msgid "remove"
msgstr "supprimer"
#: apps/activity/tables.py:82 apps/note/forms.py:68 apps/treasury/models.py:199
#: apps/activity/tables.py:82 apps/note/forms.py:68 apps/treasury/models.py:200
msgid "Type"
msgstr "Type"
@ -295,7 +295,7 @@ msgstr "Invité supprimé"
#: apps/note/models/transactions.py:257
#: apps/note/templates/note/transaction_form.html:17
#: apps/note/templates/note/transaction_form.html:152
#: note_kfet/templates/base.html:73
#: note_kfet/templates/base.html:72
msgid "Transfer"
msgstr "Virement"
@ -388,7 +388,7 @@ msgid "validate"
msgstr "valider"
#: apps/activity/templates/activity/includes/activity_info.html:71
#: apps/logs/models.py:64 apps/note/tables.py:195
#: apps/logs/models.py:64 apps/note/tables.py:220
msgid "edit"
msgstr "modifier"
@ -400,7 +400,7 @@ msgstr "Inviter"
msgid "Create new activity"
msgstr "Créer une nouvelle activité"
#: apps/activity/views.py:67 note_kfet/templates/base.html:91
#: apps/activity/views.py:67 note_kfet/templates/base.html:90
msgid "Activities"
msgstr "Activités"
@ -466,9 +466,9 @@ msgstr "nouvelles données"
msgid "create"
msgstr "créer"
#: apps/logs/models.py:65 apps/note/tables.py:165 apps/note/tables.py:211
#: apps/permission/models.py:127 apps/treasury/tables.py:38
#: apps/wei/tables.py:74
#: apps/logs/models.py:65 apps/note/tables.py:166 apps/note/tables.py:190
#: apps/note/tables.py:237 apps/permission/models.py:127
#: apps/treasury/tables.py:38 apps/wei/tables.py:74
msgid "delete"
msgstr "supprimer"
@ -507,11 +507,11 @@ msgstr "cotisation pour adhérer (normalien élève)"
msgid "membership fee (unpaid students)"
msgstr "cotisation pour adhérer (normalien étudiant)"
#: apps/member/admin.py:65 apps/member/models.py:317
#: apps/member/admin.py:65 apps/member/models.py:319
msgid "roles"
msgstr "rôles"
#: apps/member/admin.py:66 apps/member/models.py:331
#: apps/member/admin.py:66 apps/member/models.py:333
msgid "fee"
msgstr "cotisation"
@ -547,7 +547,7 @@ msgstr "Taille maximale : 2 Mo"
msgid "This image cannot be loaded."
msgstr "Cette image ne peut pas être chargée."
#: apps/member/forms.py:141 apps/member/views.py:102
#: apps/member/forms.py:141 apps/member/views.py:103
#: apps/registration/forms.py:33 apps/registration/views.py:262
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
@ -610,14 +610,14 @@ msgid "hash"
msgstr "haché"
#: apps/member/models.py:38
#: apps/member/templates/member/includes/profile_info.html:35
#: apps/member/templates/member/includes/profile_info.html:43
#: apps/registration/templates/registration/future_profile_detail.html:40
#: apps/wei/templates/wei/weimembership_form.html:44
msgid "phone number"
msgstr "numéro de téléphone"
#: apps/member/models.py:45
#: apps/member/templates/member/includes/profile_info.html:29
#: apps/member/templates/member/includes/profile_info.html:37
#: apps/registration/templates/registration/future_profile_detail.html:34
#: apps/wei/templates/wei/weimembership_form.html:38
msgid "section"
@ -705,14 +705,14 @@ msgid "Year of entry to the school (None if not ENS student)"
msgstr "Année d'entrée dans l'école (None si non-étudiant·e de l'ENS)"
#: apps/member/models.py:83
#: apps/member/templates/member/includes/profile_info.html:39
#: apps/member/templates/member/includes/profile_info.html:47
#: apps/registration/templates/registration/future_profile_detail.html:37
#: apps/wei/templates/wei/weimembership_form.html:41
msgid "address"
msgstr "adresse"
#: apps/member/models.py:90
#: apps/member/templates/member/includes/profile_info.html:42
#: apps/member/templates/member/includes/profile_info.html:50
#: apps/registration/templates/registration/future_profile_detail.html:43
#: apps/wei/templates/wei/weimembership_form.html:47
msgid "paid"
@ -784,7 +784,7 @@ msgstr "Activez votre compte Note Kfet"
#: apps/member/models.py:204
#: apps/member/templates/member/includes/club_info.html:55
#: apps/member/templates/member/includes/profile_info.html:32
#: apps/member/templates/member/includes/profile_info.html:40
#: apps/registration/templates/registration/future_profile_detail.html:22
#: apps/wei/templates/wei/base.html:70
#: apps/wei/templates/wei/weimembership_form.html:20
@ -833,46 +833,46 @@ msgstr ""
"Date maximale d'une fin d'adhésion, après laquelle les adhérents doivent la "
"renouveler."
#: apps/member/models.py:286 apps/member/models.py:311
#: apps/member/models.py:288 apps/member/models.py:313
#: apps/note/models/notes.py:176
msgid "club"
msgstr "club"
#: apps/member/models.py:287
#: apps/member/models.py:289
msgid "clubs"
msgstr "clubs"
#: apps/member/models.py:322
#: apps/member/models.py:324
msgid "membership starts on"
msgstr "l'adhésion commence le"
#: apps/member/models.py:326
#: apps/member/models.py:328
msgid "membership ends on"
msgstr "l'adhésion finit le"
#: apps/member/models.py:428
#: apps/member/models.py:430
#, python-brace-format
msgid "The role {role} does not apply to the club {club}."
msgstr "Le rôle {role} ne s'applique pas au club {club}."
#: apps/member/models.py:437 apps/member/views.py:651
#: apps/member/models.py:439 apps/member/views.py:712
msgid "User is already a member of the club"
msgstr "L'utilisateur est déjà membre du club"
#: apps/member/models.py:449 apps/member/views.py:660
#: apps/member/models.py:451 apps/member/views.py:721
msgid "User is not a member of the parent club"
msgstr "L'utilisateur n'est pas membre du club parent"
#: apps/member/models.py:502
#: apps/member/models.py:504
#, python-brace-format
msgid "Membership of {user} for the club {club}"
msgstr "Adhésion de {user} pour le club {club}"
#: apps/member/models.py:505 apps/note/models/transactions.py:389
#: apps/member/models.py:507 apps/note/models/transactions.py:389
msgid "membership"
msgstr "adhésion"
#: apps/member/models.py:506
#: apps/member/models.py:508
msgid "memberships"
msgstr "adhésions"
@ -924,7 +924,7 @@ msgid "Account #"
msgstr "Compte n°"
#: apps/member/templates/member/base.html:48
#: apps/member/templates/member/base.html:62 apps/member/views.py:59
#: apps/member/templates/member/base.html:62 apps/member/views.py:60
#: apps/registration/templates/registration/future_profile_detail.html:48
#: apps/wei/templates/wei/weimembership_form.html:117
msgid "Update Profile"
@ -985,13 +985,14 @@ msgstr ""
"seront à nouveau possible."
#: apps/member/templates/member/club_alias.html:10
#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:253
#: apps/member/views.py:456
#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:287
#: apps/member/views.py:517
msgid "Note aliases"
msgstr "Alias de la note"
#: apps/member/templates/member/club_alias.html:20
#: apps/member/templates/member/profile_alias.html:19
#: apps/member/templates/member/profile_trust.html:19
#: apps/treasury/tables.py:99
#: apps/treasury/templates/treasury/sogecredit_list.html:34
#: apps/treasury/templates/treasury/sogecredit_list.html:73
@ -1044,7 +1045,7 @@ msgid "membership fee"
msgstr "cotisation pour adhérer"
#: apps/member/templates/member/includes/club_info.html:43
#: apps/member/templates/member/includes/profile_info.html:47
#: apps/member/templates/member/includes/profile_info.html:55
#: apps/treasury/templates/treasury/sogecredit_detail.html:24
#: apps/wei/templates/wei/base.html:60
msgid "balance"
@ -1052,7 +1053,7 @@ msgstr "solde du compte"
#: apps/member/templates/member/includes/club_info.html:47
#: apps/member/templates/member/includes/profile_info.html:20
#: apps/note/models/notes.py:255 apps/wei/templates/wei/base.html:66
#: apps/note/models/notes.py:287 apps/wei/templates/wei/base.html:66
msgid "aliases"
msgstr "alias"
@ -1076,7 +1077,16 @@ msgstr "mot de passe"
msgid "Change password"
msgstr "Changer le mot de passe"
#: apps/member/templates/member/includes/profile_info.html:55
#: apps/member/templates/member/includes/profile_info.html:28
#: apps/note/models/notes.py:244
msgid "friendships"
msgstr "amitiés"
#: apps/member/templates/member/includes/profile_info.html:32
msgid "Manage friendships"
msgstr "Gérer les amitiés"
#: apps/member/templates/member/includes/profile_info.html:63
msgid "API token"
msgstr "Accès API"
@ -1148,6 +1158,23 @@ msgstr "Cliquez ici pour renvoyer un lien de validation."
msgid "View my memberships"
msgstr "Voir mes adhésions"
#: apps/member/templates/member/profile_trust.html:10 apps/member/views.py:254
msgid "Note friendships"
msgstr "Amitiés note"
#: apps/member/templates/member/profile_trust.html:28
msgid ""
"Adding someone as a friend enables them to initiate transactions coming from "
"your account (while keeping your balance positive). This is designed to "
"simplify using note kfet transfers to transfer money between users. The "
"intent is that one person can make all transfers for a group of friends "
"without needing additional rights among them."
msgstr ""
"Ajouter quelqu'un⋅e en ami⋅e lui permet de me prélever de l'argent (tant que "
"ma note reste positive). Ceci sert à simplifier les remboursements entre "
"ami⋅es via note. En effet, une personne peut effectuer tous les transferts "
"sans posséder de droits supplémentaires."
#: apps/member/templates/member/profile_update.html:18
msgid "Save Changes"
msgstr "Sauvegarder les changements"
@ -1156,47 +1183,47 @@ msgstr "Sauvegarder les changements"
msgid "Registrations"
msgstr "Inscriptions"
#: apps/member/views.py:72 apps/registration/forms.py:23
#: apps/member/views.py:73 apps/registration/forms.py:23
msgid "This address must be valid."
msgstr "Cette adresse doit être valide."
#: apps/member/views.py:139
#: apps/member/views.py:140
msgid "Profile detail"
msgstr "Détails de l'utilisateur"
#: apps/member/views.py:205
#: apps/member/views.py:206
msgid "Search user"
msgstr "Chercher un utilisateur"
#: apps/member/views.py:273
#: apps/member/views.py:308
msgid "Update note picture"
msgstr "Modifier la photo de la note"
#: apps/member/views.py:319
#: apps/member/views.py:354
msgid "Manage auth token"
msgstr "Gérer les jetons d'authentification"
#: apps/member/views.py:346
#: apps/member/views.py:381
msgid "Create new club"
msgstr "Créer un nouveau club"
#: apps/member/views.py:365
#: apps/member/views.py:400
msgid "Search club"
msgstr "Chercher un club"
#: apps/member/views.py:398
#: apps/member/views.py:433
msgid "Club detail"
msgstr "Détails du club"
#: apps/member/views.py:479
#: apps/member/views.py:540
msgid "Update club"
msgstr "Modifier le club"
#: apps/member/views.py:513
#: apps/member/views.py:574
msgid "Add new member to the club"
msgstr "Ajouter un nouveau membre au club"
#: apps/member/views.py:642 apps/wei/views.py:973
#: apps/member/views.py:703 apps/wei/views.py:973
msgid ""
"This user don't have enough money to join this club, and can't have a "
"negative balance."
@ -1204,19 +1231,19 @@ msgstr ""
"Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas "
"avoir un solde négatif."
#: apps/member/views.py:664
#: apps/member/views.py:725
msgid "The membership must start after {:%m-%d-%Y}."
msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}."
#: apps/member/views.py:669
#: apps/member/views.py:730
msgid "The membership must begin before {:%m-%d-%Y}."
msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}."
#: apps/member/views.py:815
#: apps/member/views.py:876
msgid "Manage roles of an user in the club"
msgstr "Gérer les rôles d'un utilisateur dans le club"
#: apps/member/views.py:840
#: apps/member/views.py:901
msgid "Members of the club"
msgstr "Membres du club"
@ -1234,7 +1261,7 @@ msgstr "destination"
msgid "amount"
msgstr "montant"
#: apps/note/api/serializers.py:183 apps/note/api/serializers.py:189
#: apps/note/api/serializers.py:199 apps/note/api/serializers.py:205
#: apps/note/models/transactions.py:228
msgid ""
"The transaction can't be saved since the source note or the destination note "
@ -1366,30 +1393,47 @@ msgstr "note spéciale"
msgid "special notes"
msgstr "notes spéciales"
#: apps/note/models/notes.py:237
#: apps/note/models/notes.py:232
msgid "trusting"
msgstr "note"
#: apps/note/models/notes.py:239
msgid "trusted"
msgstr "ami"
#: apps/note/models/notes.py:243
msgid "frienship"
msgstr "amitié"
#: apps/note/models/notes.py:248
#, python-brace-format
msgid "Friendship between {trusting} and {trusted}"
msgstr "Amitié entre {trusting} et {trusted}"
#: apps/note/models/notes.py:269
msgid "Invalid alias"
msgstr "Alias invalide"
#: apps/note/models/notes.py:254
#: apps/note/models/notes.py:286
msgid "alias"
msgstr "alias"
#: apps/note/models/notes.py:278
#: apps/note/models/notes.py:310
msgid "Alias is too long."
msgstr "L'alias est trop long."
#: apps/note/models/notes.py:281
#: apps/note/models/notes.py:313
msgid ""
"This alias contains only complex character. Please use a more simple alias."
msgstr ""
"Cet alias ne contient que des caractères complexes. Merci d'utiliser un "
"alias plus simple."
#: apps/note/models/notes.py:285
#: apps/note/models/notes.py:317
msgid "An alias with a similar name already exists: {} "
msgstr "Un alias avec un nom similaire existe déjà : {} "
#: apps/note/models/notes.py:299
#: apps/note/models/notes.py:331
msgid "You can't delete your main alias."
msgstr "Vous ne pouvez pas supprimer votre alias principal."
@ -1519,7 +1563,7 @@ msgstr "Transactions de crédit/retrait"
msgid "membership transaction"
msgstr "transaction d'adhésion"
#: apps/note/models/transactions.py:385 apps/treasury/models.py:292
#: apps/note/models/transactions.py:385 apps/treasury/models.py:293
msgid "membership transactions"
msgstr "transactions d'adhésion"
@ -1535,7 +1579,8 @@ msgstr "Cliquez pour valider"
msgid "No reason specified"
msgstr "Pas de motif spécifié"
#: apps/note/tables.py:169 apps/note/tables.py:213 apps/treasury/tables.py:39
#: apps/note/tables.py:173 apps/note/tables.py:194 apps/note/tables.py:239
#: apps/treasury/tables.py:39
#: apps/treasury/templates/treasury/invoice_confirm_delete.html:30
#: apps/treasury/templates/treasury/sogecredit_detail.html:65
#: apps/wei/tables.py:75 apps/wei/tables.py:118
@ -1546,7 +1591,7 @@ msgstr "Pas de motif spécifié"
msgid "Delete"
msgstr "Supprimer"
#: apps/note/tables.py:197 apps/note/templates/note/conso_form.html:132
#: apps/note/tables.py:222 apps/note/templates/note/conso_form.html:132
#: apps/wei/tables.py:49 apps/wei/tables.py:50
#: apps/wei/templates/wei/base.html:89
#: apps/wei/templates/wei/bus_detail.html:20
@ -1556,7 +1601,7 @@ msgstr "Supprimer"
msgid "Edit"
msgstr "Éditer"
#: apps/note/tables.py:201 apps/note/tables.py:224
#: apps/note/tables.py:226 apps/note/tables.py:253
msgid "Hide/Show"
msgstr "Afficher/Masquer"
@ -1637,7 +1682,7 @@ msgid "Amount"
msgstr "Montant"
#: apps/note/templates/note/transaction_form.html:132
#: apps/treasury/models.py:54
#: apps/treasury/models.py:55
msgid "Name"
msgstr "Nom"
@ -1717,7 +1762,7 @@ msgstr "Chercher un bouton"
msgid "Update button"
msgstr "Modifier le bouton"
#: apps/note/views.py:151 note_kfet/templates/base.html:67
#: apps/note/views.py:151 note_kfet/templates/base.html:66
msgid "Consumptions"
msgstr "Consommations"
@ -1915,7 +1960,7 @@ msgstr ""
"Vous n'avez pas la permission d'ajouter une instance du modèle « {model} » "
"avec ces paramètres. Merci de les corriger et de réessayer."
#: apps/permission/views.py:112 note_kfet/templates/base.html:109
#: apps/permission/views.py:112 note_kfet/templates/base.html:108
msgid "Rights"
msgstr "Droits"
@ -2122,11 +2167,11 @@ msgstr ""
msgid "Invalidate pre-registration"
msgstr "Invalider l'inscription"
#: apps/treasury/apps.py:12 note_kfet/templates/base.html:97
#: apps/treasury/apps.py:12 note_kfet/templates/base.html:96
msgid "Treasury"
msgstr "Trésorerie"
#: apps/treasury/forms.py:26 apps/treasury/models.py:93
#: apps/treasury/forms.py:26 apps/treasury/models.py:94
#: apps/treasury/templates/treasury/invoice_form.html:22
msgid "This invoice is locked and can no longer be edited."
msgstr "Cette facture est verrouillée et ne peut plus être éditée."
@ -2139,7 +2184,7 @@ msgstr "La remise est déjà fermée."
msgid "You can't change the type of the remittance."
msgstr "Vous ne pouvez pas changer le type de la remise."
#: apps/treasury/forms.py:125 apps/treasury/models.py:267
#: apps/treasury/forms.py:125 apps/treasury/models.py:268
#: apps/treasury/tables.py:97 apps/treasury/tables.py:105
#: apps/treasury/templates/treasury/invoice_list.html:16
#: apps/treasury/templates/treasury/remittance_list.html:16
@ -2155,116 +2200,116 @@ msgstr "Pas de remise associée"
msgid "Invoice identifier"
msgstr "Numéro de facture"
#: apps/treasury/models.py:40
#: apps/treasury/models.py:41
msgid "BDE"
msgstr "BDE"
#: apps/treasury/models.py:45
#: apps/treasury/models.py:46
msgid "Object"
msgstr "Objet"
#: apps/treasury/models.py:49
#: apps/treasury/models.py:50
msgid "Description"
msgstr "Description"
#: apps/treasury/models.py:58
#: apps/treasury/models.py:59
msgid "Address"
msgstr "Adresse"
#: apps/treasury/models.py:63 apps/treasury/models.py:193
#: apps/treasury/models.py:64 apps/treasury/models.py:194
msgid "Date"
msgstr "Date"
#: apps/treasury/models.py:67
#: apps/treasury/models.py:68
msgid "Acquitted"
msgstr "Acquittée"
#: apps/treasury/models.py:72
#: apps/treasury/models.py:73
msgid "Locked"
msgstr "Verrouillée"
#: apps/treasury/models.py:73
#: apps/treasury/models.py:74
msgid "An invoice can't be edited when it is locked."
msgstr "Une facture ne peut plus être modifiée si elle est verrouillée."
#: apps/treasury/models.py:79
#: apps/treasury/models.py:80
msgid "tex source"
msgstr "fichier TeX source"
#: apps/treasury/models.py:113 apps/treasury/models.py:129
#: apps/treasury/models.py:114 apps/treasury/models.py:130
msgid "invoice"
msgstr "facture"
#: apps/treasury/models.py:114
#: apps/treasury/models.py:115
msgid "invoices"
msgstr "factures"
#: apps/treasury/models.py:117
#: apps/treasury/models.py:118
#, python-brace-format
msgid "Invoice #{id}"
msgstr "Facture n°{id}"
#: apps/treasury/models.py:134
#: apps/treasury/models.py:135
msgid "Designation"
msgstr "Désignation"
#: apps/treasury/models.py:140
#: apps/treasury/models.py:141
msgid "Quantity"
msgstr "Quantité"
#: apps/treasury/models.py:145
#: apps/treasury/models.py:146
msgid "Unit price"
msgstr "Prix unitaire"
#: apps/treasury/models.py:161
#: apps/treasury/models.py:162
msgid "product"
msgstr "produit"
#: apps/treasury/models.py:162
#: apps/treasury/models.py:163
msgid "products"
msgstr "produits"
#: apps/treasury/models.py:182
#: apps/treasury/models.py:183
msgid "remittance type"
msgstr "type de remise"
#: apps/treasury/models.py:183
#: apps/treasury/models.py:184
msgid "remittance types"
msgstr "types de remises"
#: apps/treasury/models.py:204
#: apps/treasury/models.py:205
msgid "Comment"
msgstr "Commentaire"
#: apps/treasury/models.py:209
#: apps/treasury/models.py:210
msgid "Closed"
msgstr "Fermée"
#: apps/treasury/models.py:213
#: apps/treasury/models.py:214
msgid "remittance"
msgstr "remise"
#: apps/treasury/models.py:214
#: apps/treasury/models.py:215
msgid "remittances"
msgstr "remises"
#: apps/treasury/models.py:247
#: apps/treasury/models.py:248
msgid "Remittance #{:d}: {}"
msgstr "Remise n°{:d} : {}"
#: apps/treasury/models.py:271
#: apps/treasury/models.py:272
msgid "special transaction proxy"
msgstr "proxy de transaction spéciale"
#: apps/treasury/models.py:272
#: apps/treasury/models.py:273
msgid "special transaction proxies"
msgstr "proxys de transactions spéciales"
#: apps/treasury/models.py:298
#: apps/treasury/models.py:299
msgid "credit transaction"
msgstr "transaction de crédit"
#: apps/treasury/models.py:430
#: apps/treasury/models.py:432
msgid ""
"This user doesn't have enough money to pay the memberships with its note. "
"Please ask her/him to credit the note before invalidating this credit."
@ -2272,16 +2317,16 @@ msgstr ""
"Cet utilisateur n'a pas assez d'argent pour payer les adhésions avec sa "
"note. Merci de lui demander de recharger sa note avant d'invalider ce crédit."
#: apps/treasury/models.py:451
#: apps/treasury/models.py:453
#: apps/treasury/templates/treasury/sogecredit_detail.html:10
msgid "Credit from the Société générale"
msgstr "Crédit de la Société générale"
#: apps/treasury/models.py:452
#: apps/treasury/models.py:454
msgid "Credits from the Société générale"
msgstr "Crédits de la Société générale"
#: apps/treasury/models.py:455
#: apps/treasury/models.py:457
#, python-brace-format
msgid "Soge credit for {user}"
msgstr "Crédit de la société générale pour l'utilisateur {user}"
@ -2530,7 +2575,7 @@ msgstr "Gérer les crédits de la Société générale"
#: apps/wei/apps.py:10 apps/wei/models.py:50 apps/wei/models.py:51
#: apps/wei/models.py:62 apps/wei/models.py:180
#: note_kfet/templates/base.html:103
#: note_kfet/templates/base.html:102
msgid "WEI"
msgstr "WEI"
@ -2538,7 +2583,7 @@ msgstr "WEI"
msgid "The selected user is not validated. Please validate its account first"
msgstr ""
"L'utilisateur sélectionné n'est pas validé. Merci de d'abord valider son "
"compte."
"compte"
#: apps/wei/forms/registration.py:59 apps/wei/models.py:126
#: apps/wei/models.py:323
@ -2579,7 +2624,7 @@ msgstr "Sélectionnez les rôles qui vous intéressent."
msgid "This team doesn't belong to the given bus."
msgstr "Cette équipe n'appartient pas à ce bus."
#: apps/wei/forms/surveys/wei2021.py:35
#: apps/wei/forms/surveys/wei2021.py:35 apps/wei/forms/surveys/wei2022.py:38
msgid "Choose a word:"
msgstr "Choisissez un mot :"
@ -3140,19 +3185,19 @@ msgstr "Répartir les 1A dans les bus"
msgid "Attribute bus"
msgstr "Attribuer un bus"
#: note_kfet/settings/base.py:161
#: note_kfet/settings/base.py:172
msgid "German"
msgstr "Allemand"
#: note_kfet/settings/base.py:162
#: note_kfet/settings/base.py:173
msgid "English"
msgstr "Anglais"
#: note_kfet/settings/base.py:163
#: note_kfet/settings/base.py:174
msgid "Spanish"
msgstr "Espagnol"
#: note_kfet/settings/base.py:164
#: note_kfet/settings/base.py:175
msgid "French"
msgstr "Français"
@ -3209,7 +3254,7 @@ msgstr ""
"erreur, qui sera corrigée rapidement. Vous pouvez désormais aller boire une "
"bière."
#: note_kfet/templates/autocomplete_model.html:14
#: note_kfet/templates/autocomplete_model.html:15
msgid "Reset"
msgstr "Réinitialiser"
@ -3217,34 +3262,34 @@ msgstr "Réinitialiser"
msgid "The ENS Paris-Saclay BDE note."
msgstr "La note du BDE de l'ENS Paris-Saclay."
#: note_kfet/templates/base.html:79
#: note_kfet/templates/base.html:78
msgid "Users"
msgstr "Utilisateurs"
#: note_kfet/templates/base.html:85
#: note_kfet/templates/base.html:84
msgid "Clubs"
msgstr "Clubs"
#: note_kfet/templates/base.html:114
#: note_kfet/templates/base.html:113
msgid "Admin"
msgstr "Admin"
#: note_kfet/templates/base.html:128
#: note_kfet/templates/base.html:127
msgid "My account"
msgstr "Mon compte"
#: note_kfet/templates/base.html:131
#: note_kfet/templates/base.html:130
msgid "Log out"
msgstr "Se déconnecter"
#: note_kfet/templates/base.html:139
#: note_kfet/templates/base.html:138
#: note_kfet/templates/registration/signup.html:6
#: note_kfet/templates/registration/signup.html:11
#: note_kfet/templates/registration/signup.html:28
msgid "Sign up"
msgstr "Inscription"
#: note_kfet/templates/base.html:146
#: note_kfet/templates/base.html:145
#: note_kfet/templates/registration/login.html:6
#: note_kfet/templates/registration/login.html:15
#: note_kfet/templates/registration/login.html:38
@ -3252,7 +3297,7 @@ msgstr "Inscription"
msgid "Log in"
msgstr "Se connecter"
#: note_kfet/templates/base.html:160
#: note_kfet/templates/base.html:159
msgid ""
"You are not a BDE member anymore. Please renew your membership if you want "
"to use the note."
@ -3260,7 +3305,7 @@ msgstr ""
"Vous n'êtes plus adhérent BDE. Merci de réadhérer si vous voulez profiter de "
"la note."
#: note_kfet/templates/base.html:166
#: note_kfet/templates/base.html:165
msgid ""
"Your e-mail address is not validated. Please check your mail inbox and click "
"on the validation link."
@ -3268,7 +3313,7 @@ msgstr ""
"Votre adresse e-mail n'est pas validée. Merci de vérifier votre boîte mail "
"et de cliquer sur le lien de validation."
#: note_kfet/templates/base.html:172
#: note_kfet/templates/base.html:171
msgid ""
"You declared that you opened a bank account in the Société générale. The "
"bank did not validate the creation of the account to the BDE, so the "
@ -3282,14 +3327,18 @@ msgstr ""
"vérification peut durer quelques jours. Merci de vous assurer de bien aller "
"au bout de vos démarches."
#: note_kfet/templates/base.html:195
#: note_kfet/templates/base.html:194
msgid "Contact us"
msgstr "Nous contacter"
#: note_kfet/templates/base.html:197
#: note_kfet/templates/base.html:196
msgid "Technical Support"
msgstr "Support technique"
#: note_kfet/templates/base.html:198
msgid "FAQ (FR)"
msgstr "FAQ (FR)"
#: note_kfet/templates/base_search.html:15
msgid "Search by attribute such as name…"
msgstr "Chercher par un attribut tel que le nom …"

View File

@ -18,7 +18,7 @@ MAILTO=notekfet2020@lists.crans.org
# Spammer les gens en négatif
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
00 8 6 * * 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 les rapports aux gens
55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0
# Mettre à jour les boutons mis en avant

View File

@ -252,7 +252,7 @@ REST_FRAMEWORK = {
'rest_framework.authentication.TokenAuthentication',
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_PAGINATION_CLASS': 'apps.api.pagination.CustomPagination',
'PAGE_SIZE': 20,
}

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

@ -65,7 +65,10 @@ mark {
/* Last BDE colors */
.bg-primary {
background-color: rgb(18, 67, 4) !important;
/* background-color: rgb(18, 67, 4) !important; */
/* MODE VIEUXCON=ON */
/* background-color: rgb(166, 0, 2) !important; */
background-color: rgb(0, 0, 0) !important;
}
html {
@ -80,15 +83,15 @@ body {
.btn-outline-primary:hover,
.btn-outline-primary:not(:disabled):not(.disabled).active,
.btn-outline-primary:not(:disabled):not(.disabled):active {
color: #fff;
background-color: rgb(18, 67, 46);
border-color: rgb(18, 67, 46);
color: rgb(241, 229, 52);
background-color: rgb(228, 35, 132);
border-color: rgb(228, 35, 132);
}
.btn-outline-primary {
color: rgb(18, 67, 46);
background-color: rgba(248, 249, 250, 0.9);
border-color: rgb(18, 67, 46);
color: #fff;
background-color: #000;
border-color: #464647;
}
.turbolinks-progress-bar {
@ -98,36 +101,63 @@ body {
.btn-primary:hover,
.btn-primary:not(:disabled):not(.disabled).active,
.btn-primary:not(:disabled):not(.disabled):active {
color: #fff;
background-color: rgb(18, 67, 46);
border-color: rgb(18, 67, 46);
color: rgb(241, 229, 52);
background-color: rgb(228, 35, 132);
border-color: rgb(228, 35, 132);
}
.btn-primary {
color: rgba(248, 249, 250, 0.9);
background-color: rgb(28, 114, 10);
border-color: rgb(18, 67, 46);
color: #fff;
background-color: #000;
border-color: #adb5bd;
}
.border-primary {
border-color: rgb(28, 114, 10) !important;
border-color: rgb(228, 35, 132) !important;
}
.btn-secondary {
color: #fff;
background-color: #000;
border-color: #adb5bd;
}
.btn-secondary:hover,
.btn-secondary:not(:disabled):not(.disabled).active,
.btn-secondary:not(:disabled):not(.disabled):active {
color: rgb(241, 229, 52);
background-color: rgb(228, 35, 132);
border-color: rgb(228, 35, 132);
}
.btn-outline-dark {
color: #343a40;
border-color: #343a40;
}
.btn-outline-dark:hover,
.btn-outline-dark:not(:disabled):not(.disabled).active,
.btn-outline-dark:not(:disabled):not(.disabled):active {
color: rgb(241, 229, 52);
background-color: rgb(228, 35, 132);
border-color: rgb(228, 35, 132);
}
a {
color: rgb(28, 114, 10);
color: rgb(228, 35, 132);
}
a:hover {
color: rgb(122, 163, 75);
color: rgb(228, 35, 132);
}
.form-control:focus {
box-shadow: 0 0 0 0.25rem rgba(122, 163, 75, 0.25);
border-color: rgb(122, 163, 75);
box-shadow: 0 0 0 0.25rem rgb(228 35 132 / 50%);
border-color: rgb(228, 35, 132);
}
.btn-outline-primary.focus {
box-shadow: 0 0 0 0.25rem rgba(122, 163, 75, 0.5);
box-shadow: 0 0 0 0.25rem rgb(228 35 132 / 10%);
}

View File

@ -13,21 +13,29 @@ $(document).ready(function () {
$('#' + prefix + '_reset').removeClass('d-none')
$.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) {
let html = ''
let html = '<ul class="list-group list-group-flush" id="' + prefix + '_list">'
objects.results.forEach(function (obj) {
html += li(prefix + '_' + obj.id, obj[name_field])
})
html += '</ul>'
const results_list = $('#' + prefix + '_list')
results_list.html(html)
target.tooltip({
html: true,
placement: 'bottom',
trigger: 'manual',
container: target.parent(),
fallbackPlacement: 'clockwise'
})
target.attr("data-original-title", html).tooltip("show")
objects.results.forEach(function (obj) {
$('#' + prefix + '_' + obj.id).click(function () {
target.val(obj[name_field])
$('#' + prefix + '_pk').val(obj.id)
results_list.html('')
target.tooltip("hide")
target.removeClass('is-invalid')
target.addClass('is-valid')
@ -37,8 +45,8 @@ $(document).ready(function () {
if (input === obj[name_field]) { $('#' + prefix + '_pk').val(obj.id) }
})
if (results_list.children().length === 1 && e.originalEvent.keyCode >= 32) {
results_list.children().first().trigger('click')
if (objects.results.length === 1 && e.originalEvent.keyCode >= 32) {
$('#' + prefix + '_' + objects.results[0].id).trigger('click')
}
})
})

View File

@ -9,9 +9,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
name="{{ widget.name }}_name" autocomplete="off"
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
{% endfor %}
aria-describedby="{{widget.attrs.id}}_tooltip">
{% if widget.resetable %}
<a id="{{ widget.attrs.id }}_reset" class="btn btn-light autocomplete-reset{% if not widget.value %} d-none{% endif %}">{% trans "Reset" %}</a>
{% endif %}
<ul class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
</ul>

View File

@ -194,6 +194,8 @@ SPDX-License-Identifier: GPL-3.0-or-later
class="text-muted">{% trans "Contact us" %}</a> &mdash;
<a href="mailto:{{ "SUPPORT_EMAIL" | getenv }}"
class="text-muted">{% trans "Technical Support" %}</a> &mdash;
<a href="https://note.crans.org/doc/faq/"
class="text-muted">{% trans "FAQ (FR)" %}</a> &mdash;
</span>
{% csrf_token %}
<select title="language" name="language"

View File

@ -23,11 +23,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% csrf_token %}
{{ form|crispy }}
{{ profile_form|crispy }}
{{ soge_form|crispy }}
{% comment "Soge not for membership (only WEI)" %} {{ soge_form|crispy }} {% endcomment %}
<button class="btn btn-success" type="submit">
{% trans "Sign up" %}
</button>
</form>
</div>
</div>
{% endblock %}
{% endblock %}

34
shell-static.nix Executable file
View File

@ -0,0 +1,34 @@
# This is a workaround meant for use with the nix package manager. If you don't know what it is or don't use it, please ignore this file.
#
# The nk20 javascript static location are hardcoded for imperative system.
# This make ./manage.py collectstatic hard to use with nixos.
#
# A workaround is to enter a FHSUserEnv with the static placed under /share/javascript/<static>.
# This emulate a debian like system and enable collecting static normally with ./manage.py collectstatics.
# The regular shell.nix should be enough for other configurations.
#
# Warning, you are still supposed to use pip package with a venv !
{ pkgs ? import <nixpkgs> {} }:
(pkgs.buildFHSUserEnv {
name = "pipzone";
targetPkgs = pkgs: (with pkgs;
let
fhs-static = stdenv.mkDerivation {
name = "fhs-static";
buildCommand = ''
mkdir -p $out/share/javascript/bootstrap4
mkdir -p $out/share/javascript/jquery
ln -s ${python39Packages.xstatic-bootstrap}/lib/python3.9/site-packages/xstatic/pkg/bootstrap/data/* $out/share/javascript/bootstrap4
ln -s ${python39Packages.xstatic-jquery}/lib/python3.9/site-packages/xstatic/pkg/jquery/data/* $out/share/javascript/jquery
'';
};
in [
fhs-static
python39
gettext
python39Packages.pip
python39Packages.virtualenv
python39Packages.setuptools
]);
runScript = "bash";
}).env

23
shell.nix Executable file
View File

@ -0,0 +1,23 @@
# This is meant for use with the nix package manager. If you don't know what it is or don't use it, please ignore this file.
#
# This shell.nix contains all dependencies require to create a venv and pip install -r requirements.txt.
#
# Please check shell-static.nix for running ./manage.py collectstatics.
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
buildInputs = with pkgs; [
python39
python39Packages.pip
python39Packages.setuptools
gettext
];
shellHook = ''
# Tells pip to put packages into $PIP_PREFIX instead of the usual locations.
# See https://pip.pypa.io/en/stable/user_guide/#environment-variables.
export PIP_PREFIX=$(pwd)/_build/pip_packages
export PYTHONPATH="$PIP_PREFIX/${pkgs.python39.sitePackages}:$PYTHONPATH"
export PATH="$PIP_PREFIX/bin:$PATH"
unset SOURCE_DATE_EPOCH
'';
}