1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-12-26 17:32:22 +00:00

Merge branch 'main' into potvieux

This commit is contained in:
misterkrafts 2024-01-11 02:19:51 +01:00 committed by www-data
commit 90e3871934
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,12 +326,12 @@ 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
@ -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

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,