1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-02-25 17:36:31 +00:00

Compare commits

...

102 Commits

Author SHA1 Message Date
misterkrafts
90e3871934 Merge branch 'main' into potvieux 2024-01-11 02:19:51 +01:00
nicomarg
802fd8c2d7 Merge branch 'search_conso_bugfix' into 'main'
Bugfix

See merge request bde/nk20!225
2023-11-13 14:29:29 +01:00
Nicolas Margulies
5209a586a9 Fixed const being redeclared when script is reevaluated 2023-11-08 17:10:05 +01:00
nicomarg
24f54ac876 Merge branch 'search-conso' into 'main'
Added a search tab for the conso page, fixes #58

Closes #58

See merge request bde/nk20!224
2023-10-27 16:45:41 +02:00
Nicolas Margulies
988b4c9e88 Linting 2023-10-26 21:03:48 +02:00
Nicolas Margulies
e32c267995 Moved js code to the external conso file 2023-10-26 19:10:43 +02:00
Nicolas Margulies
5e39209ab1 Made searchbar completely client-based 2023-10-26 19:01:09 +02:00
Nicolas Margulies
08b2fabe07 Removing jquery means changing the event API... 2023-10-26 00:22:51 +02:00
Nicolas Margulies
405479e5ad Execute script to add behavior to searched buttons 2023-10-26 00:10:56 +02:00
Nicolas Margulies
0cc130092f Added a search tab for the conso page 2023-10-25 20:01:48 +02:00
charliep
ff6e207512 Merge branch 'beta' into 'main'
check for a model in permission and use that in treasury

See merge request bde/nk20!222
2023-09-29 12:08:00 +02:00
bleizi
0f1e4d2e60
check for a model in permission and use that in treasury 2023-09-28 18:48:57 +02:00
nicomarg
6255bcbbb1 Merge branch 'beta' into 'main'
Merge beta

See merge request bde/nk20!221
2023-09-27 17:14:49 +02:00
Nicolas Margulies
d82a1001c4 Moved transaction through frienships right to basic rights 2023-09-27 16:55:00 +02:00
Nicolas Margulies
31a54482f0 Updated doc to tell maintainers to create psql superusers 2023-09-27 16:53:30 +02:00
nicomarg
4ee02345d4 Merge branch 'better-friendship-view' into 'main'
Rework of the friendships page

See merge request bde/nk20!220
2023-09-21 15:48:00 +02:00
bleizi
422c087d17
fix wei test 2023-09-20 07:04:13 +02:00
Nicolas Margulies
30d6e2c95e Added trusts to note admin site 2023-09-19 15:07:30 +02:00
Nicolas Margulies
f3a3f07e38 Tweaked message and did missing french translations 2023-09-18 17:29:52 +02:00
Nicolas Margulies
a5e802f370 Improved the error message when trying to duplicate a Trust 2023-09-18 17:12:31 +02:00
Nicolas Margulies
540f3bc354 regenerated messages so locations are consistent with codebase 2023-09-02 00:04:54 +02:00
elkmaennchen
2d19457506 Add spanish translation for friendship 2023-09-01 17:35:52 +02:00
Nicolas Margulies
72786d0d2b Translated js strings, unified some case 2023-09-01 17:34:52 +02:00
Nicolas Margulies
f099cbc879 Linting 2023-09-01 17:32:29 +02:00
Nicolas Margulies
977eb7c0d4 Generated translation files, did french 2023-09-01 17:30:38 +02:00
Nicolas Margulies
d81b1f2710 Tweaked trust back display 2023-09-01 17:15:24 +02:00
Nicolas Margulies
6a69590a82 Added a 'trust back' button, front can be improved 2023-09-01 17:15:24 +02:00
Nicolas Margulies
7afc583282 Made trust adding widget resetable, corrected the unexpected empty field behavior and improved autocomplete's responsiveness 2023-09-01 17:15:24 +02:00
Nicolas Margulies
4fb0b7d736 First pass on a display of users trusting you, added a corresponding right 2023-09-01 17:15:13 +02:00
bleizi
18a5b65a1c Merge branch 'VSS' into 'main'
anti VSS

See merge request bde/nk20!219
2023-08-31 15:58:52 +02:00
bleizi
f545af4977
typo 2023-08-31 15:40:49 +02:00
bleizi
103e2d0635
add GC anti-VSS 2023-08-31 15:25:44 +02:00
bleizi
aedf0e87ba
prez BDE can block note 2023-08-31 13:46:27 +02:00
bleizi
dab45b5fd4
translation 2023-08-31 13:40:53 +02:00
bleizi
b3353b563c
add VSS checkbox on registration 2023-08-31 12:21:38 +02:00
bleizi
6bc52be707 Merge branch 'WEI_with_questions' into 'main'
Wei with questions

See merge request bde/nk20!218
2023-08-31 12:01:39 +02:00
charliep
834d68fe35 typo 2023-08-31 11:45:17 +02:00
bleizi
c6a2849d35
test 2023-08-30 16:16:29 +02:00
bleizi
4ab22c92b3
After WEI registration validation, come back to unvalidate registration page 2023-08-30 09:52:17 +02:00
bleizi
c328c1457c
add register button at the end of WEI registration 2023-08-28 22:27:45 +02:00
bleizi
96da7d01ae
change on a field that everyone have (1A don't have bus) 2023-08-28 19:26:51 +02:00
bleizi
d27f942339
typo 2023-08-28 10:13:28 +02:00
bleizi
738d6c932d
questions ! 2023-08-28 00:42:33 +02:00
bleizi
1760196578
more tests 2023-08-27 23:11:40 +02:00
bleizi
13b9b6edea
tests 2023-08-27 18:09:46 +02:00
bleizi
e06e3b2972
one question by page 2023-08-26 23:47:10 +02:00
bleizi
9596aa7b8c
base for questions instead of words 2023-08-26 17:52:48 +02:00
bleizi
ba0d64f0d4 Merge branch 'new_default_year' into 'main'
new default year

See merge request bde/nk20!217
2023-08-23 23:53:45 +02:00
bleizi
8d17801e28
new default year 2023-08-23 23:32:01 +02:00
bleizi
609362c4f8 Merge branch 'update_permission' into 'main'
Update permission

See merge request bde/nk20!216
2023-08-23 22:50:24 +02:00
bleizi
03d2d5f03e
change -50€ to -20€ and doc 2023-08-22 21:51:02 +02:00
bleizi
d2057a9f45
remove respo-info perm and change Prez BDE prem 2023-08-22 21:19:05 +02:00
charliep
b6e68eeebe Merge branch 'charliep-main-patch-47507' into 'main'
Update forms.py - Homogénéisation des cases

See merge request bde/nk20!215
2023-08-08 15:39:44 +02:00
charliep
6410542027 Update forms.py - Homogénéisation des cases 2023-08-08 15:38:29 +02:00
bleizi
6b1cd3ba7a
manage self aliases for BDE member instead of kfet 2023-07-24 12:42:44 +02:00
bleizi
9f114b8ca2
fixtures activities 2023-07-24 12:26:34 +02:00
bleizi
e0132b6dc8
migration permission 2023-07-24 12:20:16 +02:00
bleizi
f1cc82fab3 Merge branch 'linters' into 'main'
Linters

See merge request bde/nk20!214
2023-07-17 09:27:22 +02:00
bleizi
644cf14c4b missing brackets 2023-07-17 09:11:25 +02:00
bleizi
f19a489313
linters (removing B019) 2023-07-17 08:50:10 +02:00
bleizi
dedd6c69cc
new commits in nk20-scripts 2023-07-17 06:58:01 +02:00
charliep
b42f5afeab Merge branch 'registration2023' into 'main'
Registration2023

See merge request bde/nk20!213
2023-07-16 17:12:33 +02:00
bleizi
31e67ae3f6
typo 2023-07-09 16:06:30 +02:00
bleizi
b08da7a727
help text on WEI emergency contact 2023-07-09 14:57:48 +02:00
bleizi
451aa64f33
Unisexe clothing cut 2023-07-09 12:30:23 +02:00
bleizi
3c99b0f3e9
do not change transactions date when validating/deleting credit-soge (and typo) 2023-07-09 11:23:33 +02:00
bleizi
201a179947
linters 2023-07-09 10:36:36 +02:00
bleizi
96784aee3b
remove (comment) soge from registration 2023-07-07 21:44:18 +02:00
bleizi
981c4d0300
fix update of club membership start/end date 2023-07-07 20:39:19 +02:00
bleizi
11223430fd Merge branch 'WEI2023' into 'main'
Préparation WEI 2023

See merge request bde/nk20!212
2023-07-04 19:17:17 +02:00
charliep
7aeb977e72 Oubli dans le fichier test_wei_registration_.py d'un 2022 en 2023 2023-07-04 18:33:54 +02:00
charliep
52fef1df42 Préparation WEI 2023 2023-07-04 18:23:43 +02:00
bleizi
16f8a60a3f
possibilité de l'adhésion au BDA lors de l'inscription 2023-07-04 17:32:48 +02:00
bleizi
2839d3de1e
club facultatif pour un role lors du changement dans l'interface admin 2023-06-22 14:52:11 +02:00
bleizi
30afa6da0a
création d'une permission pour faire les crédits uniquement 2023-06-12 18:29:23 +02:00
bleizi
84fc77696f
see activities: BDE members instead of kfet 2023-06-05 19:04:19 +02:00
bleizi
19fc620d1f
see kfet members' note for respot 2023-06-05 17:26:49 +02:00
charliep
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
bleizi
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
Théo Le Moigne
364b18e188
migrations 2023-04-14 16:52:46 +02:00
Hugo
10a883b2e5 new treasury phone number 2023-04-14 16:00:48 +02:00
misterkrafts
1410ab6c4f Almost on time, the SIRET number is now changed 2023-04-14 15:35:18 +02:00
misterkrafts
623dd61be6 Remove phone number 2023-04-14 14:56:34 +02:00
Hugo
48a0a87e7c changement du fond des factures 2023-04-14 00:25:26 +02:00
bleizi
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
misterkrafts
63c1d74f1a Ignore notes containing '- BDE-' in the list of negative balances 2023-04-07 15:47:06 +02:00
Théo Le Moigne
c42fb380a6
frequence des mails de négatif aux trez : 1 mois -> 1 semiane 2023-04-06 09:04:27 +02:00
Théo Le Moigne
c636d52a73
traduction (allemand et espagnol probablement pas optimal) 2023-03-31 17:21:58 +02:00
Otthorn
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
charliep
9c9149b53a Ajout d'un lien vers la FAQ de la note. 2023-03-31 12:34:14 +02:00
misterkrafts
cb74311e7b Commit migration, j'étais triggered 2023-03-30 19:14:52 +02:00
misterkrafts
9d7dd566c9 Ignore /tmp/ 2023-03-30 17:26:06 +02:00
Théo Le Moigne
6bceb394c5
prez BDE sould see invoice list 2023-03-29 20:43:54 +02:00
Théo Le Moigne
62cf8f9d84
forgetted coma 2023-03-28 20:41:53 +02:00
parpaing
9944ebcaad changement des couleurs de la note vers les couleurs totalist spies 2023-03-25 02:13:16 +01:00
parpaing
8537f043f7 changement des couleurs de la note vers les couleurs totalist spies 2023-03-25 00:57:19 +01:00
Théo Le Moigne
2dd1c3fb89
change mask for some perm 2023-03-20 22:35:51 +01:00
Théo Le Moigne
c8665c5798
change permissions for role 2023-03-20 22:21:18 +01:00
Théo Le Moigne
e9f1b6f52d
change permanent permissions 2023-03-20 17:19:14 +01:00
Théo Le Moigne
1d95ae4810
sort perm by number 2023-03-20 16:16:32 +01:00
bleizi
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
parpaing
73640b1dfa changement du fond des factures 2023-01-30 00:06:45 +01:00
68 changed files with 2334 additions and 1177 deletions

1
.gitignore vendored
View File

@ -42,6 +42,7 @@ map.json
backups/
/static/
/media/
/tmp/
# Virtualenv
env/

View File

@ -6,7 +6,7 @@
"name": "Pot",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 500
"guest_entry_fee": 1000
}
},
{
@ -28,5 +28,25 @@
"can_invite": false,
"guest_entry_fee": 0
}
},
{
"model": "activity.activitytype",
"pk": 5,
"fields": {
"name": "Soir\u00e9e avec entrées",
"manage_entries": true,
"can_invite": false,
"guest_entry_fee": 0
}
},
{
"model": "activity.activitytype",
"pk": 7,
"fields": {
"name": "Soir\u00e9e avec invitations",
"manage_entries": true,
"can_invite": true,
"guest_entry_fee": 0
}
}
]

View File

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

View File

@ -47,6 +47,13 @@ class ProfileForm(forms.ModelForm):
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
VSS_charter_read = forms.BooleanField(
required=True,
label=_("Anti-VSS (<em>Violences Sexistes et Sexuelles</em>) charter read and approved"),
help_text=_("Tick after having read and accepted the anti-VSS charter \
<a href=https://perso.crans.org/club-bde/Charte-anti-VSS.pdf target=_blank> available here in pdf</a>")
)
def clean_promotion(self):
promotion = self.cleaned_data["promotion"]
if promotion > timezone.now().year:

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,18 @@
# Generated by Django 2.2.28 on 2023-08-23 21:29
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0009_auto_20220904_2325'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='promotion',
field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2023-08-31 09:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0010_new_default_year'),
]
operations = [
migrations.AddField(
model_name='profile',
name='VSS_charter_read',
field=models.BooleanField(default=False, verbose_name='VSS charter read'),
),
]

View File

@ -134,6 +134,11 @@ class Profile(models.Model):
default=False,
)
VSS_charter_read = models.BooleanField(
verbose_name=_("VSS charter read"),
default=False
)
@property
def ens_year(self):
"""
@ -263,7 +268,7 @@ class Club(models.Model):
today = datetime.date.today()
if (today - self.membership_start).days >= 365:
while (today - self.membership_start).days >= 365:
if self.membership_start:
self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)

View File

@ -1,7 +1,7 @@
/**
* On form submit, create a new friendship
*/
function create_trust (e) {
function form_create_trust (e) {
// Do not submit HTML form
e.preventDefault()
@ -14,25 +14,35 @@ function create_trust (e) {
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)
})
create_trust(formData.get('trusting'), trusted_alias.note)
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* On click of "delete", delete the alias
* @param button_id:Integer Alias id to remove
* Create a trust between users
* @param trusting:Integer trusting note id
* @param trusted:Integer trusted note id
*/
function create_trust(trusting, trusted) {
$.post('/api/note/trust/', {
trusting: trusting,
trusted: trusted,
csrfmiddlewaretoken: CSRF_TOKEN
}).done(function () {
// Reload tables
$('#trust_table').load(location.pathname + ' #trust_table')
$('#trusted_table').load(location.pathname + ' #trusted_table')
addMsg(gettext('Friendship successfully added'), 'success')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
}
/**
* On click of "delete", delete the trust
* @param button_id:Integer Trust id to remove
*/
function delete_button (button_id) {
$.ajax({
@ -42,6 +52,7 @@ function delete_button (button_id) {
}).done(function () {
addMsg(gettext('Friendship successfully deleted'), 'success')
$('#trust_table').load(location.pathname + ' #trust_table')
$('#trusted_table').load(location.pathname + ' #trusted_table')
}).fail(function (xhr, _textStatus, _error) {
errMsg(xhr.responseJSON)
})
@ -49,5 +60,5 @@ function delete_button (button_id) {
$(document).ready(function () {
// Attach event
document.getElementById('form_trust').addEventListener('submit', create_trust)
document.getElementById('form_trust').addEventListener('submit', form_create_trust)
})

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block profile_content %}
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "Note friendships" %}
{% trans "Add friends" %}
</h3>
<div class="card-body">
{% if can_create %}
@ -24,7 +24,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% render_table trusting %}
</div>
<div class="alert alert-warning card">
<div class="alert alert-warning card mb-3">
{% blocktrans trimmed %}
Adding someone as a friend enables them to initiate transactions coming
from your account (while keeping your balance positive). This is
@ -33,6 +33,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
friends without needing additional rights among them.
{% endblocktrans %}
</div>
<div class="card bg-light mb-3">
<h3 class="card-header text-center">
{% trans "People having you as a friend" %}
</h3>
{% render_table trusted_by %}
</div>
{% endblock %}
{% block extrajavascript %}

View File

@ -183,7 +183,7 @@ class TestMemberships(TestCase):
club = Club.objects.get(name="Kfet")
else:
club = Club.objects.create(
name="Second club " + ("with BDE" if bde_parent else "without BDE"),
name="Second club without BDE",
parent_club=None,
email="newclub@example.com",
require_memberships=True,
@ -335,6 +335,7 @@ class TestMemberships(TestCase):
ml_sports_registration=True,
ml_art_registration=True,
report_frequency=7,
VSS_charter_read=True
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.assertTrue(User.objects.filter(username="toto changed").exists())

View File

@ -8,7 +8,6 @@ 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
@ -21,7 +20,7 @@ from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
from note.models import Alias, NoteClub, NoteUser, Trust
from note.models.transactions import Transaction, SpecialTransaction
from note.tables import HistoryTable, AliasTable, TrustTable
from note.tables import HistoryTable, AliasTable, TrustTable, TrustedTable
from note_kfet.middlewares import _set_current_request
from permission.backends import PermissionBackend
from permission.models import Role
@ -258,17 +257,18 @@ class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
note = context['object'].note
context["trusting"] = TrustTable(
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["trusted_by"] = TrustedTable(
note.trusted.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
trusting=context["object"].note,
trusted=context["object"].note
))
context["widget"] = {
"name": "trusted",
"resetable": True,
"attrs": {
"model_pk": ContentType.objects.get_for_model(Alias).pk,
"class": "autocomplete form-control",
"id": "trusted",
"resetable": True,
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
"name_field": "name",
"placeholder": ""
@ -753,6 +753,10 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
club = old_membership.club
user = old_membership.user
# Update club membership date
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
club.update_membership_dates()
form.instance.club = club
# Get form data

View File

@ -7,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from note_kfet.admin import admin_site
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
RecurrentTransaction, MembershipTransaction, SpecialTransaction
from .templatetags.pretty_money import pretty_money
@ -21,6 +21,16 @@ class AliasInlines(admin.TabularInline):
model = Alias
class TrustInlines(admin.TabularInline):
"""
Define trusts when editing the trusting note
"""
model = Trust
fk_name = "trusting"
extra = 0
readonly_fields = ("trusted",)
@admin.register(Note, site=admin_site)
class NoteAdmin(PolymorphicParentModelAdmin):
"""
@ -92,7 +102,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
"""
Child for an user note, see NoteAdmin
"""
inlines = (AliasInlines,)
inlines = (AliasInlines, TrustInlines)
# We can't change user after creation or the balance
readonly_fields = ('user', 'balance')

View File

@ -11,6 +11,7 @@ from member.models import Membership
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
from rest_framework.utils import model_meta
from rest_framework.validators import UniqueTogetherValidator
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
@ -86,11 +87,9 @@ class TrustSerializer(serializers.ModelSerializer):
class Meta:
model = Trust
fields = '__all__'
def validate(self, attrs):
instance = Trust(**attrs)
instance.clean()
return attrs
validators = [UniqueTogetherValidator(
queryset=Trust.objects.all(), fields=('trusting', 'trusted'),
message=_("This friendship already exists"))]
class AliasSerializer(serializers.ModelSerializer):

View File

@ -325,8 +325,8 @@ class SpecialTransaction(Transaction):
def clean(self):
# SpecialTransaction are only possible with NoteSpecial object
if self.is_credit() == self.is_debit():
raise(ValidationError(_("A special transaction is only possible between a"
" Note associated to a payment method and a User or a Club")))
raise ValidationError(_("A special transaction is only possible between a"
" Note associated to a payment method and a User or a Club"))
@transaction.atomic
def save(self, *args, **kwargs):

View File

@ -221,7 +221,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
.done(function () {
if (!isNaN(source.balance)) {
const newBalance = source.balance - quantity * amount
if (newBalance <= -5000) {
if (newBalance <= -2000) {
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
} else if (newBalance < 0) {
@ -258,3 +258,39 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
})
})
}
var searchbar = document.getElementById("search-input")
var search_results = document.getElementById("search-results")
var old_pattern = null;
var firstMatch = null;
/**
* Updates the button search tab
* @param force Forces the update even if the pattern didn't change
*/
function updateSearch(force = false) {
let pattern = searchbar.value
if (pattern === "")
firstMatch = null;
if ((pattern === old_pattern || pattern === "") && !force)
return;
firstMatch = null;
const re = new RegExp(pattern, "i");
Array.from(search_results.children).forEach(function(b) {
if (re.test(b.innerText)) {
b.hidden = false;
if (firstMatch === null) {
firstMatch = b;
}
} else
b.hidden = true;
});
}
searchbar.addEventListener("input", function (e) {
debounce(updateSearch)()
});
searchbar.addEventListener("keyup", function (e) {
if (firstMatch && e.key === "Enter")
firstMatch.click()
});

View File

@ -314,7 +314,7 @@ $('#btn_transfer').click(function () {
if (!isNaN(source.note.balance)) {
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
if (newBalance <= -5000) {
if (newBalance <= -2000) {
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
reset()

View File

@ -159,11 +159,11 @@ class TrustTable(tables.Table):
template_name = 'django_tables2/bootstrap4.html'
show_header = False
trusted = tables.Column(attrs={'td': {'class': 'text_center'}})
trusted = tables.Column(attrs={'td': {'class': 'text-center'}})
delete_col = tables.TemplateColumn(
template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')},
extra_context={"delete_trans": _('Delete')},
attrs={
'td': {
'class': lambda record: 'col-sm-1'
@ -173,6 +173,46 @@ class TrustTable(tables.Table):
verbose_name=_("Delete"),)
class TrustedTable(tables.Table):
class Meta:
attrs = {
'class': 'table table condensed table-striped',
'id': 'trusted_table'
}
Model = Trust
fields = ("trusting",)
template_name = "django_tables2/bootstrap4.html"
show_header = False
trusting = tables.Column(attrs={
'td': {'class': 'text-center', 'width': '100%'}})
trust_back = tables.Column(
verbose_name=_("Trust back"),
accessor="pk",
attrs={
'td': {
'class': '',
'id': lambda record: "trust_back_" + str(record.pk),
}
},
)
def render_trust_back(self, record):
user_note = record.trusted
trusting_note = record.trusting
if Trust.objects.filter(trusted=trusting_note, trusting=user_note):
return ""
val = '<button id="'
val += str(record.pk)
val += '" class="btn btn-success btn-sm text-nowrap" \
onclick="create_trust(' + str(record.trusted.pk) + ',' + \
str(record.trusting.pk) + ')">'
val += str(_("Add back"))
val += '</button>'
return mark_safe(val)
class AliasTable(tables.Table):
class Meta:
attrs = {

View File

@ -103,6 +103,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
</a>
</li>
{% endfor %}
<li class="nav-item">
<a class="nav-link font-weight-bold" data-toggle="tab" href="#search">
{% trans "Search" %}
</a>
</li>
</ul>
</div>
@ -123,6 +128,20 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
</div>
{% endfor %}
<div class="tab-pane" id="search">
<input class="form-control mx-auto d-block mb-3"
placeholder="{% trans "Search button..." %}" type="search" id="search-input"/>
<div class="d-inline-flex flex-wrap justify-content-center" id="search-results">
{% for button in all_buttons %}
{% if button.display %}
<button class="btn btn-outline-dark rounded-0 flex-fill" hidden
id="search_button{{ button.id }}" name="button" value="{{ button.name }}">
{{ button.name }} ({{ button.amount | pretty_money }})
</button>
{% endif %}
{% endfor %}
</div>
</div>
</div>
</div>
@ -163,7 +182,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<script type="text/javascript">
{% for button in highlighted %}
{% if button.display %}
$("#highlighted_button{{ button.id }}").click(function() {
document.getElementById("highlighted_button{{ button.id }}").addEventListener("click", function() {
addConso({{ button.destination_id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
{{ button.id }}, "{{ button.name|escapejs }}");
@ -174,7 +193,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% for category in categories %}
{% for button in category.templates_filtered %}
{% if button.display %}
$("#button{{ button.id }}").click(function() {
document.getElementById("button{{ button.id }}").addEventListener("click", function() {
addConso({{ button.destination_id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
{{ button.id }}, "{{ button.name|escapejs }}");
@ -182,5 +201,15 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
{% endfor %}
{% endfor %}
{% for button in all_buttons %}
{% if button.display %}
document.getElementById("search_button{{ button.id }}").addEventListener("click", function() {
addConso({{ button.destination_id }}, {{ button.amount }},
{{ polymorphic_ctype }}, {{ button.category_id }}, "{{ button.category.name|escapejs }}",
{{ button.id }}, "{{ button.name|escapejs }}");
});
{% endif %}
{% endfor %}
</script>
{% endblock %}

View File

@ -10,12 +10,12 @@ from django.core.exceptions import PermissionDenied
from django.db.models import Q, F
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, UpdateView, DetailView
from django_tables2 import SingleTableView
from django.urls import reverse_lazy
from django_tables2 import SingleTableView
from activity.models import Entry
from note_kfet.inputs import AmountInput
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from note_kfet.inputs import AmountInput
from .forms import TransactionTemplateForm, SearchTransactionForm
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial, Note
@ -190,6 +190,10 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
).order_by('name').all()
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
context['all_buttons'] = TransactionTemplate.objects.filter(
PermissionBackend.filter_queryset(self.request, TransactionTemplate, "view")
).filter(display=True).order_by('name').all()
return context

View File

@ -198,6 +198,41 @@ class PermissionBackend(ModelBackend):
def has_module_perms(self, user_obj, app_label):
return False
@staticmethod
@memoize
def has_model_perm(request, model, type):
"""
Check is the given user has the permission over a given model for a given action.
The result is then memoized.
:param request: The current request
:param model: The model that the permissions shoud apply
:param type: The type of the permissions: view, change, add or delete
For view action, it is consider possible if user can view or change the model
"""
# Requested by a shell
if request is None:
return False
user_obj = request.user
sess = request.session
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
# OAuth2 Authentication
user_obj = request.auth.user
if user_obj is None or user_obj.is_anonymous:
return False
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
return True
ct = ContentType.objects.get_for_model(model)
if any(PermissionBackend.permissions(request, ct, type)):
return True
if type == "view" and any(PermissionBackend.permissions(request, ct, "change")):
return True
return False
def get_all_permissions(self, user_obj, obj=None):
ct = ContentType.objects.get_for_model(obj)
return list(self.permissions(get_current_request(), ct, "view"))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.28 on 2023-07-24 10:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('permission', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='role',
name='for_club',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, to='member.Club', verbose_name='for club'),
),
]

View File

@ -339,6 +339,7 @@ class Role(models.Model):
"member.Club",
verbose_name=_("for club"),
on_delete=models.PROTECT,
blank=True,
null=True,
default=None,
)

View File

@ -5,6 +5,7 @@ from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from member.models import Club
from note.models import NoteSpecial, Alias
from note_kfet.inputs import AmountInput
@ -44,14 +45,14 @@ class SignUpForm(UserCreationForm):
fields = ('first_name', 'last_name', 'username', 'email', )
class DeclareSogeAccountOpenedForm(forms.Form):
soge_account = forms.BooleanField(
label=_("I declare that I opened or I will open soon a bank account in the Société générale with the BDE "
"partnership."),
help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
"account, you will have to pay the BDE membership."),
required=False,
)
# class DeclareSogeAccountOpenedForm(forms.Form):
# soge_account = forms.BooleanField(
# label=_("I declare that I opened or I will open soon a bank account in the Société générale with the BDE "
# "partnership."),
# help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
# "account, you will have to pay the BDE membership."),
# required=False,
# )
class WEISignupForm(forms.Form):
@ -67,11 +68,11 @@ class ValidationForm(forms.Form):
"""
Validate the inscription of the new users and pay memberships.
"""
soge = forms.BooleanField(
label=_("Inscription paid by Société Générale"),
required=False,
help_text=_("Check this case if the Société Générale paid the inscription."),
)
# soge = forms.BooleanField(
# label=_("Inscription paid by Société Générale"),
# required=False,
# help_text=_("Check this case if the Société Générale paid the inscription."),
# )
credit_type = forms.ModelChoiceField(
queryset=NoteSpecial.objects,
@ -114,3 +115,12 @@ class ValidationForm(forms.Form):
required=False,
initial=True,
)
# If the bda exists
if Club.objects.filter(name__iexact="bda").exists():
# The user can join the bda club at the inscription
join_bda = forms.BooleanField(
label=_("Join BDA Club"),
required=False,
initial=True,
)

View File

@ -57,11 +57,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<h4> {% trans "Validate account" %}</h4>
</div>
{% comment "Soge not for membership (only WEI)" %}
{% if declare_soge_account %}
<div class="alert alert-info">
{% trans "The user declared that he/she opened a bank account in the Société générale." %}
</div>
{% endif %}
{% endcomment %}
<div class="card-body" id="profile_infos">
{% csrf_token %}
@ -76,6 +78,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
{% endblock %}
{% comment "Soge not for membership (only WEI)" %}
{% block extrajavascript %}
<script>
soge_field = $("#id_soge");
@ -118,3 +121,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
</script>
{% endblock %}
{% endcomment %}

View File

@ -48,6 +48,7 @@ class TestSignup(TestCase):
ml_events_registration="en",
ml_sport_registration=True,
ml_art_registration=True,
VSS_charter_read=True
))
self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200)
self.assertTrue(User.objects.filter(username="toto").exists())
@ -105,6 +106,7 @@ class TestSignup(TestCase):
ml_events_registration="en",
ml_sport_registration=True,
ml_art_registration=True,
VSS_charter_read=True
))
self.assertTrue(response.status_code, 200)
@ -124,6 +126,7 @@ class TestSignup(TestCase):
ml_events_registration="en",
ml_sport_registration=True,
ml_art_registration=True,
VSS_charter_read=True
))
self.assertTrue(response.status_code, 200)
@ -143,6 +146,27 @@ class TestSignup(TestCase):
ml_events_registration="en",
ml_sport_registration=True,
ml_art_registration=True,
VSS_charter_read=True
))
self.assertTrue(response.status_code, 200)
# The VSS charter is not read
response = self.client.post(reverse("registration:signup"), dict(
first_name="Toto",
last_name="TOTO",
username="Ihaveanotherusername",
email="othertoto@example.com",
password1="toto1234",
password2="toto1234",
phone_number="+33123456789",
department="EXT",
promotion=Club.objects.get(name="BDE").membership_start.year,
address="Earth",
paid=False,
ml_events_registration="en",
ml_sport_registration=True,
ml_art_registration=True,
VSS_charter_read=False
))
self.assertTrue(response.status_code, 200)
@ -190,7 +214,7 @@ class TestValidateRegistration(TestCase):
# BDE Membership is mandatory
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
soge=False,
# soge=False,
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
credit_amount=4200,
last_name="TOTO",
@ -204,7 +228,7 @@ class TestValidateRegistration(TestCase):
# Same
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
soge=False,
# soge=False,
credit_type="",
credit_amount=0,
last_name="TOTO",
@ -218,7 +242,7 @@ class TestValidateRegistration(TestCase):
# The BDE membership is not free
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
soge=False,
# soge=False,
credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
credit_amount=0,
last_name="TOTO",
@ -232,7 +256,7 @@ class TestValidateRegistration(TestCase):
# Last and first names are required for a credit
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
soge=False,
# soge=False,
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
credit_amount=4000,
last_name="",
@ -249,7 +273,7 @@ class TestValidateRegistration(TestCase):
self.user.username = "admïntoto"
self.user.save()
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
soge=False,
# soge=False,
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
credit_amount=500,
last_name="TOTO",
@ -275,7 +299,7 @@ class TestValidateRegistration(TestCase):
self.user.profile.save()
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
soge=False,
# soge=False,
credit_type=NoteSpecial.objects.get(special_type="Chèque").id,
credit_amount=500,
last_name="TOTO",
@ -290,6 +314,7 @@ class TestValidateRegistration(TestCase):
self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists())
self.assertFalse(Membership.objects.filter(club__name="Kfet", user=self.user).exists())
self.assertFalse(Membership.objects.filter(club__name__iexact="BDA", user=self.user).exists())
self.assertFalse(SogeCredit.objects.filter(user=self.user).exists())
self.assertEqual(Transaction.objects.filter(
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2)
@ -311,7 +336,7 @@ class TestValidateRegistration(TestCase):
self.user.profile.save()
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
soge=False,
# soge=False,
credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
credit_amount=4000,
last_name="TOTO",
@ -326,6 +351,7 @@ class TestValidateRegistration(TestCase):
self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists())
self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists())
self.assertFalse(Membership.objects.filter(club__name__iexact="BDA", user=self.user).exists())
self.assertFalse(SogeCredit.objects.filter(user=self.user).exists())
self.assertEqual(Transaction.objects.filter(
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
@ -333,42 +359,43 @@ class TestValidateRegistration(TestCase):
response = self.client.get(self.user.profile.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_validate_kfet_registration_with_soge(self):
"""
The user joins the BDE and the Kfet, but the membership is paid by the Société générale.
"""
response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,)))
self.assertEqual(response.status_code, 200)
response = self.client.get(self.user.profile.get_absolute_url())
self.assertEqual(response.status_code, 404)
self.user.profile.email_confirmed = True
self.user.profile.save()
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
soge=True,
credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
credit_amount=4000,
last_name="TOTO",
first_name="Toto",
bank="Société générale",
join_bde=True,
join_kfet=True,
))
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
self.user.profile.refresh_from_db()
self.assertTrue(self.user.profile.registration_valid)
self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists())
self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists())
self.assertTrue(SogeCredit.objects.filter(user=self.user).exists())
self.assertEqual(Transaction.objects.filter(
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
self.assertFalse(Transaction.objects.filter(valid=True).exists())
response = self.client.get(self.user.profile.get_absolute_url())
self.assertEqual(response.status_code, 200)
# def test_validate_kfet_registration_with_soge(self):
# """
# The user joins the BDE and the Kfet, but the membership is paid by the Société générale.
# """
# response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,)))
# self.assertEqual(response.status_code, 200)
#
# response = self.client.get(self.user.profile.get_absolute_url())
# self.assertEqual(response.status_code, 404)
#
# self.user.profile.email_confirmed = True
# self.user.profile.save()
#
# response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
# soge=True,
# credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
# credit_amount=4000,
# last_name="TOTO",
# first_name="Toto",
# bank="Société générale",
# join_bde=True,
# join_kfet=True,
# ))
# self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
# self.user.profile.refresh_from_db()
# self.assertTrue(self.user.profile.registration_valid)
# self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
# self.assertTrue(Membership.objects.filter(club__name="BDE", user=self.user).exists())
# self.assertTrue(Membership.objects.filter(club__name="Kfet", user=self.user).exists())
# self.assertFalse(Membership.objects.filter(club__name__iexact="BDA", user=self.user).exists())
# self.assertTrue(SogeCredit.objects.filter(user=self.user).exists())
# self.assertEqual(Transaction.objects.filter(
# Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
# self.assertFalse(Transaction.objects.filter(valid=True).exists())
#
# response = self.client.get(self.user.profile.get_absolute_url())
# self.assertEqual(response.status_code, 200)
def test_invalidate_registration(self):
"""

View File

@ -24,7 +24,8 @@ from permission.models import Role
from permission.views import ProtectQuerysetMixin
from treasury.models import SogeCredit
from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm
# from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm
from .forms import SignUpForm, ValidationForm
from .tables import FutureUserTable
from .tokens import email_validation_token
@ -42,7 +43,7 @@ class UserCreateView(CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form(self.request.POST if self.request.POST else None)
context["soge_form"] = DeclareSogeAccountOpenedForm(self.request.POST if self.request.POST else None)
# context["soge_form"] = DeclareSogeAccountOpenedForm(self.request.POST if self.request.POST else None)
del context["profile_form"].fields["section"]
del context["profile_form"].fields["report_frequency"]
del context["profile_form"].fields["last_report"]
@ -75,12 +76,12 @@ class UserCreateView(CreateView):
user.profile.send_email_validation_link()
soge_form = DeclareSogeAccountOpenedForm(self.request.POST)
if "soge_account" in soge_form.data and soge_form.data["soge_account"]:
# If the user declares that a bank account got opened, prepare the soge credit to warn treasurers
soge_credit = SogeCredit(user=user)
soge_credit._force_save = True
soge_credit.save()
# soge_form = DeclareSogeAccountOpenedForm(self.request.POST)
# if "soge_account" in soge_form.data and soge_form.data["soge_account"]:
# # If the user declares that a bank account got opened, prepare the soge credit to warn treasurers
# soge_credit = SogeCredit(user=user)
# soge_credit._force_save = True
# soge_credit.save()
return super().form_valid(form)
@ -237,9 +238,12 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
kfet = Club.objects.get(name="Kfet")
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
if Club.objects.filter(name__iexact="BDA").exists():
bda = Club.objects.get(name__iexact="BDA")
fee += bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
# ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
return ctx
@ -262,8 +266,13 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
form.add_error(None, _("An alias with a similar name already exists."))
return self.form_invalid(form)
# Check if BDA exist to propose membership at regisration
bda_exists = False
if Club.objects.filter(name__iexact="BDA").exists():
bda_exists = True
# Get form data
soge = form.cleaned_data["soge"]
# soge = form.cleaned_data["soge"]
credit_type = form.cleaned_data["credit_type"]
credit_amount = form.cleaned_data["credit_amount"]
last_name = form.cleaned_data["last_name"]
@ -271,11 +280,13 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
bank = form.cleaned_data["bank"]
join_bde = form.cleaned_data["join_bde"]
join_kfet = form.cleaned_data["join_kfet"]
if bda_exists:
join_bda = form.cleaned_data["join_bda"]
if soge:
# If Société Générale pays the inscription, the user automatically joins the two clubs.
join_bde = True
join_kfet = True
# if soge:
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
# join_bde = True
# join_kfet = True
if not join_bde:
# This software belongs to the BDE.
@ -292,15 +303,21 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
# Add extra fee for the full membership
fee += kfet_fee if join_kfet else 0
if bda_exists:
bda = Club.objects.get(name__iexact="BDA")
bda_fee = bda.membership_fee_paid if user.profile.paid else bda.membership_fee_unpaid
# Add extra fee for the bda membership
fee += bda_fee if join_bda else 0
# If the bank pays, then we don't credit now. Treasurers will validate the transaction
# and credit the note later.
credit_type = None if soge else credit_type
# # If the bank pays, then we don't credit now. Treasurers will validate the transaction
# # and credit the note later.
# credit_type = None if soge else credit_type
# If the user does not select any payment method, then no credit will be performed.
credit_amount = 0 if credit_type is None else credit_amount
if fee > credit_amount and not soge:
# if fee > credit_amount and not soge:
if fee > credit_amount:
# Check if the user credits enough money
form.add_error('credit_type',
_("The entered amount is not enough for the memberships, should be at least {}")
@ -320,12 +337,12 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
user.profile.save()
user.refresh_from_db()
if not soge and SogeCredit.objects.filter(user=user).exists():
# If the user declared that a bank account was opened but in the validation form the SoGé case was
# unchecked, delete the associated credit
soge_credit = SogeCredit.objects.get(user=user)
soge_credit._force_delete = True
soge_credit.delete()
# if not soge and SogeCredit.objects.filter(user=user).exists():
# # If the user declared that a bank account was opened but in the validation form the SoGé case was
# # unchecked, delete the associated credit
# soge_credit = SogeCredit.objects.get(user=user)
# soge_credit._force_delete = True
# soge_credit.delete()
if credit_type is not None and credit_amount > 0:
# Credit the note
@ -334,7 +351,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
destination=user.note,
quantity=1,
amount=credit_amount,
reason="Crédit " + ("Société générale" if soge else credit_type.special_type) + " (Inscription)",
reason="Crédit " + credit_type.special_type + " (Inscription)",
# reason="Crédit " + ("Société générale" if soge else credit_type.special_type) + " (Inscription)",
last_name=last_name,
first_name=first_name,
bank=bank,
@ -348,8 +366,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
user=user,
fee=bde_fee,
)
if soge:
membership._soge = True
# if soge:
# membership._soge = True
membership.save()
membership.refresh_from_db()
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
@ -362,17 +380,29 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
user=user,
fee=kfet_fee,
)
if soge:
membership._soge = True
# if soge:
# membership._soge = True
membership.save()
membership.refresh_from_db()
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
membership.save()
if soge:
soge_credit = SogeCredit.objects.get(user=user)
# Update the credit transaction amount
soge_credit.save()
if bda_exists and join_bda:
# Create membership for the user to the BDA starting today
membership = Membership(
club=bda,
user=user,
fee=bda_fee,
)
membership.save()
membership.refresh_from_db()
membership.roles.add(Role.objects.get(name="Membre de club"))
membership.save()
# if soge:
# soge_credit = SogeCredit.objects.get(user=user)
# # Update the credit transaction amount
# soge_credit.save()
return ret

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

@ -1,6 +1,5 @@
# 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 datetime
from datetime import date
from django.conf import settings
@ -12,7 +11,8 @@ from django.db.models import Q
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import Club, Membership
# from member.models import Club, Membership # Club unused because of disabled soge
from member.models import Membership
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
@ -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
@ -324,23 +326,23 @@ class SogeCredit(models.Model):
if self.valid or not self.pk:
return
bde = Club.objects.get(name="BDE")
kfet = Club.objects.get(name="Kfet")
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)
# Soge do not pay BDE and kfet memberships since 2022
# bde = Club.objects.get(name="BDE")
# kfet = Club.objects.get(name="Kfet")
# 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)
## 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 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
@ -386,7 +388,6 @@ class SogeCredit(models.Model):
for tr in self.transactions.all():
tr.valid = True
tr._force_save = True
tr.created_at = timezone.now()
tr.save()
@transaction.atomic
@ -435,12 +436,11 @@ class SogeCredit(models.Model):
for tr in self.transactions.all():
tr._force_save = True
tr.valid = True
tr.created_at = timezone.now()
tr.save()
if self.credit_transaction:
# If the soge credit is deleted while the user is not validated yet,
# there is not credit transaction.
# There is a credit transaction iff the user declares that no bank account
# There is a credit transaction if the user declares that no bank account
# was opened after the validation of the account.
self.credit_transaction.valid = False
self.credit_transaction.reason += " (invalide)"

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

@ -385,8 +385,7 @@ class TestSogeCredits(TestCase):
response = self.client.post(reverse("treasury:manage_soge_credit", args=(soge_credit.pk,)),
data=dict(delete=True))
# 403 because no SogeCredit exists anymore, then a PermissionDenied is raised
self.assertRedirects(response, reverse("treasury:soge_credits"), 302, 403)
self.assertRedirects(response, reverse("treasury:soge_credits"), 302, 200)
self.assertFalse(SogeCredit.objects.filter(pk=soge_credit.pk))
self.user.note.refresh_from_db()
self.assertEqual(self.user.note.balance, 0)

View File

@ -101,14 +101,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
if not request.user.is_authenticated:
return self.handle_no_permission()
sample_invoice = Invoice(
id=0,
object="",
description="",
name="",
address="",
)
if not PermissionBackend.check_perm(self.request, "treasury.add_invoice", sample_invoice):
if not PermissionBackend.has_model_perm(self.request, Invoice(), "view"):
raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs)
@ -278,11 +271,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
if not request.user.is_authenticated:
return self.handle_no_permission()
sample_remittance = Remittance(
remittance_type_id=1,
comment="",
)
if not PermissionBackend.check_perm(self.request, "treasury.add_remittance", sample_remittance):
if not PermissionBackend.has_model_perm(self.request, Remittance(), "view"):
raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs)
@ -408,7 +397,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
if not request.user.is_authenticated:
return self.handle_no_permission()
if not super().get_queryset().exists():
if not PermissionBackend.has_model_perm(self.request, SogeCredit(), "view"):
raise PermissionDenied(_("You are not able to see the treasury interface."))
return super().dispatch(request, *args, **kwargs)

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
from django import forms
@ -38,7 +38,7 @@ class WEIRegistrationForm(forms.ModelForm):
class Meta:
model = WEIRegistration
exclude = ('wei', )
exclude = ('wei', 'clothing_cut')
widgets = {
"user": Autocomplete(
User,

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

@ -1,4 +1,4 @@
# Copyright (C) 2018-2022 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import time
@ -14,17 +14,17 @@ from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInf
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'
'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,391 @@
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from functools import lru_cache
from django import forms
from django.db import transaction
from django.db.models import Q
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
from ...models import WEIMembership
WORDS = {
"ambiance": ["Ambiance de bus :", {
1: "Ambiance calme et posée",
2: "Ambiance rigolage entre copaing",
3: "Ambiance danse de camping autour d'une piscine inexistante",
4: "Grosse soirée avec de la musique qui fait bouger",
5: "On retourne le camping et le bus (dans le respect et le savoir vivre)"
}],
"musique": ["Musique :", {
1: "Musique tranquille",
2: "Musique commerciale",
3: "Chansons paillardes",
4: "Musique de Colonie de vacances",
5: "Grosse techno"
}],
"boisson": ["Boissons :", {
1: "Boisson soft",
2: "Des cocktails de temps en temps",
3: "Des coktails fancy de pétasse (parce que c'est les meilleurs)",
4: "Bière !",
5: "L'alcool c'est dans les céréales"
}],
"beauferie": ["Échelle de la beauferie :", {
1: "Je suis toujours classe",
2: "Je rote de temps en temps",
3: "Claquette chaussette, c'est confortable",
4: "L'aviron bayonnais est dans ma plyaylist",
5: "Je suis champion⋅ne de concours de rots et d'éclatage de gobelet sur mon front"
}],
"sommeil": ["Échelle de ton sommeil pendant le WEI :", {
1: "Dormir, c'est pour les faibles",
2: "5h maximum",
3: "10h",
4: "15h",
5: "Deux bonnes nuits de sommeil, c'est important pour être en forme pour les activités proposées par nos supers GC WEI"
}],
"vacances": ["Tes vacances de rêve :", {
1: "Dans ma chambre",
2: "Retourner chez popa et moman pour pouvoir enfin arrêter de manger des pasta box",
3: "Être une grosse larve sous le soleil des troopiiiiiiiiques",
4: "Faire un road trip camping sauvage, manger des racines et boire son pipi",
5: "Le crime ne prend pas de vacances"
}],
"activite": ["T'as une heure de trou pendant ton WEI, que fais-tu ?", {
1: "Je cherche des copaines pour faire un petit jeu de société",
2: "Je cherche un moyen de me dépenser, n'importe quel ballon ferait l'affaire",
3: "Je cherche un endroit où il y a de la musique pour bouger sur le dancefloor",
4: "Petit apéro, petite pétanque avec les collègues autour d'un bon pastaga",
5: "Je cherche une connerie à faire (mais pas trop méchante, pour ne pas embêter mes GC WEI préférés)"
}],
"hygiene": ["Échelle de ton hygiène :", {
1: "La douche, c'eest tous les jours",
2: "La règle des 2 jours, c'est un droit et un devoir",
3: "Je ne me lave qu'après le sport",
4: "« Ne vous inquiétez pas, je pue pas »",
5: "Y a que les sales qui se lavent"
}],
"animal": ["Tu décrirais ton animal totem plutôt comme :", {
1: "Un dragon qui raserait des villes entières d'un seul souffle",
2: "Une mouette qui pique des frites aux dunkerquois",
3: "Un poulpe tout meunion",
4: "Un pitbull qui au fond cache un petit cœur en sucre",
5: "Un canard en plastique au bord d'une baignoire qui n'a pas servi depuis 10 ans"
}],
"fensfoire": ["Quel est ton rapport à la F[ENS]foire ?", {
1: "Je réveille les autres à 6h avec mon instrument",
2: "Je la suis partout",
3: "J'aime bien l'écouter de temps en temps",
4: "Je mets des bouchons d'oreilles pour ne pas l'entendre",
5: "La quoi ?"
}],
"kokarde": ["Qu'est-ce que le mot Kokarde t'évoque ?", {
1: "Vraiment pas mon truc les soirées…",
2: "Bof, je viens pour manger et je repars aussitôt",
3: "Je kiffe, good vibes",
4: "Perso, je ne m'arrêterai pas de danser sur la piste !",
5: "J'resterai jusqu'à 3h ou rien"
}],
"copain": ["Qu'est-ce que tu fais avec un⋅e «copain⋅ine» ?", {
1: "Je l'insulte de sale merde",
2: "J'lui fais faire des trucs cons et je l'affiche !",
3: "On parlerait ensemble et on se marrerait",
4: "On aurait des vrais gros délires",
5: "Je meurs pour lui/elle"
}],
"vie": ["Selon toi, qu'est-ce que la vie ?", {
1: "La vie, cette sale race !",
2: "Un moment paisible avant la mort",
3: "C'est difficile à définir...",
4: "En vrai, c'est cool !",
5: "Une gigantestque tranche de kiff ! Et tous les jours, j'en mange un morceau"
}],
"jeux": ["Quel est ton rapport avec les jeux de société ?", {
1: "éloigné",
2: "nonchalant",
3: "timide",
4: "assumé",
5: "sexuel"
}],
"calin": ["Qu'est-ce que tu penses des câlins ?", {
1: "Jamais je n'en fais et jamais je n'en ferai !",
2: "J'en fais mais ça ne me plaît pas",
3: "J'en fais rarement mais c'est toujours cool",
4: "J'en fais tous les jours avec mes ami⋅es !",
5: "Je pourrais en faire à n'importe qui. Pourquoi ne pas créer le club Câl[ENS] ?"
}],
"vomi": ["Quel est ton rapport au vomi ?", {
1: "C'est compliqué…",
2: "Jamais je ne vomis mais je nettoie quand mes potes vomissent",
3: "Jamais je ne vomis et jamais je ne nettoie celui de quelqu'un d'autre",
4: "Je vomis quelquefois, ça arrive, faites pas cette tête, mais je fins toujours par nettoyer !",
5: "Je vomis à chaque soirée et ce n'est jamais moi qui nettoie"
}],
"kfet": ["Qu'est ce que la Kfet t'évoque ?", {
1: "La Kfet, quel lieu de dépravé⋅es sérieux…",
2: "C'est un endroit à l'hygiène plus que douteuse…",
3: "Téma les prix des boissons et des snacks, c'est aberrant !",
4: "En vrai, c'est cool, petit billard, petit canapé, chill !",
5: "Banger, j'y reste jusqu'à la fin de mes jours"
}],
"fatigue": ["Comment combattre la fatigue lors de ton WEI ?", {
1: "Le sport en journée, ça réveille",
2: "Le sucre du coca, ça réveille",
3: "La taurine du Red Bull, ça réveille",
4: "L'alcool dans le sang, ça réveille",
5: "L'écocup sur le front, ça réveille"
}],
"duree trajet": ["Quelle serait ta durée de trajet préférée ?", {
1: "Trajet instantané, pas le temps de niaiser",
2: "1h, histoire de faire connaissance avec quelques personnes avant de se jeter sur les boissons",
3: "3h, on peut vraiment parler et apprendre à connaître nos voisin⋅es",
4: "6h, histoire d'avoir le temps de faire des conneries dans le bus pour bien se marrer !",
5: "12h, il faut bien trouver un moment pour dormir, ce seront deux gros dodos dans un bus"
}],
"scolarite": ["Comment tu vois ton cursus à l'ENS ?", {
1: "La tranquillité et le travail",
2: "On va s'amuser tout en bossant",
3: "Ça va profiter et réviser au dernier moment pour les exams…",
4: "Nous festoierons sans songer aux conséquences",
5: "Je ne vois qu'une seule issue : la débauche"
}]
}
class WEISurveyForm2023(forms.Form):
"""
Survey form for the year 2023.
Members answer 20 questions, from which we calculate the best associated bus.
"""
def set_registration(self, registration):
"""
Filter the bus selector with the buses of the current WEI.
"""
information = WEISurveyInformation2023(registration)
question = information.questions[information.step]
self.fields[question] = forms.ChoiceField(
label=WORDS[question][0],
widget=forms.RadioSelect(),
)
answers = [(answer, WORDS[question][1][answer]) for answer in WORDS[question][1]]
self.fields[question].choices = answers
class WEIBusInformation2023(WEIBusInformation):
"""
For each question, the bus has ordered answers
"""
scores: dict
def __init__(self, bus):
self.scores = {}
for question in WORDS:
self.scores[question] = []
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.
"""
step = 0
questions = list(WORDS.keys())
def __init__(self, registration):
for question in WORDS:
setattr(self, str(question), 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):
self.information.step += 1
for question in WORDS:
if question in form.cleaned_data:
answer = form.cleaned_data[question]
setattr(self.information, question, answer)
self.save()
@classmethod
def get_algorithm_class(cls):
return WEISurveyAlgorithm2023
def is_complete(self) -> bool:
"""
The survey is complete once the bus is chosen.
"""
for question in WORDS:
if not getattr(self.information, question):
return False
return True
@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 = 0
for question in WORDS:
s += bus_info.scores[question][str(getattr(self.information, question))]
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):
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,18 @@
# Generated by Django 2.2.28 on 2023-07-09 09:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0005_auto_20230128_1850'),
]
operations = [
migrations.AlterField(
model_name='weiregistration',
name='clothing_cut',
field=models.CharField(choices=[('male', 'Male'), ('female', 'Female'), ('unisex', 'Unisex')], default='unisex', max_length=16, verbose_name='clothing cut'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.28 on 2023-07-09 12:46
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wei', '0006_unisex_clothing_cut'),
]
operations = [
migrations.AlterField(
model_name='weiregistration',
name='emergency_contact_name',
field=models.CharField(help_text='The emergency contact must not be a WEI participant', max_length=255, verbose_name='emergency contact name'),
),
]

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 json
@ -209,7 +209,9 @@ class WEIRegistration(models.Model):
choices=(
('male', _("Male")),
('female', _("Female")),
('unisex', _("Unisex")),
),
default='unisex',
verbose_name=_("clothing cut"),
)
@ -235,6 +237,7 @@ class WEIRegistration(models.Model):
emergency_contact_name = models.CharField(
max_length=255,
verbose_name=_("emergency contact name"),
help_text=_("The emergency contact must not be a WEI participant")
)
emergency_contact_phone = PhoneNumberField(

View File

@ -56,7 +56,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<dd class="col-xl-6">{{ registration.get_gender_display }}</dd>
<dt class="col-xl-6">{% trans 'clothing cut'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.clothing_cut }}</dd>
<dd class="col-xl-6">{{ registration.get_clothing_cut_display }}</dd>
<dt class="col-xl-6">{% trans 'clothing size'|capfirst %}</dt>
<dd class="col-xl-6">{{ registration.clothing_size }}</dd>

View File

@ -0,0 +1,170 @@
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import random
from datetime import date, timedelta
from django.contrib.auth.models import User
from django.test import TestCase
from django.urls import reverse
from note.models import NoteUser
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.user = User.objects.create_superuser(
username="weiadmin",
password="admin",
email="admin@example.com",
)
self.user.save()
self.client.force_login(self.user)
sess = self.client.session
sess["permission_mask"] = 42
sess.save()
self.wei = WEIClub.objects.create(
name="WEI 2023",
email="wei2023@example.com",
parent_club_id=2,
membership_fee_paid=12500,
membership_fee_unpaid=5500,
membership_start='2023-01-01',
membership_end='2023-12-31',
date_start=date.today() + timedelta(days=2),
date_end='2023-12-31',
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 question in WORDS:
information.scores[question] = {answer: random.randint(1, 5) for answer in WORDS[question][1]}
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 question in WORDS:
setattr(information, question, random.randint(1, 5))
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 question in WORDS:
setattr(information, question, random.randint(1, 5))
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 %
def test_register_1a(self):
"""
Test register a first year member to the WEI and complete the survey
"""
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
self.assertEqual(response.status_code, 200)
user = User.objects.create(username="toto", email="toto@example.com")
NoteUser.objects.create(user=user)
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
user=user.id,
soge_credit=True,
birth_date=date(2000, 1, 1),
gender='nonbinary',
clothing_cut='female',
clothing_size='XS',
health_issues='I am a bot',
emergency_contact_name='NoteKfet2020',
emergency_contact_phone='+33123456789',
))
qs = WEIRegistration.objects.filter(user_id=user.id)
self.assertTrue(qs.exists())
registration = qs.get()
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
for question in WORDS:
# Fill 1A Survey, 20 pages
# be careful if questionnary form change (number of page, type of answer...)
response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), {
question: "1"
})
registration.refresh_from_db()
survey = WEISurvey2023(registration)
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302,
302 if survey.is_complete() else 200)
self.assertIsNotNone(getattr(survey.information, question), "Survey page " + question + " failed")
survey = WEISurvey2023(registration)
self.assertTrue(survey.is_complete())
survey.select_bus(self.buses[0])
survey.save()
self.assertIsNotNone(survey.information.get_selected_bus())

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
@ -380,7 +380,7 @@ class TestWEIRegistration(TestCase):
def test_register_1a(self):
"""
Test register a first year member to the WEI and complete the survey.
Test register a first year member to the WEI.
"""
response = self.client.get(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)))
self.assertEqual(response.status_code, 200)
@ -402,21 +402,6 @@ class TestWEIRegistration(TestCase):
self.assertTrue(qs.exists())
registration = qs.get()
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302, 200)
for i in range(1, 21):
# Fill 1A Survey, 20 pages
response = self.client.post(reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), dict(
word="Jus de fruit",
))
registration.refresh_from_db()
survey = CurrentSurvey(registration)
self.assertRedirects(response, reverse("wei:wei_survey", kwargs=dict(pk=registration.pk)), 302,
302 if survey.is_complete() else 200)
self.assertIsNotNone(getattr(survey.information, "word" + str(i)), "Survey page #" + str(i) + " failed")
survey = CurrentSurvey(registration)
self.assertTrue(survey.is_complete())
survey.select_bus(self.bus)
survey.save()
self.assertIsNotNone(survey.information.get_selected_bus())
# Check that the user can't be registered twice
response = self.client.post(reverse("wei:wei_register_1A", kwargs=dict(wei_pk=self.wei.pk)), dict(
@ -662,7 +647,7 @@ class TestWEIRegistration(TestCase):
first_name="admin",
bank="Société générale",
))
self.assertRedirects(response, reverse("wei:wei_detail", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
self.assertRedirects(response, reverse("wei:wei_registrations", kwargs=dict(pk=self.registration.wei.pk)), 302, 200)
# Check if the membership is successfully created
membership = WEIMembership.objects.filter(user_id=self.user.id, club=self.wei)
self.assertTrue(membership.exists())
@ -782,7 +767,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):

View File

@ -969,7 +969,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
if not registration.soge_credit and user.note.balance + credit_amount < fee:
# Users must have money before registering to the WEI.
form.add_error('bus',
form.add_error('credit_type',
_("This user don't have enough money to join this club, and can't have a negative balance."))
return super().form_invalid(form)
@ -1014,7 +1014,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, ProtectedCreateView):
def get_success_url(self):
self.object.refresh_from_db()
return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.club.pk})
return reverse_lazy("wei:wei_registrations", kwargs={"pk": self.object.club.pk})
class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
@ -1084,7 +1084,44 @@ class WEISurveyEndView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
club = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
context["club"] = club
random_user = User.objects.filter(~Q(wei__wei__in=[club])).first()
if random_user is None:
# This case occurs when all users are registered to the WEI.
# Don't worry, Pikachu never went to the WEI.
# This bug can arrive only in dev mode.
context["can_add_first_year_member"] = True
context["can_add_any_member"] = True
else:
# Check if the user has the right to create a registration of a random first year member.
empty_fy_registration = WEIRegistration(
wei=club,
user=random_user,
first_year=True,
birth_date="1970-01-01",
gender="No",
emergency_contact_name="No",
emergency_contact_phone="No",
)
context["can_add_first_year_member"] = PermissionBackend \
.check_perm(self.request, "wei.add_weiregistration", empty_fy_registration)
# Check if the user has the right to create a registration of a random old member.
empty_old_registration = WEIRegistration(
wei=club,
user=User.objects.filter(~Q(wei__wei__in=[club])).first(),
first_year=False,
birth_date="1970-01-01",
gender="No",
emergency_contact_name="No",
emergency_contact_phone="No",
)
context["can_add_any_member"] = PermissionBackend \
.check_perm(self.request, "wei.add_weiregistration", empty_old_registration)
return context

View File

@ -448,6 +448,10 @@ Options
"value": "female",
"display_name": "Femme"
}
{
"value": "unisex",
"display_name": "Unisexe"
},
]
},
"clothing_size": {

View File

@ -118,13 +118,13 @@ Exemples
{"F": [
"ADD",
["F", "source__balance"],
5000]
2000]
}
}
]
| si la destination est la note du club dont on est membre et si le montant est inférieur au solde de la source + 50 €,
autrement dit le solde final est au-dessus de -50 €.
| si la destination est la note du club dont on est membre et si le montant est inférieur au solde de la source + 20 €,
autrement dit le solde final est au-dessus de -20 €.
Masques de permissions

View File

@ -83,13 +83,6 @@ Je suis trésorier d'un club, qu'ai-je le droit de faire ?
bien sûr permis pour faciliter des transferts. Tout abus de droits constaté
pourra mener à des sanctions prises par le bureau du BDE.
.. warning::
Une fonctionnalité pour permettre de gérer plus proprement les remboursements
entre amis est en cours de développement. Temporairement et pour des raisons
de confort, les trésoriers de clubs ont le droit de prélever n'importe quelle
adhérente vers n'importe quelle autre note adhérente, tant que la source ne
descend pas sous ``- 50 €``. Ces droits seront retirés d'ici quelques semaines.
Je suis trésorier d'un club, je n'arrive pas à voir le solde du club / faire des transactions
---------------------------------------------------------------------------------------------------

View File

@ -615,7 +615,7 @@ pas déjà fait par créer un utilisateur sur les deux serveurs :
.. code:: bash
ynerant@bde-note:~$ sudo -u postgres createuser -l ynerant
ynerant@bde-note:~$ sudo -u postgres createuser -s ynerant
On réinitialise **sur le serveur de développement** la base de données présente, en
éteignant tout d'abord le serveur Web :

File diff suppressed because it is too large Load Diff

View File

@ -7,11 +7,11 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-15 23:21+0100\n"
"POT-Creation-Date: 2022-10-07 09:07+0200\n"
"PO-Revision-Date: 2020-11-16 20:21+0000\n"
"Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n"
"Language-Team: German <http://translate.ynerant.fr/projects/nk20/nk20-js/de/>"
"\n"
"Language-Team: German <http://translate.ynerant.fr/projects/nk20/nk20-js/de/"
">\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@ -27,6 +27,22 @@ msgstr "Alias erfolgreich hinzugefügt"
msgid "Alias successfully deleted"
msgstr "Alias erfolgreich gelöscht"
#: apps/member/static/member/js/trust.js:14
msgid "You can't add yourself as a friend"
msgstr ""
#: apps/member/static/member/js/trust.js:37
#, fuzzy
#| msgid "Alias successfully added"
msgid "Friendship successfully added"
msgstr "Alias erfolgreich hinzugefügt"
#: apps/member/static/member/js/trust.js:53
#, fuzzy
#| msgid "Alias successfully deleted"
msgid "Friendship successfully deleted"
msgstr "Alias erfolgreich gelöscht"
#: apps/note/static/note/js/consos.js:225
#, javascript-format
msgid ""
@ -46,32 +62,32 @@ msgstr ""
"ist negativ."
#: apps/note/static/note/js/consos.js:232
#: apps/note/static/note/js/transfer.js:298
#: apps/note/static/note/js/transfer.js:401
#: apps/note/static/note/js/transfer.js:309
#: apps/note/static/note/js/transfer.js:412
#, javascript-format
msgid "Warning, the emitter note %s is no more a BDE member."
msgstr "Warnung, der Emittent Hinweis %s ist kein BDE-Mitglied mehr."
#: apps/note/static/note/js/consos.js:253
#: apps/note/static/note/js/consos.js:254
msgid "The transaction couldn't be validated because of insufficient balance."
msgstr ""
"Die Transaktion konnte aufgrund eines unzureichenden Saldos nicht validiert "
"werden."
#: apps/note/static/note/js/transfer.js:238
#: apps/note/static/note/js/transfer.js:249
msgid "This field is required and must contain a decimal positive number."
msgstr ""
"Dieses Feld ist erforderlich und muss eine positive Dezimalzahl enthalten."
#: apps/note/static/note/js/transfer.js:245
#: apps/note/static/note/js/transfer.js:256
msgid "The amount must stay under 21,474,836.47 €."
msgstr "Der Betrag muss unter 21.474.836,47 € bleiben."
#: apps/note/static/note/js/transfer.js:251
#: apps/note/static/note/js/transfer.js:262
msgid "This field is required."
msgstr "Dies ist ein Pflichtfeld."
#: apps/note/static/note/js/transfer.js:277
#: apps/note/static/note/js/transfer.js:288
#, javascript-format
msgid ""
"Warning: the transaction of %s from %s to %s was not made because it is the "
@ -80,12 +96,12 @@ msgstr ""
"Warnung: Die Transaktion von %s von %s nach %s wurde nicht durchgeführt, da "
"es sich um die gleiche Quell- und Zielnotiz handelt."
#: apps/note/static/note/js/transfer.js:301
#: apps/note/static/note/js/transfer.js:312
#, javascript-format
msgid "Warning, the destination note %s is no more a BDE member."
msgstr "Warnung, der Bestimmungsvermerk %s ist kein BDE-Mitglied mehr."
#: apps/note/static/note/js/transfer.js:307
#: apps/note/static/note/js/transfer.js:318
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
@ -94,7 +110,7 @@ msgstr ""
"Warnung, die Transaktion von %s von der Note %s zur Note %s gelingt, aber "
"die Emitternote %s ist sehr negativ."
#: apps/note/static/note/js/transfer.js:312
#: apps/note/static/note/js/transfer.js:323
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
@ -103,31 +119,32 @@ msgstr ""
"Warnung, die Transaktion von %s von der Note %s zur Note %s gelingt, aber "
"die Emitternote %s ist negativ."
#: apps/note/static/note/js/transfer.js:318
#: apps/note/static/note/js/transfer.js:329
#, javascript-format
msgid "Transfer of %s from %s to %s succeed!"
msgstr "Übertragung von %s von %s auf %s gelingt!"
#: apps/note/static/note/js/transfer.js:325
#: apps/note/static/note/js/transfer.js:346
#: apps/note/static/note/js/transfer.js:353
#: apps/note/static/note/js/transfer.js:336
#: apps/note/static/note/js/transfer.js:357
#: apps/note/static/note/js/transfer.js:364
#, javascript-format
msgid "Transfer of %s from %s to %s failed: %s"
msgstr "Übertragung von %s von %s auf %s fehlgeschlagen: %s"
#: apps/note/static/note/js/transfer.js:347
#: apps/note/static/note/js/transfer.js:358
msgid "insufficient funds"
msgstr "unzureichende Geldmittel"
#: apps/note/static/note/js/transfer.js:400
#: apps/note/static/note/js/transfer.js:411
msgid "Credit/debit succeed!"
msgstr "Kredit/Debit erfolgreich!"
#: apps/note/static/note/js/transfer.js:407
#: apps/note/static/note/js/transfer.js:418
#, javascript-format
msgid "Credit/debit failed: %s"
msgstr "Kredit/Debit fehlgeschlagen: %s"
#: note_kfet/static/js/base.js:366
#: note_kfet/static/js/base.js:370
msgid "An error occured while (in)validating this transaction:"
msgstr "Bei der (Un-)Validierung dieser Transaktion ist ein Fehler aufgetreten:"
msgstr ""
"Bei der (Un-)Validierung dieser Transaktion ist ein Fehler aufgetreten:"

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: 2020-11-15 23:21+0100\n"
"PO-Revision-Date: 2020-11-21 12:23+0100\n"
"POT-Creation-Date: 2022-10-07 09:07+0200\n"
"PO-Revision-Date: 2022-10-07 13:20+0200\n"
"Last-Translator: elkmaennchen <elkmaennchen@crans.org>\n"
"Language-Team: \n"
"Language: es\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"
"Last-Translator: elkmaennchen <elkmaennchen@crans.org>\n"
"Language-Team: \n"
"X-Generator: Poedit 2.3\n"
"X-Generator: Poedit 3.0.1\n"
#: apps/member/static/member/js/alias.js:17
msgid "Alias successfully added"
@ -26,6 +26,18 @@ msgstr "Alias añadido con éxito"
msgid "Alias successfully deleted"
msgstr "Alias suprimido con éxito"
#: apps/member/static/member/js/trust.js:14
msgid "You can't add yourself as a friend"
msgstr "No puede añadir asimismo como amig@"
#: apps/member/static/member/js/trust.js:37
msgid "Friendship successfully added"
msgstr "Amig@ añadido con éxito"
#: apps/member/static/member/js/trust.js:53
msgid "Friendship successfully deleted"
msgstr "Amig@ suprimido con éxito"
#: apps/note/static/note/js/consos.js:225
#, javascript-format
msgid ""
@ -44,30 +56,29 @@ msgstr ""
"Cuidado, la transacción de %s fue un éxito, pero la note %s está negativa."
#: apps/note/static/note/js/consos.js:232
#: apps/note/static/note/js/transfer.js:298
#: apps/note/static/note/js/transfer.js:401
#: apps/note/static/note/js/transfer.js:309
#: apps/note/static/note/js/transfer.js:412
#, javascript-format
msgid "Warning, the emitter note %s is no more a BDE member."
msgstr "Cuidado, la note remitente %s no está más miembro del BDE."
#: apps/note/static/note/js/consos.js:253
#: apps/note/static/note/js/consos.js:254
msgid "The transaction couldn't be validated because of insufficient balance."
msgstr ""
"La transacción no pudo ser validada por culpa de saldo demasiado bajo."
msgstr "La transacción no pudo ser validada por culpa de saldo demasiado bajo."
#: apps/note/static/note/js/transfer.js:238
#: apps/note/static/note/js/transfer.js:249
msgid "This field is required and must contain a decimal positive number."
msgstr "Este campo obligatorio requiere un número decimal positivo."
#: apps/note/static/note/js/transfer.js:245
#: apps/note/static/note/js/transfer.js:256
msgid "The amount must stay under 21,474,836.47 €."
msgstr "El monto no puede superar los 21 474 836,47 €."
#: apps/note/static/note/js/transfer.js:251
#: apps/note/static/note/js/transfer.js:262
msgid "This field is required."
msgstr "Este campo es obligatorio."
#: apps/note/static/note/js/transfer.js:277
#: apps/note/static/note/js/transfer.js:288
#, javascript-format
msgid ""
"Warning: the transaction of %s from %s to %s was not made because it is the "
@ -76,12 +87,12 @@ msgstr ""
"Cuidado : la transacción de %s de %s a %s no fue echa porque la fuente y el "
"destino son iguales."
#: apps/note/static/note/js/transfer.js:301
#: apps/note/static/note/js/transfer.js:312
#, javascript-format
msgid "Warning, the destination note %s is no more a BDE member."
msgstr "Cuidado, la note destino %s no está más miembro del BDE."
#: apps/note/static/note/js/transfer.js:307
#: apps/note/static/note/js/transfer.js:318
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
@ -90,7 +101,7 @@ msgstr ""
"Cuidado, la transacción de %s de la note %s a la note %s fue un éxito, pero "
"la note fuente %s está muy negativa."
#: apps/note/static/note/js/transfer.js:312
#: apps/note/static/note/js/transfer.js:323
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
@ -99,31 +110,31 @@ msgstr ""
"Cuidado, la transacción de %s de la note %s a la note %s fue un éxito, pero "
"la note fuente %s está negativa."
#: apps/note/static/note/js/transfer.js:318
#: apps/note/static/note/js/transfer.js:329
#, javascript-format
msgid "Transfer of %s from %s to %s succeed!"
msgstr "¡ La transacción de %s de %s a %s fue un éxito !"
#: apps/note/static/note/js/transfer.js:325
#: apps/note/static/note/js/transfer.js:346
#: apps/note/static/note/js/transfer.js:353
#: apps/note/static/note/js/transfer.js:336
#: apps/note/static/note/js/transfer.js:357
#: apps/note/static/note/js/transfer.js:364
#, javascript-format
msgid "Transfer of %s from %s to %s failed: %s"
msgstr "La transacción de %s de %s a %s fue un fracaso : %s"
#: apps/note/static/note/js/transfer.js:347
#: apps/note/static/note/js/transfer.js:358
msgid "insufficient funds"
msgstr "fundos insuficientes"
#: apps/note/static/note/js/transfer.js:400
#: apps/note/static/note/js/transfer.js:411
msgid "Credit/debit succeed!"
msgstr "¡ Crédito/débito tubo éxito !"
#: apps/note/static/note/js/transfer.js:407
#: apps/note/static/note/js/transfer.js:418
#, javascript-format
msgid "Credit/debit failed: %s"
msgstr "Crédito/débito falló : %s"
#: note_kfet/static/js/base.js:366
#: note_kfet/static/js/base.js:370
msgid "An error occured while (in)validating this transaction:"
msgstr "Un error ocurrió durante la (in)validación de esta transacción :"

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,11 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-11-15 23:21+0100\n"
"POT-Creation-Date: 2022-10-07 09:07+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -26,6 +25,18 @@ msgstr "Alias ajouté avec succès"
msgid "Alias successfully deleted"
msgstr "Alias supprimé avec succès"
#: apps/member/static/member/js/trust.js:14
msgid "You can't add yourself as a friend"
msgstr "Vous ne pouvez pas vous ajouter vous-même en ami"
#: apps/member/static/member/js/trust.js:37
msgid "Friendship successfully added"
msgstr "Amitié ajoutée avec succès"
#: apps/member/static/member/js/trust.js:53
msgid "Friendship successfully deleted"
msgstr "Amitié supprimée avec succès"
#: apps/note/static/note/js/consos.js:225
#, javascript-format
msgid ""
@ -45,31 +56,31 @@ msgstr ""
"la note émettrice %s est en négatif."
#: apps/note/static/note/js/consos.js:232
#: apps/note/static/note/js/transfer.js:298
#: apps/note/static/note/js/transfer.js:401
#: apps/note/static/note/js/transfer.js:309
#: apps/note/static/note/js/transfer.js:412
#, javascript-format
msgid "Warning, the emitter note %s is no more a BDE member."
msgstr "Attention, la note émettrice %s n'est plus adhérente."
#: apps/note/static/note/js/consos.js:253
#: apps/note/static/note/js/consos.js:254
msgid "The transaction couldn't be validated because of insufficient balance."
msgstr ""
"La transaction n'a pas pu être validée pour cause de solde insuffisant."
#: apps/note/static/note/js/transfer.js:238
#: apps/note/static/note/js/transfer.js:249
msgid "This field is required and must contain a decimal positive number."
msgstr ""
"Ce champ est requis et doit comporter un nombre décimal strictement positif."
#: apps/note/static/note/js/transfer.js:245
#: apps/note/static/note/js/transfer.js:256
msgid "The amount must stay under 21,474,836.47 €."
msgstr "Le montant ne doit pas excéder 21 474 836.47 €."
#: apps/note/static/note/js/transfer.js:251
#: apps/note/static/note/js/transfer.js:262
msgid "This field is required."
msgstr "Ce champ est requis."
#: apps/note/static/note/js/transfer.js:277
#: apps/note/static/note/js/transfer.js:288
#, javascript-format
msgid ""
"Warning: the transaction of %s from %s to %s was not made because it is the "
@ -78,12 +89,12 @@ msgstr ""
"Attention : la transaction de %s de la note %s vers la note %s n'a pas été "
"faite car il s'agit de la même note au départ et à l'arrivée."
#: apps/note/static/note/js/transfer.js:301
#: apps/note/static/note/js/transfer.js:312
#, javascript-format
msgid "Warning, the destination note %s is no more a BDE member."
msgstr "Attention, la note de destination %s n'est plus adhérente."
#: apps/note/static/note/js/transfer.js:307
#: apps/note/static/note/js/transfer.js:318
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
@ -92,7 +103,7 @@ msgstr ""
"Attention, La transaction de %s depuis la note %s vers la note %s a été "
"réalisée avec succès, mais la note émettrice %s est en négatif sévère."
#: apps/note/static/note/js/transfer.js:312
#: apps/note/static/note/js/transfer.js:323
#, javascript-format
msgid ""
"Warning, the transaction of %s from the note %s to the note %s succeed, but "
@ -101,33 +112,33 @@ msgstr ""
"Attention, La transaction de %s depuis la note %s vers la note %s a été "
"réalisée avec succès, mais la note émettrice %s est en négatif."
#: apps/note/static/note/js/transfer.js:318
#: apps/note/static/note/js/transfer.js:329
#, javascript-format
msgid "Transfer of %s from %s to %s succeed!"
msgstr ""
"Le transfert de %s de la note %s vers la note %s a été fait avec succès !"
#: apps/note/static/note/js/transfer.js:325
#: apps/note/static/note/js/transfer.js:346
#: apps/note/static/note/js/transfer.js:353
#: apps/note/static/note/js/transfer.js:336
#: apps/note/static/note/js/transfer.js:357
#: apps/note/static/note/js/transfer.js:364
#, javascript-format
msgid "Transfer of %s from %s to %s failed: %s"
msgstr "Le transfert de %s de la note %s vers la note %s a échoué : %s"
#: apps/note/static/note/js/transfer.js:347
#: apps/note/static/note/js/transfer.js:358
msgid "insufficient funds"
msgstr "solde insuffisant"
#: apps/note/static/note/js/transfer.js:400
#: apps/note/static/note/js/transfer.js:411
msgid "Credit/debit succeed!"
msgstr "Le crédit/retrait a bien été effectué !"
#: apps/note/static/note/js/transfer.js:407
#: apps/note/static/note/js/transfer.js:418
#, javascript-format
msgid "Credit/debit failed: %s"
msgstr "Le crédit/retrait a échoué : %s"
#: note_kfet/static/js/base.js:366
#: note_kfet/static/js/base.js:370
msgid "An error occured while (in)validating this transaction:"
msgstr ""
"Une erreur est survenue lors de la validation/dévalidation de cette "

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

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

@ -67,7 +67,8 @@ mark {
.bg-primary {
/* background-color: rgb(18, 67, 4) !important; */
/* MODE VIEUXCON=ON */
background-color: rgb(0, 119, 139) !important;
/* background-color: rgb(166, 0, 2) !important; */
background-color: rgb(0, 0, 0) !important;
}
html {
@ -82,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(0, 119, 139);
border-color: rgb(0, 119, 139);
color: rgb(241, 229, 52);
background-color: rgb(228, 35, 132);
border-color: rgb(228, 35, 132);
}
.btn-outline-primary {
color: rgb(0, 119, 139);
background-color: rgba(248, 249, 250, 0.9);
border-color: rgb(0, 119, 139);
color: #fff;
background-color: #000;
border-color: #464647;
}
.turbolinks-progress-bar {
@ -100,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(0, 119, 139);
border-color: rgb(0, 119, 139);
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(0, 119, 139);
border-color: rgb(0, 119, 139);
color: #fff;
background-color: #000;
border-color: #adb5bd;
}
.border-primary {
border-color: rgb(0,85, 102) !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(0, 119, 139);
color: rgb(228, 35, 132);
}
a:hover {
color: rgb(0, 171, 205);
color: rgb(228, 35, 132);
}
.form-control:focus {
box-shadow: 0 0 0 0.25rem rgba(0, 171, 205, 0.25);
border-color: rgb(0, 171, 205);
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(0, 171, 205, 0.5);
box-shadow: 0 0 0 0.25rem rgb(228 35 132 / 10%);
}

View File

@ -1,3 +1,5 @@
const keycodes = [32, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 106, 107, 109, 110, 111, 186, 187, 188, 189, 190, 191, 219, 220, 221, 222]
$(document).ready(function () {
$('.autocomplete').keyup(function (e) {
const target = $('#' + e.target.id)
@ -10,7 +12,6 @@ $(document).ready(function () {
const input = target.val()
target.addClass('is-invalid')
target.removeClass('is-valid')
$('#' + prefix + '_reset').removeClass('d-none')
$.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) {
let html = '<ul class="list-group list-group-flush" id="' + prefix + '_list">'
@ -41,11 +42,14 @@ $(document).ready(function () {
if (typeof autocompleted !== 'undefined') { autocompleted(obj, prefix) }
})
if (input === obj[name_field]) { $('#' + prefix + '_pk').val(obj.id) }
})
if (objects.results.length === 1 && e.originalEvent.keyCode >= 32) {
if (objects.results.length >= 2) {
$('#' + prefix + '_pk').val(objects.results[0].id)
}
if (objects.results.length === 1 &&
(keycodes.includes(e.originalEvent.keyCode) ||
input === objects.results[0][name_field])) {
$('#' + prefix + '_' + objects.results[0].id).trigger('click')
}
})
@ -55,7 +59,6 @@ $(document).ready(function () {
const name = $(this).attr('id').replace('_reset', '')
$('#' + name + '_pk').val('')
$('#' + name).val('')
$('#' + name + '_list').html('')
$(this).addClass('d-none')
$('#' + name).tooltip('hide')
})
})

View File

@ -97,7 +97,7 @@ function displayStyle (note) {
const balance = note.balance
var css = ''
var ms_per_year = 31536000000 // 365 * 24 * 3600 * 1000
if (balance < -5000) { css += ' text-danger bg-dark' }
if (balance < -2000) { css += ' text-danger bg-dark' }
else if (balance < -1000) { css += ' text-danger' }
else if (balance < 0) { css += ' text-warning' }
if (!note.email_confirmed) { css += ' bg-primary' }

View File

@ -12,6 +12,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% 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>
<a id="{{ widget.attrs.id }}_reset" class="btn btn-light autocomplete-reset">{% trans "Reset" %}</a>
{% endif %}

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

@ -35,7 +35,7 @@ commands =
flake8 apps --extend-exclude apps/scripts
[flake8]
ignore = W503, I100, I101
ignore = W503, I100, I101, B019
exclude =
.tox,
.git,