mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 09:58:23 +02:00
Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
e6f3084588 | |||
145e55da75 | |||
d3ba95cdca | |||
8ffb0ebb56 | |||
5038af9e34 | |||
819b4214c9 | |||
b8a93b0b75 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,7 +42,6 @@ map.json
|
|||||||
backups/
|
backups/
|
||||||
/static/
|
/static/
|
||||||
/media/
|
/media/
|
||||||
/tmp/
|
|
||||||
|
|
||||||
# Virtualenv
|
# Virtualenv
|
||||||
env/
|
env/
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
"name": "Pot",
|
"name": "Pot",
|
||||||
"manage_entries": true,
|
"manage_entries": true,
|
||||||
"can_invite": true,
|
"can_invite": true,
|
||||||
"guest_entry_fee": 1000
|
"guest_entry_fee": 500
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,25 +28,5 @@
|
|||||||
"can_invite": false,
|
"can_invite": false,
|
||||||
"guest_entry_fee": 0
|
"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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -38,6 +38,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
|
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
|
||||||
|
<button id="trigger" class="btn btn-secondary">Click me !</button>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
@ -63,15 +64,46 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
refreshBalance();
|
refreshBalance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function process_qrcode() {
|
||||||
|
let name = alias_obj.val();
|
||||||
|
$.get("/api/note/note?search=" + name + "&format=json").done(
|
||||||
|
function (res) {
|
||||||
|
let note = res.results[0];
|
||||||
|
$.post("/api/activity/entry/?format=json", {
|
||||||
|
csrfmiddlewaretoken: CSRF_TOKEN,
|
||||||
|
activity: {{ activity.id }},
|
||||||
|
note: note.id,
|
||||||
|
guest: null
|
||||||
|
}).done(function () {
|
||||||
|
addMsg(interpolate(gettext(
|
||||||
|
"Entry made for %s whose balance is %s €"),
|
||||||
|
[note.name, note.balance / 100]), "success", 4000);
|
||||||
|
reloadTable(true);
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
errMsg(xhr.responseJSON, 4000);
|
||||||
|
});
|
||||||
|
}).fail(function (xhr) {
|
||||||
|
errMsg(xhr.responseJSON, 4000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
alias_obj.keyup(function(event) {
|
alias_obj.keyup(function(event) {
|
||||||
let code = event.originalEvent.keyCode
|
let code = event.originalEvent.keyCode
|
||||||
if (65 <= code <= 122 || code === 13) {
|
if (65 <= code <= 122 || code === 13) {
|
||||||
debounce(reloadTable)()
|
debounce(reloadTable)()
|
||||||
}
|
}
|
||||||
|
if (code === 0)
|
||||||
|
process_qrcode();
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).ready(init);
|
$(document).ready(init);
|
||||||
|
|
||||||
|
alias_obj2 = document.getElementById("alias");
|
||||||
|
$("#trigger").click(function (e) {
|
||||||
|
addMsg("Clicked", "success", 1000);
|
||||||
|
alias_obj.val(alias_obj.val() + "\0");
|
||||||
|
alias_obj2.dispatchEvent(new KeyboardEvent('keyup'));
|
||||||
|
})
|
||||||
function init() {
|
function init() {
|
||||||
$(".table-row").click(function (e) {
|
$(".table-row").click(function (e) {
|
||||||
let target = e.target.parentElement;
|
let target = e.target.parentElement;
|
||||||
@ -168,4 +200,4 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
from rest_framework.pagination import PageNumberPagination
|
|
||||||
|
|
||||||
|
|
||||||
class CustomPagination(PageNumberPagination):
|
|
||||||
page_size_query_param = 'page_size'
|
|
@ -47,13 +47,6 @@ class ProfileForm(forms.ModelForm):
|
|||||||
|
|
||||||
last_report = forms.DateTimeField(required=False, disabled=True, label=_("Last report date"))
|
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):
|
def clean_promotion(self):
|
||||||
promotion = self.cleaned_data["promotion"]
|
promotion = self.cleaned_data["promotion"]
|
||||||
if promotion > timezone.now().year:
|
if promotion > timezone.now().year:
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.26 on 2022-09-04 21:25
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('member', '0008_auto_20211005_1544'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='promotion',
|
|
||||||
field=models.PositiveSmallIntegerField(default=2022, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.28 on 2023-08-23 21:29
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('member', '0009_auto_20220904_2325'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='profile',
|
|
||||||
name='promotion',
|
|
||||||
field=models.PositiveSmallIntegerField(default=2023, help_text='Year of entry to the school (None if not ENS student)', null=True, verbose_name='promotion'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.28 on 2023-08-31 09:50
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('member', '0010_new_default_year'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='profile',
|
|
||||||
name='VSS_charter_read',
|
|
||||||
field=models.BooleanField(default=False, verbose_name='VSS charter read'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -134,11 +134,6 @@ class Profile(models.Model):
|
|||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
VSS_charter_read = models.BooleanField(
|
|
||||||
verbose_name=_("VSS charter read"),
|
|
||||||
default=False
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ens_year(self):
|
def ens_year(self):
|
||||||
"""
|
"""
|
||||||
@ -268,7 +263,7 @@ class Club(models.Model):
|
|||||||
|
|
||||||
today = datetime.date.today()
|
today = datetime.date.today()
|
||||||
|
|
||||||
while (today - self.membership_start).days >= 365:
|
if (today - self.membership_start).days >= 365:
|
||||||
if self.membership_start:
|
if self.membership_start:
|
||||||
self.membership_start = datetime.date(self.membership_start.year + 1,
|
self.membership_start = datetime.date(self.membership_start.year + 1,
|
||||||
self.membership_start.month, self.membership_start.day)
|
self.membership_start.month, self.membership_start.day)
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* On form submit, create a new friendship
|
|
||||||
*/
|
|
||||||
function create_trust (e) {
|
|
||||||
// Do not submit HTML form
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
// Get data and send to API
|
|
||||||
const formData = new FormData(e.target)
|
|
||||||
$.getJSON('/api/note/alias/'+formData.get('trusted') + '/',
|
|
||||||
function (trusted_alias) {
|
|
||||||
if ((trusted_alias.note == formData.get('trusting')))
|
|
||||||
{
|
|
||||||
addMsg(gettext("You can't add yourself as a friend"), "danger")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$.post('/api/note/trust/', {
|
|
||||||
csrfmiddlewaretoken: formData.get('csrfmiddlewaretoken'),
|
|
||||||
trusting: formData.get('trusting'),
|
|
||||||
trusted: trusted_alias.note
|
|
||||||
}).done(function () {
|
|
||||||
// Reload table
|
|
||||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
|
||||||
addMsg(gettext('Friendship successfully added'), 'success')
|
|
||||||
}).fail(function (xhr, _textStatus, _error) {
|
|
||||||
errMsg(xhr.responseJSON)
|
|
||||||
})
|
|
||||||
}).fail(function (xhr, _textStatus, _error) {
|
|
||||||
errMsg(xhr.responseJSON)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* On click of "delete", delete the alias
|
|
||||||
* @param button_id:Integer Alias id to remove
|
|
||||||
*/
|
|
||||||
function delete_button (button_id) {
|
|
||||||
$.ajax({
|
|
||||||
url: '/api/note/trust/' + button_id + '/',
|
|
||||||
method: 'DELETE',
|
|
||||||
headers: { 'X-CSRFTOKEN': CSRF_TOKEN }
|
|
||||||
}).done(function () {
|
|
||||||
addMsg(gettext('Friendship successfully deleted'), 'success')
|
|
||||||
$('#trust_table').load(location.pathname + ' #trust_table')
|
|
||||||
}).fail(function (xhr, _textStatus, _error) {
|
|
||||||
errMsg(xhr.responseJSON)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function () {
|
|
||||||
// Attach event
|
|
||||||
document.getElementById('form_trust').addEventListener('submit', create_trust)
|
|
||||||
})
|
|
@ -25,14 +25,6 @@
|
|||||||
</a>
|
</a>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'friendships'|capfirst %}</dt>
|
|
||||||
<dd class="col-xl-6">
|
|
||||||
<a class="badge badge-secondary" href="{% url 'member:user_trust' user_object.pk %}">
|
|
||||||
<i class="fa fa-edit"></i>
|
|
||||||
{% trans 'Manage friendships' %} ({{ user_object.note.trusting.all|length }})
|
|
||||||
</a>
|
|
||||||
</dd>
|
|
||||||
|
|
||||||
{% if "member.view_profile"|has_perm:user_object.profile %}
|
{% if "member.view_profile"|has_perm:user_object.profile %}
|
||||||
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
|
||||||
@ -60,7 +52,10 @@
|
|||||||
{% if user_object.pk == user.pk %}
|
{% if user_object.pk == user.pk %}
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
<a class="small badge badge-secondary" href="{% url 'member:auth_token' %}">
|
||||||
<i class="fa fa-cogs"></i>{% trans 'API token' %}
|
<i class="fa fa-cogs"></i> {% trans 'API token' %}
|
||||||
|
</a>
|
||||||
|
<a class="small badge badge-secondary" href="{% url 'member:qr_code' user_object.pk %}">
|
||||||
|
<i class="fa fa-qrcode"></i> {% trans 'QR Code' %}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
{% extends "member/base.html" %}
|
|
||||||
{% comment %}
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
{% endcomment %}
|
|
||||||
{% load static django_tables2 i18n %}
|
|
||||||
|
|
||||||
{% block profile_content %}
|
|
||||||
<div class="card bg-light mb-3">
|
|
||||||
<h3 class="card-header text-center">
|
|
||||||
{% trans "Note friendships" %}
|
|
||||||
</h3>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if can_create %}
|
|
||||||
<form class="input-group" method="POST" id="form_trust">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="trusting" value="{{ object.note.pk }}">
|
|
||||||
{%include "autocomplete_model.html" %}
|
|
||||||
<div class="input-group-append">
|
|
||||||
<input type="submit" class="btn btn-success" value="{% trans "Add" %}">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% render_table trusting %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-warning card">
|
|
||||||
{% blocktrans trimmed %}
|
|
||||||
Adding someone as a friend enables them to initiate transactions coming
|
|
||||||
from your account (while keeping your balance positive). This is
|
|
||||||
designed to simplify using note kfet transfers to transfer money between
|
|
||||||
users. The intent is that one person can make all transfers for a group of
|
|
||||||
friends without needing additional rights among them.
|
|
||||||
{% endblocktrans %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block extrajavascript %}
|
|
||||||
<script src="{% static "member/js/trust.js" %}"></script>
|
|
||||||
<script src="{% static "js/autocomplete_model.js" %}"></script>
|
|
||||||
{% endblock%}
|
|
36
apps/member/templates/member/qr_code.html
Normal file
36
apps/member/templates/member/qr_code.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% comment %}
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
{% endcomment %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card bg-light">
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{% trans "QR Code for" %} {{ user_object.username }} ({{ user_object.first_name }} {{user_object.last_name }})
|
||||||
|
</h3>
|
||||||
|
<div class="text-center" id="qrcode">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script>
|
||||||
|
var qrc = new QRCode(document.getElementById("qrcode"), {
|
||||||
|
text: "{{ user_object.pk }}\0",
|
||||||
|
width: 1024,
|
||||||
|
height: 1024
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extracss %}
|
||||||
|
<style>
|
||||||
|
img {
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
@ -183,7 +183,7 @@ class TestMemberships(TestCase):
|
|||||||
club = Club.objects.get(name="Kfet")
|
club = Club.objects.get(name="Kfet")
|
||||||
else:
|
else:
|
||||||
club = Club.objects.create(
|
club = Club.objects.create(
|
||||||
name="Second club without BDE",
|
name="Second club " + ("with BDE" if bde_parent else "without BDE"),
|
||||||
parent_club=None,
|
parent_club=None,
|
||||||
email="newclub@example.com",
|
email="newclub@example.com",
|
||||||
require_memberships=True,
|
require_memberships=True,
|
||||||
@ -335,7 +335,6 @@ class TestMemberships(TestCase):
|
|||||||
ml_sports_registration=True,
|
ml_sports_registration=True,
|
||||||
ml_art_registration=True,
|
ml_art_registration=True,
|
||||||
report_frequency=7,
|
report_frequency=7,
|
||||||
VSS_charter_read=True
|
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
self.assertTrue(User.objects.filter(username="toto changed").exists())
|
self.assertTrue(User.objects.filter(username="toto changed").exists())
|
||||||
|
@ -23,6 +23,6 @@ urlpatterns = [
|
|||||||
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
|
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
|
||||||
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
||||||
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
|
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
|
||||||
path('user/<int:pk>/trust', views.ProfileTrustView.as_view(), name="user_trust"),
|
|
||||||
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
||||||
|
path('user/<int:pk>/qr_code/', views.QRCodeView.as_view(), name='qr_code'),
|
||||||
]
|
]
|
||||||
|
@ -8,7 +8,6 @@ from django.contrib.auth import logout
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LoginView
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
@ -19,9 +18,9 @@ from django.views.generic import DetailView, UpdateView, TemplateView
|
|||||||
from django.views.generic.edit import FormMixin
|
from django.views.generic.edit import FormMixin
|
||||||
from django_tables2.views import SingleTableView
|
from django_tables2.views import SingleTableView
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from note.models import Alias, NoteClub, NoteUser, Trust
|
from note.models import Alias, NoteUser, NoteClub
|
||||||
from note.models.transactions import Transaction, SpecialTransaction
|
from note.models.transactions import Transaction, SpecialTransaction
|
||||||
from note.tables import HistoryTable, AliasTable, TrustTable
|
from note.tables import HistoryTable, AliasTable
|
||||||
from note_kfet.middlewares import _set_current_request
|
from note_kfet.middlewares import _set_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.models import Role
|
from permission.models import Role
|
||||||
@ -244,39 +243,6 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileTrustView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|
||||||
"""
|
|
||||||
View and manage user trust relationships
|
|
||||||
"""
|
|
||||||
model = User
|
|
||||||
template_name = 'member/profile_trust.html'
|
|
||||||
context_object_name = 'user_object'
|
|
||||||
extra_context = {"title": _("Note friendships")}
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
note = context['object'].note
|
|
||||||
context["trusting"] = TrustTable(
|
|
||||||
note.trusting.filter(PermissionBackend.filter_queryset(self.request, Trust, "view")).distinct().all())
|
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_trust", Trust(
|
|
||||||
trusting=context["object"].note,
|
|
||||||
trusted=context["object"].note
|
|
||||||
))
|
|
||||||
context["widget"] = {
|
|
||||||
"name": "trusted",
|
|
||||||
"attrs": {
|
|
||||||
"model_pk": ContentType.objects.get_for_model(Alias).pk,
|
|
||||||
"class": "autocomplete form-control",
|
|
||||||
"id": "trusted",
|
|
||||||
"resetable": True,
|
|
||||||
"api_url": "/api/note/alias/?note__polymorphic_ctype__model=noteuser",
|
|
||||||
"name_field": "name",
|
|
||||||
"placeholder": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
"""
|
"""
|
||||||
View and manage user aliases.
|
View and manage user aliases.
|
||||||
@ -365,6 +331,14 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
|||||||
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
|
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
class QRCodeView(LoginRequiredMixin, DetailView):
|
||||||
|
"""
|
||||||
|
Affiche le QR Code
|
||||||
|
"""
|
||||||
|
model = User
|
||||||
|
context_object_name = "user_object"
|
||||||
|
template_name = "member/qr_code.html"
|
||||||
|
extra_context = {"title": _("QR Code")}
|
||||||
|
|
||||||
# ******************************* #
|
# ******************************* #
|
||||||
# CLUB #
|
# CLUB #
|
||||||
@ -753,10 +727,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
club = old_membership.club
|
club = old_membership.club
|
||||||
user = old_membership.user
|
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
|
form.instance.club = club
|
||||||
|
|
||||||
# Get form data
|
# Get form data
|
||||||
|
@ -12,7 +12,7 @@ from note_kfet.middlewares import get_current_request
|
|||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from rest_framework.utils import model_meta
|
from rest_framework.utils import model_meta
|
||||||
|
|
||||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias, Trust
|
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
||||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
|
||||||
RecurrentTransaction, SpecialTransaction
|
RecurrentTransaction, SpecialTransaction
|
||||||
|
|
||||||
@ -77,22 +77,6 @@ class NoteUserSerializer(serializers.ModelSerializer):
|
|||||||
return str(obj)
|
return str(obj)
|
||||||
|
|
||||||
|
|
||||||
class TrustSerializer(serializers.ModelSerializer):
|
|
||||||
"""
|
|
||||||
REST API Serializer for Trusts.
|
|
||||||
The djangorestframework plugin will analyse the model `Trust` and parse all fields in the API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
model = Trust
|
|
||||||
fields = '__all__'
|
|
||||||
|
|
||||||
def validate(self, attrs):
|
|
||||||
instance = Trust(**attrs)
|
|
||||||
instance.clean()
|
|
||||||
return attrs
|
|
||||||
|
|
||||||
|
|
||||||
class AliasSerializer(serializers.ModelSerializer):
|
class AliasSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
REST API Serializer for Aliases.
|
REST API Serializer for Aliases.
|
||||||
|
@ -2,8 +2,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \
|
||||||
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet, \
|
TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet
|
||||||
TrustViewSet
|
|
||||||
|
|
||||||
|
|
||||||
def register_note_urls(router, path):
|
def register_note_urls(router, path):
|
||||||
@ -12,7 +11,6 @@ def register_note_urls(router, path):
|
|||||||
"""
|
"""
|
||||||
router.register(path + '/note', NotePolymorphicViewSet)
|
router.register(path + '/note', NotePolymorphicViewSet)
|
||||||
router.register(path + '/alias', AliasViewSet)
|
router.register(path + '/alias', AliasViewSet)
|
||||||
router.register(path + '/trust', TrustViewSet)
|
|
||||||
router.register(path + '/consumer', ConsumerViewSet)
|
router.register(path + '/consumer', ConsumerViewSet)
|
||||||
|
|
||||||
router.register(path + '/transaction/category', TemplateCategoryViewSet)
|
router.register(path + '/transaction/category', TemplateCategoryViewSet)
|
||||||
|
@ -14,9 +14,8 @@ from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSe
|
|||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
|
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
|
||||||
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer, \
|
TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer
|
||||||
TrustSerializer
|
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial
|
||||||
from ..models.notes import Note, Alias, NoteUser, NoteClub, NoteSpecial, Trust
|
|
||||||
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory
|
||||||
|
|
||||||
|
|
||||||
@ -57,41 +56,11 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
|||||||
return queryset.order_by("id")
|
return queryset.order_by("id")
|
||||||
|
|
||||||
|
|
||||||
class TrustViewSet(ReadProtectedModelViewSet):
|
|
||||||
"""
|
|
||||||
REST Trust View set.
|
|
||||||
The djangorestframework plugin will get all `Trust` objects, serialize it to JSON with the given serializer,
|
|
||||||
then render it on /api/note/trust/
|
|
||||||
"""
|
|
||||||
queryset = Trust.objects
|
|
||||||
serializer_class = TrustSerializer
|
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
|
||||||
search_fields = ['$trusting__alias__name', '$trusting__alias__normalized_name',
|
|
||||||
'$trusted__alias__name', '$trusted__alias__normalized_name']
|
|
||||||
filterset_fields = ['trusting', 'trusting__noteuser__user', 'trusted', 'trusted__noteuser__user']
|
|
||||||
ordering_fields = ['trusting', 'trusted', ]
|
|
||||||
|
|
||||||
def get_serializer_class(self):
|
|
||||||
serializer_class = self.serializer_class
|
|
||||||
if self.request.method in ['PUT', 'PATCH']:
|
|
||||||
# trust relationship can't change people involved
|
|
||||||
serializer_class.Meta.read_only_fields = ('trusting', 'trusting',)
|
|
||||||
return serializer_class
|
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
|
||||||
instance = self.get_object()
|
|
||||||
try:
|
|
||||||
self.perform_destroy(instance)
|
|
||||||
except ValidationError as e:
|
|
||||||
return Response({e.code: str(e)}, status.HTTP_400_BAD_REQUEST)
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
class AliasViewSet(ReadProtectedModelViewSet):
|
class AliasViewSet(ReadProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/note/aliases/
|
then render it on /api/aliases/
|
||||||
"""
|
"""
|
||||||
queryset = Alias.objects
|
queryset = Alias.objects
|
||||||
serializer_class = AliasSerializer
|
serializer_class = AliasSerializer
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 2.2.24 on 2021-09-05 19:16
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('note', '0005_auto_20210313_1235'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Trust',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('trusted', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusted', to='note.Note', verbose_name='trusted')),
|
|
||||||
('trusting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trusting', to='note.Note', verbose_name='trusting')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'frienship',
|
|
||||||
'verbose_name_plural': 'friendships',
|
|
||||||
'unique_together': {('trusting', 'trusted')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,13 +1,13 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser, Trust
|
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
||||||
from .transactions import MembershipTransaction, Transaction, \
|
from .transactions import MembershipTransaction, Transaction, \
|
||||||
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
|
TemplateCategory, TransactionTemplate, RecurrentTransaction, SpecialTransaction
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Notes
|
# Notes
|
||||||
'Alias', 'Trust', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
|
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
|
||||||
# Transactions
|
# Transactions
|
||||||
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
|
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
|
||||||
'RecurrentTransaction', 'SpecialTransaction',
|
'RecurrentTransaction', 'SpecialTransaction',
|
||||||
|
@ -217,38 +217,6 @@ class NoteSpecial(Note):
|
|||||||
return self.special_type
|
return self.special_type
|
||||||
|
|
||||||
|
|
||||||
class Trust(models.Model):
|
|
||||||
"""
|
|
||||||
A one-sided trust relationship bertween two users
|
|
||||||
|
|
||||||
If another user considers you as your friend, you can transfer money from
|
|
||||||
them
|
|
||||||
"""
|
|
||||||
|
|
||||||
trusting = models.ForeignKey(
|
|
||||||
Note,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='trusting',
|
|
||||||
verbose_name=_('trusting')
|
|
||||||
)
|
|
||||||
|
|
||||||
trusted = models.ForeignKey(
|
|
||||||
Note,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='trusted',
|
|
||||||
verbose_name=_('trusted')
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("frienship")
|
|
||||||
verbose_name_plural = _("friendships")
|
|
||||||
unique_together = ("trusting", "trusted")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return _("Friendship between {trusting} and {trusted}").format(
|
|
||||||
trusting=str(self.trusting), trusted=str(self.trusted))
|
|
||||||
|
|
||||||
|
|
||||||
class Alias(models.Model):
|
class Alias(models.Model):
|
||||||
"""
|
"""
|
||||||
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
|
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
|
||||||
|
@ -325,8 +325,8 @@ class SpecialTransaction(Transaction):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
# SpecialTransaction are only possible with NoteSpecial object
|
# SpecialTransaction are only possible with NoteSpecial object
|
||||||
if self.is_credit() == self.is_debit():
|
if self.is_credit() == self.is_debit():
|
||||||
raise ValidationError(_("A special transaction is only possible between a"
|
raise(ValidationError(_("A special transaction is only possible between a"
|
||||||
" Note associated to a payment method and a User or a Club"))
|
" Note associated to a payment method and a User or a Club")))
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
@ -221,7 +221,7 @@ function consume (source, source_alias, dest, quantity, amount, reason, type, ca
|
|||||||
.done(function () {
|
.done(function () {
|
||||||
if (!isNaN(source.balance)) {
|
if (!isNaN(source.balance)) {
|
||||||
const newBalance = source.balance - quantity * amount
|
const newBalance = source.balance - quantity * amount
|
||||||
if (newBalance <= -2000) {
|
if (newBalance <= -5000) {
|
||||||
addMsg(interpolate(gettext('Warning, the transaction from the note %s succeed, ' +
|
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)
|
'but the emitter note %s is very negative.'), [source_alias, source_alias]), 'danger', 30000)
|
||||||
} else if (newBalance < 0) {
|
} else if (newBalance < 0) {
|
||||||
|
@ -314,7 +314,7 @@ $('#btn_transfer').click(function () {
|
|||||||
|
|
||||||
if (!isNaN(source.note.balance)) {
|
if (!isNaN(source.note.balance)) {
|
||||||
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
|
const newBalance = source.note.balance - source.quantity * dest.quantity * amount
|
||||||
if (newBalance <= -2000) {
|
if (newBalance <= -5000) {
|
||||||
addMsg(interpolate(gettext('Warning, the transaction of %s from the note %s to the note %s succeed, but the emitter note %s is very negative.'),
|
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)
|
[pretty_money(source.quantity * dest.quantity * amount), source.name, dest.name, source.name]), 'danger', 10000)
|
||||||
reset()
|
reset()
|
||||||
|
@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from note_kfet.middlewares import get_current_request
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models.notes import Alias, Trust
|
from .models.notes import Alias
|
||||||
from .models.transactions import Transaction, TransactionTemplate
|
from .models.transactions import Transaction, TransactionTemplate
|
||||||
from .templatetags.pretty_money import pretty_money
|
from .templatetags.pretty_money import pretty_money
|
||||||
|
|
||||||
@ -148,31 +148,6 @@ DELETE_TEMPLATE = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class TrustTable(tables.Table):
|
|
||||||
class Meta:
|
|
||||||
attrs = {
|
|
||||||
'class': 'table table condensed table-striped',
|
|
||||||
'id': "trust_table"
|
|
||||||
}
|
|
||||||
model = Trust
|
|
||||||
fields = ("trusted",)
|
|
||||||
template_name = 'django_tables2/bootstrap4.html'
|
|
||||||
|
|
||||||
show_header = False
|
|
||||||
trusted = tables.Column(attrs={'td': {'class': 'text_center'}})
|
|
||||||
|
|
||||||
delete_col = tables.TemplateColumn(
|
|
||||||
template_code=DELETE_TEMPLATE,
|
|
||||||
extra_context={"delete_trans": _('delete')},
|
|
||||||
attrs={
|
|
||||||
'td': {
|
|
||||||
'class': lambda record: 'col-sm-1'
|
|
||||||
+ (' d-none' if not PermissionBackend.check_perm(
|
|
||||||
get_current_request(), "note.delete_trust", record)
|
|
||||||
else '')}},
|
|
||||||
verbose_name=_("Delete"),)
|
|
||||||
|
|
||||||
|
|
||||||
class AliasTable(tables.Table):
|
class AliasTable(tables.Table):
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,19 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -339,7 +339,6 @@ class Role(models.Model):
|
|||||||
"member.Club",
|
"member.Club",
|
||||||
verbose_name=_("for club"),
|
verbose_name=_("for club"),
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
blank=True,
|
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
@ -5,7 +5,6 @@ from django import forms
|
|||||||
from django.contrib.auth.forms import UserCreationForm
|
from django.contrib.auth.forms import UserCreationForm
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from member.models import Club
|
|
||||||
from note.models import NoteSpecial, Alias
|
from note.models import NoteSpecial, Alias
|
||||||
from note_kfet.inputs import AmountInput
|
from note_kfet.inputs import AmountInput
|
||||||
|
|
||||||
@ -45,14 +44,14 @@ class SignUpForm(UserCreationForm):
|
|||||||
fields = ('first_name', 'last_name', 'username', 'email', )
|
fields = ('first_name', 'last_name', 'username', 'email', )
|
||||||
|
|
||||||
|
|
||||||
# class DeclareSogeAccountOpenedForm(forms.Form):
|
class DeclareSogeAccountOpenedForm(forms.Form):
|
||||||
# soge_account = forms.BooleanField(
|
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 "
|
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."),
|
"partnership."),
|
||||||
# help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
|
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."),
|
"account, you will have to pay the BDE membership."),
|
||||||
# required=False,
|
required=False,
|
||||||
# )
|
)
|
||||||
|
|
||||||
|
|
||||||
class WEISignupForm(forms.Form):
|
class WEISignupForm(forms.Form):
|
||||||
@ -68,11 +67,11 @@ class ValidationForm(forms.Form):
|
|||||||
"""
|
"""
|
||||||
Validate the inscription of the new users and pay memberships.
|
Validate the inscription of the new users and pay memberships.
|
||||||
"""
|
"""
|
||||||
# soge = forms.BooleanField(
|
soge = forms.BooleanField(
|
||||||
# label=_("Inscription paid by Société Générale"),
|
label=_("Inscription paid by Société Générale"),
|
||||||
# required=False,
|
required=False,
|
||||||
# help_text=_("Check this case if the Société Générale paid the inscription."),
|
help_text=_("Check this case if the Société Générale paid the inscription."),
|
||||||
# )
|
)
|
||||||
|
|
||||||
credit_type = forms.ModelChoiceField(
|
credit_type = forms.ModelChoiceField(
|
||||||
queryset=NoteSpecial.objects,
|
queryset=NoteSpecial.objects,
|
||||||
@ -115,12 +114,3 @@ class ValidationForm(forms.Form):
|
|||||||
required=False,
|
required=False,
|
||||||
initial=True,
|
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,
|
|
||||||
)
|
|
||||||
|
@ -57,13 +57,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<h4> {% trans "Validate account" %}</h4>
|
<h4> {% trans "Validate account" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% comment "Soge not for membership (only WEI)" %}
|
|
||||||
{% if declare_soge_account %}
|
{% if declare_soge_account %}
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
{% trans "The user declared that he/she opened a bank account in the Société générale." %}
|
{% trans "The user declared that he/she opened a bank account in the Société générale." %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endcomment %}
|
|
||||||
|
|
||||||
<div class="card-body" id="profile_infos">
|
<div class="card-body" id="profile_infos">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
@ -78,7 +76,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% comment "Soge not for membership (only WEI)" %}
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script>
|
<script>
|
||||||
soge_field = $("#id_soge");
|
soge_field = $("#id_soge");
|
||||||
@ -121,4 +118,3 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endcomment %}
|
|
||||||
|
@ -48,7 +48,6 @@ class TestSignup(TestCase):
|
|||||||
ml_events_registration="en",
|
ml_events_registration="en",
|
||||||
ml_sport_registration=True,
|
ml_sport_registration=True,
|
||||||
ml_art_registration=True,
|
ml_art_registration=True,
|
||||||
VSS_charter_read=True
|
|
||||||
))
|
))
|
||||||
self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200)
|
self.assertRedirects(response, reverse("registration:email_validation_sent"), 302, 200)
|
||||||
self.assertTrue(User.objects.filter(username="toto").exists())
|
self.assertTrue(User.objects.filter(username="toto").exists())
|
||||||
@ -106,7 +105,6 @@ class TestSignup(TestCase):
|
|||||||
ml_events_registration="en",
|
ml_events_registration="en",
|
||||||
ml_sport_registration=True,
|
ml_sport_registration=True,
|
||||||
ml_art_registration=True,
|
ml_art_registration=True,
|
||||||
VSS_charter_read=True
|
|
||||||
))
|
))
|
||||||
self.assertTrue(response.status_code, 200)
|
self.assertTrue(response.status_code, 200)
|
||||||
|
|
||||||
@ -126,7 +124,6 @@ class TestSignup(TestCase):
|
|||||||
ml_events_registration="en",
|
ml_events_registration="en",
|
||||||
ml_sport_registration=True,
|
ml_sport_registration=True,
|
||||||
ml_art_registration=True,
|
ml_art_registration=True,
|
||||||
VSS_charter_read=True
|
|
||||||
))
|
))
|
||||||
self.assertTrue(response.status_code, 200)
|
self.assertTrue(response.status_code, 200)
|
||||||
|
|
||||||
@ -146,27 +143,6 @@ class TestSignup(TestCase):
|
|||||||
ml_events_registration="en",
|
ml_events_registration="en",
|
||||||
ml_sport_registration=True,
|
ml_sport_registration=True,
|
||||||
ml_art_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)
|
self.assertTrue(response.status_code, 200)
|
||||||
|
|
||||||
@ -214,7 +190,7 @@ class TestValidateRegistration(TestCase):
|
|||||||
|
|
||||||
# BDE Membership is mandatory
|
# BDE Membership is mandatory
|
||||||
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
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_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
credit_amount=4200,
|
credit_amount=4200,
|
||||||
last_name="TOTO",
|
last_name="TOTO",
|
||||||
@ -228,7 +204,7 @@ class TestValidateRegistration(TestCase):
|
|||||||
|
|
||||||
# Same
|
# Same
|
||||||
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
# soge=False,
|
soge=False,
|
||||||
credit_type="",
|
credit_type="",
|
||||||
credit_amount=0,
|
credit_amount=0,
|
||||||
last_name="TOTO",
|
last_name="TOTO",
|
||||||
@ -242,7 +218,7 @@ class TestValidateRegistration(TestCase):
|
|||||||
|
|
||||||
# The BDE membership is not free
|
# The BDE membership is not free
|
||||||
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
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_type=NoteSpecial.objects.get(special_type="Espèces").id,
|
||||||
credit_amount=0,
|
credit_amount=0,
|
||||||
last_name="TOTO",
|
last_name="TOTO",
|
||||||
@ -256,7 +232,7 @@ class TestValidateRegistration(TestCase):
|
|||||||
|
|
||||||
# Last and first names are required for a credit
|
# Last and first names are required for a credit
|
||||||
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
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_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
credit_amount=4000,
|
credit_amount=4000,
|
||||||
last_name="",
|
last_name="",
|
||||||
@ -273,7 +249,7 @@ class TestValidateRegistration(TestCase):
|
|||||||
self.user.username = "admïntoto"
|
self.user.username = "admïntoto"
|
||||||
self.user.save()
|
self.user.save()
|
||||||
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
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_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
credit_amount=500,
|
credit_amount=500,
|
||||||
last_name="TOTO",
|
last_name="TOTO",
|
||||||
@ -299,7 +275,7 @@ class TestValidateRegistration(TestCase):
|
|||||||
self.user.profile.save()
|
self.user.profile.save()
|
||||||
|
|
||||||
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
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_type=NoteSpecial.objects.get(special_type="Chèque").id,
|
||||||
credit_amount=500,
|
credit_amount=500,
|
||||||
last_name="TOTO",
|
last_name="TOTO",
|
||||||
@ -314,7 +290,6 @@ class TestValidateRegistration(TestCase):
|
|||||||
self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
|
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="BDE", user=self.user).exists())
|
||||||
self.assertFalse(Membership.objects.filter(club__name="Kfet", 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.assertFalse(SogeCredit.objects.filter(user=self.user).exists())
|
||||||
self.assertEqual(Transaction.objects.filter(
|
self.assertEqual(Transaction.objects.filter(
|
||||||
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2)
|
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 2)
|
||||||
@ -336,7 +311,7 @@ class TestValidateRegistration(TestCase):
|
|||||||
self.user.profile.save()
|
self.user.profile.save()
|
||||||
|
|
||||||
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
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_type=NoteSpecial.objects.get(special_type="Espèces").id,
|
||||||
credit_amount=4000,
|
credit_amount=4000,
|
||||||
last_name="TOTO",
|
last_name="TOTO",
|
||||||
@ -351,7 +326,6 @@ class TestValidateRegistration(TestCase):
|
|||||||
self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
|
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="BDE", user=self.user).exists())
|
||||||
self.assertTrue(Membership.objects.filter(club__name="Kfet", 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.assertFalse(SogeCredit.objects.filter(user=self.user).exists())
|
||||||
self.assertEqual(Transaction.objects.filter(
|
self.assertEqual(Transaction.objects.filter(
|
||||||
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
||||||
@ -359,43 +333,42 @@ class TestValidateRegistration(TestCase):
|
|||||||
response = self.client.get(self.user.profile.get_absolute_url())
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# def test_validate_kfet_registration_with_soge(self):
|
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.
|
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,)))
|
response = self.client.get(reverse("registration:future_user_detail", args=(self.user.pk,)))
|
||||||
# self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
#
|
|
||||||
# response = self.client.get(self.user.profile.get_absolute_url())
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
# self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
#
|
|
||||||
# self.user.profile.email_confirmed = True
|
self.user.profile.email_confirmed = True
|
||||||
# self.user.profile.save()
|
self.user.profile.save()
|
||||||
#
|
|
||||||
# response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
response = self.client.post(reverse("registration:future_user_detail", args=(self.user.pk,)), data=dict(
|
||||||
# soge=True,
|
soge=True,
|
||||||
# credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
|
credit_type=NoteSpecial.objects.get(special_type="Espèces").id,
|
||||||
# credit_amount=4000,
|
credit_amount=4000,
|
||||||
# last_name="TOTO",
|
last_name="TOTO",
|
||||||
# first_name="Toto",
|
first_name="Toto",
|
||||||
# bank="Société générale",
|
bank="Société générale",
|
||||||
# join_bde=True,
|
join_bde=True,
|
||||||
# join_kfet=True,
|
join_kfet=True,
|
||||||
# ))
|
))
|
||||||
# self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
self.assertRedirects(response, self.user.profile.get_absolute_url(), 302, 200)
|
||||||
# self.user.profile.refresh_from_db()
|
self.user.profile.refresh_from_db()
|
||||||
# self.assertTrue(self.user.profile.registration_valid)
|
self.assertTrue(self.user.profile.registration_valid)
|
||||||
# self.assertTrue(NoteUser.objects.filter(user=self.user).exists())
|
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="BDE", user=self.user).exists())
|
||||||
# self.assertTrue(Membership.objects.filter(club__name="Kfet", 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.assertTrue(SogeCredit.objects.filter(user=self.user).exists())
|
self.assertEqual(Transaction.objects.filter(
|
||||||
# self.assertEqual(Transaction.objects.filter(
|
Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
||||||
# Q(source=self.user.note) | Q(destination=self.user.note)).count(), 3)
|
self.assertFalse(Transaction.objects.filter(valid=True).exists())
|
||||||
# self.assertFalse(Transaction.objects.filter(valid=True).exists())
|
|
||||||
#
|
response = self.client.get(self.user.profile.get_absolute_url())
|
||||||
# response = self.client.get(self.user.profile.get_absolute_url())
|
self.assertEqual(response.status_code, 200)
|
||||||
# self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
def test_invalidate_registration(self):
|
def test_invalidate_registration(self):
|
||||||
"""
|
"""
|
||||||
|
@ -24,8 +24,7 @@ from permission.models import Role
|
|||||||
from permission.views import ProtectQuerysetMixin
|
from permission.views import ProtectQuerysetMixin
|
||||||
from treasury.models import SogeCredit
|
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 .tables import FutureUserTable
|
||||||
from .tokens import email_validation_token
|
from .tokens import email_validation_token
|
||||||
|
|
||||||
@ -43,7 +42,7 @@ class UserCreateView(CreateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["profile_form"] = self.second_form(self.request.POST if self.request.POST else None)
|
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["section"]
|
||||||
del context["profile_form"].fields["report_frequency"]
|
del context["profile_form"].fields["report_frequency"]
|
||||||
del context["profile_form"].fields["last_report"]
|
del context["profile_form"].fields["last_report"]
|
||||||
@ -76,12 +75,12 @@ class UserCreateView(CreateView):
|
|||||||
|
|
||||||
user.profile.send_email_validation_link()
|
user.profile.send_email_validation_link()
|
||||||
|
|
||||||
# soge_form = DeclareSogeAccountOpenedForm(self.request.POST)
|
soge_form = DeclareSogeAccountOpenedForm(self.request.POST)
|
||||||
# if "soge_account" in soge_form.data and soge_form.data["soge_account"]:
|
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
|
# If the user declares that a bank account got opened, prepare the soge credit to warn treasurers
|
||||||
# soge_credit = SogeCredit(user=user)
|
soge_credit = SogeCredit(user=user)
|
||||||
# soge_credit._force_save = True
|
soge_credit._force_save = True
|
||||||
# soge_credit.save()
|
soge_credit.save()
|
||||||
|
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -238,12 +237,9 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
|
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
|
||||||
kfet = Club.objects.get(name="Kfet")
|
kfet = Club.objects.get(name="Kfet")
|
||||||
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
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["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
|
return ctx
|
||||||
|
|
||||||
@ -266,13 +262,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
form.add_error(None, _("An alias with a similar name already exists."))
|
form.add_error(None, _("An alias with a similar name already exists."))
|
||||||
return self.form_invalid(form)
|
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
|
# Get form data
|
||||||
# soge = form.cleaned_data["soge"]
|
soge = form.cleaned_data["soge"]
|
||||||
credit_type = form.cleaned_data["credit_type"]
|
credit_type = form.cleaned_data["credit_type"]
|
||||||
credit_amount = form.cleaned_data["credit_amount"]
|
credit_amount = form.cleaned_data["credit_amount"]
|
||||||
last_name = form.cleaned_data["last_name"]
|
last_name = form.cleaned_data["last_name"]
|
||||||
@ -280,13 +271,11 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
bank = form.cleaned_data["bank"]
|
bank = form.cleaned_data["bank"]
|
||||||
join_bde = form.cleaned_data["join_bde"]
|
join_bde = form.cleaned_data["join_bde"]
|
||||||
join_kfet = form.cleaned_data["join_kfet"]
|
join_kfet = form.cleaned_data["join_kfet"]
|
||||||
if bda_exists:
|
|
||||||
join_bda = form.cleaned_data["join_bda"]
|
|
||||||
|
|
||||||
# if soge:
|
if soge:
|
||||||
# # If Société Générale pays the inscription, the user automatically joins the two clubs.
|
# If Société Générale pays the inscription, the user automatically joins the two clubs.
|
||||||
# join_bde = True
|
join_bde = True
|
||||||
# join_kfet = True
|
join_kfet = True
|
||||||
|
|
||||||
if not join_bde:
|
if not join_bde:
|
||||||
# This software belongs to the BDE.
|
# This software belongs to the BDE.
|
||||||
@ -303,21 +292,15 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||||
# Add extra fee for the full membership
|
# Add extra fee for the full membership
|
||||||
fee += kfet_fee if join_kfet else 0
|
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
|
# If the bank pays, then we don't credit now. Treasurers will validate the transaction
|
||||||
# # and credit the note later.
|
# and credit the note later.
|
||||||
# credit_type = None if soge else credit_type
|
credit_type = None if soge else credit_type
|
||||||
|
|
||||||
# If the user does not select any payment method, then no credit will be performed.
|
# 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
|
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
|
# Check if the user credits enough money
|
||||||
form.add_error('credit_type',
|
form.add_error('credit_type',
|
||||||
_("The entered amount is not enough for the memberships, should be at least {}")
|
_("The entered amount is not enough for the memberships, should be at least {}")
|
||||||
@ -337,12 +320,12 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
user.profile.save()
|
user.profile.save()
|
||||||
user.refresh_from_db()
|
user.refresh_from_db()
|
||||||
|
|
||||||
# if not soge and SogeCredit.objects.filter(user=user).exists():
|
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
|
# If the user declared that a bank account was opened but in the validation form the SoGé case was
|
||||||
# # unchecked, delete the associated credit
|
# unchecked, delete the associated credit
|
||||||
# soge_credit = SogeCredit.objects.get(user=user)
|
soge_credit = SogeCredit.objects.get(user=user)
|
||||||
# soge_credit._force_delete = True
|
soge_credit._force_delete = True
|
||||||
# soge_credit.delete()
|
soge_credit.delete()
|
||||||
|
|
||||||
if credit_type is not None and credit_amount > 0:
|
if credit_type is not None and credit_amount > 0:
|
||||||
# Credit the note
|
# Credit the note
|
||||||
@ -351,8 +334,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
destination=user.note,
|
destination=user.note,
|
||||||
quantity=1,
|
quantity=1,
|
||||||
amount=credit_amount,
|
amount=credit_amount,
|
||||||
reason="Crédit " + credit_type.special_type + " (Inscription)",
|
reason="Crédit " + ("Société générale" if soge else 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,
|
last_name=last_name,
|
||||||
first_name=first_name,
|
first_name=first_name,
|
||||||
bank=bank,
|
bank=bank,
|
||||||
@ -366,8 +348,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
user=user,
|
user=user,
|
||||||
fee=bde_fee,
|
fee=bde_fee,
|
||||||
)
|
)
|
||||||
# if soge:
|
if soge:
|
||||||
# membership._soge = True
|
membership._soge = True
|
||||||
membership.save()
|
membership.save()
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
||||||
@ -380,29 +362,17 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
user=user,
|
user=user,
|
||||||
fee=kfet_fee,
|
fee=kfet_fee,
|
||||||
)
|
)
|
||||||
# if soge:
|
if soge:
|
||||||
# membership._soge = True
|
membership._soge = True
|
||||||
membership.save()
|
membership.save()
|
||||||
membership.refresh_from_db()
|
membership.refresh_from_db()
|
||||||
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
membership.roles.add(Role.objects.get(name="Adhérent Kfet"))
|
||||||
membership.save()
|
membership.save()
|
||||||
|
|
||||||
if bda_exists and join_bda:
|
if soge:
|
||||||
# Create membership for the user to the BDA starting today
|
soge_credit = SogeCredit.objects.get(user=user)
|
||||||
membership = Membership(
|
# Update the credit transaction amount
|
||||||
club=bda,
|
soge_credit.save()
|
||||||
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
|
return ret
|
||||||
|
|
||||||
|
Submodule apps/scripts updated: f580f9b9e9...86bc2d2698
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,5 +1,6 @@
|
|||||||
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
import datetime
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -11,8 +12,7 @@ from django.db.models import Q
|
|||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
# from member.models import Club, Membership # Club unused because of disabled soge
|
from member.models import Club, Membership
|
||||||
from member.models import Membership
|
|
||||||
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
|
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
|
||||||
|
|
||||||
|
|
||||||
@ -28,10 +28,8 @@ class Invoice(models.Model):
|
|||||||
|
|
||||||
bde = models.CharField(
|
bde = models.CharField(
|
||||||
max_length=32,
|
max_length=32,
|
||||||
default='SecretStorlist',
|
default='Saperlistpopette',
|
||||||
choices=(
|
choices=(
|
||||||
('SecretStorlist', 'SecretStor[list]'),
|
|
||||||
('TotalistSpies', 'Tota[list]Spies'),
|
|
||||||
('Saperlistpopette', 'Saper[list]popette'),
|
('Saperlistpopette', 'Saper[list]popette'),
|
||||||
('Finalist', 'Fina[list]'),
|
('Finalist', 'Fina[list]'),
|
||||||
('Listorique', '[List]orique'),
|
('Listorique', '[List]orique'),
|
||||||
@ -97,7 +95,7 @@ class Invoice(models.Model):
|
|||||||
products = self.products.all()
|
products = self.products.all()
|
||||||
|
|
||||||
self.place = "Gif-sur-Yvette"
|
self.place = "Gif-sur-Yvette"
|
||||||
self.my_name = "BDE ENS Paris Saclay"
|
self.my_name = "BDE ENS Cachan"
|
||||||
self.my_address_street = "4 avenue des Sciences"
|
self.my_address_street = "4 avenue des Sciences"
|
||||||
self.my_city = "91190 Gif-sur-Yvette"
|
self.my_city = "91190 Gif-sur-Yvette"
|
||||||
self.bank_code = 30003
|
self.bank_code = 30003
|
||||||
@ -312,8 +310,8 @@ class SogeCredit(models.Model):
|
|||||||
amount = sum(transaction.total for transaction in self.transactions.all())
|
amount = sum(transaction.total for transaction in self.transactions.all())
|
||||||
if 'wei' in settings.INSTALLED_APPS:
|
if 'wei' in settings.INSTALLED_APPS:
|
||||||
from wei.models import WEIMembership
|
from wei.models import WEIMembership
|
||||||
if not WEIMembership.objects\
|
if not WEIMembership.objects.filter(club__weiclub__year=datetime.date.today().year, user=self.user)\
|
||||||
.filter(club__weiclub__year=self.credit_transaction.created_at.year, user=self.user).exists():
|
.exists():
|
||||||
# 80 € for people that don't go to WEI
|
# 80 € for people that don't go to WEI
|
||||||
amount += 8000
|
amount += 8000
|
||||||
return amount
|
return amount
|
||||||
@ -326,23 +324,22 @@ class SogeCredit(models.Model):
|
|||||||
if self.valid or not self.pk:
|
if self.valid or not self.pk:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Soge do not pay BDE and kfet memberships since 2022
|
bde = Club.objects.get(name="BDE")
|
||||||
# bde = Club.objects.get(name="BDE")
|
kfet = Club.objects.get(name="Kfet")
|
||||||
# kfet = Club.objects.get(name="Kfet")
|
bde_qs = Membership.objects.filter(user=self.user, club=bde, date_start__gte=bde.membership_start)
|
||||||
# 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)
|
||||||
# kfet_qs = Membership.objects.filter(user=self.user, club=kfet, date_start__gte=kfet.membership_start)
|
|
||||||
|
|
||||||
# if bde_qs.exists():
|
if bde_qs.exists():
|
||||||
# m = bde_qs.get()
|
m = bde_qs.get()
|
||||||
# if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
|
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
|
||||||
# if m.transaction not in self.transactions.all():
|
if m.transaction not in self.transactions.all():
|
||||||
# self.transactions.add(m.transaction)
|
self.transactions.add(m.transaction)
|
||||||
#
|
|
||||||
# if kfet_qs.exists():
|
if kfet_qs.exists():
|
||||||
# m = kfet_qs.get()
|
m = kfet_qs.get()
|
||||||
# if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
|
if MembershipTransaction.objects.filter(membership=m).exists(): # non-free membership
|
||||||
# if m.transaction not in self.transactions.all():
|
if m.transaction not in self.transactions.all():
|
||||||
# self.transactions.add(m.transaction)
|
self.transactions.add(m.transaction)
|
||||||
|
|
||||||
if 'wei' in settings.INSTALLED_APPS:
|
if 'wei' in settings.INSTALLED_APPS:
|
||||||
from wei.models import WEIClub
|
from wei.models import WEIClub
|
||||||
@ -388,6 +385,7 @@ class SogeCredit(models.Model):
|
|||||||
for tr in self.transactions.all():
|
for tr in self.transactions.all():
|
||||||
tr.valid = True
|
tr.valid = True
|
||||||
tr._force_save = True
|
tr._force_save = True
|
||||||
|
tr.created_at = timezone.now()
|
||||||
tr.save()
|
tr.save()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@ -436,11 +434,12 @@ class SogeCredit(models.Model):
|
|||||||
for tr in self.transactions.all():
|
for tr in self.transactions.all():
|
||||||
tr._force_save = True
|
tr._force_save = True
|
||||||
tr.valid = True
|
tr.valid = True
|
||||||
|
tr.created_at = timezone.now()
|
||||||
tr.save()
|
tr.save()
|
||||||
if self.credit_transaction:
|
if self.credit_transaction:
|
||||||
# If the soge credit is deleted while the user is not validated yet,
|
# If the soge credit is deleted while the user is not validated yet,
|
||||||
# there is not credit transaction.
|
# there is not credit transaction.
|
||||||
# There is a credit transaction if the user declares that no bank account
|
# There is a credit transaction iff the user declares that no bank account
|
||||||
# was opened after the validation of the account.
|
# was opened after the validation of the account.
|
||||||
self.credit_transaction.valid = False
|
self.credit_transaction.valid = False
|
||||||
self.credit_transaction.reason += " (invalide)"
|
self.credit_transaction.reason += " (invalide)"
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 690 KiB |
Binary file not shown.
Before Width: | Height: | Size: 77 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.0 MiB |
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
@ -105,8 +105,8 @@
|
|||||||
|
|
||||||
\renewcommand{\headrulewidth}{0pt}
|
\renewcommand{\headrulewidth}{0pt}
|
||||||
\cfoot{
|
\cfoot{
|
||||||
\small{\MonNom ~--~ \MonAdresseRue ~ \MonAdresseVille ~--~ Téléphone : +33(0)7 78 17 22 34\newline
|
\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 00029
|
Site web : bde.ens-cachan.fr ~--~ E-mail : tresorerie.bde@lists.crans.org \newline Numéro SIRET : 399 485 838 00011
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
|
|||||||
name="",
|
name="",
|
||||||
address="",
|
address="",
|
||||||
)
|
)
|
||||||
if not PermissionBackend.check_perm(self.request, "treasury.view_invoice", sample_invoice):
|
if not PermissionBackend.check_perm(self.request, "treasury.add_invoice", sample_invoice):
|
||||||
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -38,7 +38,7 @@ class WEIRegistrationForm(forms.ModelForm):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WEIRegistration
|
model = WEIRegistration
|
||||||
exclude = ('wei', 'clothing_cut')
|
exclude = ('wei', )
|
||||||
widgets = {
|
widgets = {
|
||||||
"user": Autocomplete(
|
"user": Autocomplete(
|
||||||
User,
|
User,
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
|
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm
|
||||||
from .wei2023 import WEISurvey2023
|
from .wei2022 import WEISurvey2022
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
|
||||||
]
|
]
|
||||||
|
|
||||||
CurrentSurvey = WEISurvey2023
|
CurrentSurvey = WEISurvey2022
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2022 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@ -14,17 +14,14 @@ from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInf
|
|||||||
from ...models import WEIMembership
|
from ...models import WEIMembership
|
||||||
|
|
||||||
WORDS = [
|
WORDS = [
|
||||||
'ABBA', 'After', 'Alcoolique anonyme', 'Ambiance festive', 'Années 2000', 'Apéro', 'Art',
|
'13 organisé', '3ième mi temps', 'Années 2000', 'Apéro', 'BBQ', 'BP', 'Beauf', 'Binge drinking', 'Bon enfant',
|
||||||
'Baby foot billard biere pong', 'BBQ', 'Before', 'Bière pong', 'Bon enfant', 'Calme', 'Canapé',
|
'Cartouche', 'Catacombes', 'Chansons paillardes', 'Chansons populaires', 'Chanteur', 'Chartreuse', 'Chill',
|
||||||
'Chanson paillarde', 'Chanson populaire', 'Chartreuse', 'Cheerleader', 'Chill', 'Choré',
|
'Core', 'DJ', 'Dancefloor', 'Danse', 'David Guetta', 'Disco', 'Eau de vie', 'Électro', 'Escalade', 'Familial',
|
||||||
'Cinéma', 'Cocktail', 'Comédie musicle', 'Commercial', 'Copaing', 'Danse', 'Dancefloor',
|
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
|
||||||
'Electro', 'Fanfare', 'Gin tonic', 'Inclusif', 'Jazz', "Jeux d'alcool", 'Jeux de carte',
|
'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit',
|
||||||
'Jeux de rôle', 'Jeux de société', 'JUL', 'Jus de fruit', 'Kfet', 'Kleptomanie assurée',
|
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic',
|
||||||
'LGBTQ+', 'Livre', 'Morning beer', 'Musique', 'NAPS', 'Paillettes', 'Pastis', 'Paté Hénaff',
|
'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi',
|
||||||
'Peluche', 'Pena baiona', "Peu d'alcool", 'Pilier de bar', 'PMU', 'Poulpe', 'Punch', 'Rap',
|
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
|
||||||
'Réveil', 'Rock', 'Rugby', 'Sandwich', 'Serge', 'Shot', 'Sociable', 'Spectacle', 'Techno',
|
|
||||||
'Techno house', 'Thérapie Taxi', 'Tradition kchanaises', 'Troisième mi-temps', 'Turn up',
|
|
||||||
'Vodka', 'Vodka pomme', 'Volley', 'Vomi stratégique'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,296 +0,0 @@
|
|||||||
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
import time
|
|
||||||
from functools import lru_cache
|
|
||||||
from random import Random
|
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.db import transaction
|
|
||||||
from django.db.models import Q
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
|
||||||
|
|
||||||
from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, WEIBusInformation
|
|
||||||
from ...models import WEIMembership
|
|
||||||
|
|
||||||
WORDS = [
|
|
||||||
'ABBA', 'After', 'Alcoolique anonyme', 'Ambiance festive', 'Années 2000', 'Apéro', 'Art',
|
|
||||||
'Baby foot billard biere pong', 'BBQ', 'Before', 'Bière pong', 'Bon enfant', 'Calme', 'Canapé',
|
|
||||||
'Chanson paillarde', 'Chanson populaire', 'Chartreuse', 'Cheerleader', 'Chill', 'Choré',
|
|
||||||
'Cinéma', 'Cocktail', 'Comédie musicle', 'Commercial', 'Copaing', 'Danse', 'Dancefloor',
|
|
||||||
'Electro', 'Fanfare', 'Gin tonic', 'Inclusif', 'Jazz', "Jeux d'alcool", 'Jeux de carte',
|
|
||||||
'Jeux de rôle', 'Jeux de société', 'JUL', 'Jus de fruit', 'Kfet', 'Kleptomanie assurée',
|
|
||||||
'LGBTQ+', 'Livre', 'Morning beer', 'Musique', 'NAPS', 'Paillettes', 'Pastis', 'Paté Hénaff',
|
|
||||||
'Peluche', 'Pena baiona', "Peu d'alcool", 'Pilier de bar', 'PMU', 'Poulpe', 'Punch', 'Rap',
|
|
||||||
'Réveil', 'Rock', 'Rugby', 'Sandwich', 'Serge', 'Shot', 'Sociable', 'Spectacle', 'Techno',
|
|
||||||
'Techno house', 'Thérapie Taxi', 'Tradition kchanaises', 'Troisième mi-temps', 'Turn up',
|
|
||||||
'Vodka', 'Vodka pomme', 'Volley', 'Vomi stratégique'
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class WEISurveyForm2023(forms.Form):
|
|
||||||
"""
|
|
||||||
Survey form for the year 2023.
|
|
||||||
Members choose 20 words, from which we calculate the best associated bus.
|
|
||||||
"""
|
|
||||||
|
|
||||||
word = forms.ChoiceField(
|
|
||||||
label=_("Choose a word:"),
|
|
||||||
widget=forms.RadioSelect(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def set_registration(self, registration):
|
|
||||||
"""
|
|
||||||
Filter the bus selector with the buses of the current WEI.
|
|
||||||
"""
|
|
||||||
information = WEISurveyInformation2023(registration)
|
|
||||||
if not information.seed:
|
|
||||||
information.seed = int(1000 * time.time())
|
|
||||||
information.save(registration)
|
|
||||||
registration._force_save = True
|
|
||||||
registration.save()
|
|
||||||
|
|
||||||
if self.data:
|
|
||||||
self.fields["word"].choices = [(w, w) for w in WORDS]
|
|
||||||
if self.is_valid():
|
|
||||||
return
|
|
||||||
|
|
||||||
rng = Random((information.step + 1) * information.seed)
|
|
||||||
|
|
||||||
words = None
|
|
||||||
|
|
||||||
buses = WEISurveyAlgorithm2023.get_buses()
|
|
||||||
informations = {bus: WEIBusInformation2023(bus) for bus in buses}
|
|
||||||
scores = sum((list(informations[bus].scores.values()) for bus in buses), [])
|
|
||||||
average_score = sum(scores) / len(scores)
|
|
||||||
|
|
||||||
preferred_words = {bus: [word for word in WORDS
|
|
||||||
if informations[bus].scores[word] >= average_score]
|
|
||||||
for bus in buses}
|
|
||||||
while words is None or len(set(words)) != len(words):
|
|
||||||
# Ensure that there is no the same word 2 times
|
|
||||||
words = [rng.choice(words) for _ignored2, words in preferred_words.items()]
|
|
||||||
rng.shuffle(words)
|
|
||||||
words = [(w, w) for w in words]
|
|
||||||
self.fields["word"].choices = words
|
|
||||||
|
|
||||||
|
|
||||||
class WEIBusInformation2023(WEIBusInformation):
|
|
||||||
"""
|
|
||||||
For each word, the bus has a score
|
|
||||||
"""
|
|
||||||
scores: dict
|
|
||||||
|
|
||||||
def __init__(self, bus):
|
|
||||||
self.scores = {}
|
|
||||||
for word in WORDS:
|
|
||||||
self.scores[word] = 0.0
|
|
||||||
super().__init__(bus)
|
|
||||||
|
|
||||||
|
|
||||||
class WEISurveyInformation2023(WEISurveyInformation):
|
|
||||||
"""
|
|
||||||
We store the id of the selected bus. We store only the name, but is not used in the selection:
|
|
||||||
that's only for humans that try to read data.
|
|
||||||
"""
|
|
||||||
# Random seed that is stored at the first time to ensure that words are generated only once
|
|
||||||
seed = 0
|
|
||||||
step = 0
|
|
||||||
|
|
||||||
def __init__(self, registration):
|
|
||||||
for i in range(1, 21):
|
|
||||||
setattr(self, "word" + str(i), None)
|
|
||||||
super().__init__(registration)
|
|
||||||
|
|
||||||
|
|
||||||
class WEISurvey2023(WEISurvey):
|
|
||||||
"""
|
|
||||||
Survey for the year 2023.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_year(cls):
|
|
||||||
return 2023
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_survey_information_class(cls):
|
|
||||||
return WEISurveyInformation2023
|
|
||||||
|
|
||||||
def get_form_class(self):
|
|
||||||
return WEISurveyForm2023
|
|
||||||
|
|
||||||
def update_form(self, form):
|
|
||||||
"""
|
|
||||||
Filter the bus selector with the buses of the WEI.
|
|
||||||
"""
|
|
||||||
form.set_registration(self.registration)
|
|
||||||
|
|
||||||
@transaction.atomic
|
|
||||||
def form_valid(self, form):
|
|
||||||
word = form.cleaned_data["word"]
|
|
||||||
self.information.step += 1
|
|
||||||
setattr(self.information, "word" + str(self.information.step), word)
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_algorithm_class(cls):
|
|
||||||
return WEISurveyAlgorithm2023
|
|
||||||
|
|
||||||
def is_complete(self) -> bool:
|
|
||||||
"""
|
|
||||||
The survey is complete once the bus is chosen.
|
|
||||||
"""
|
|
||||||
return self.information.step == 20
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@lru_cache()
|
|
||||||
def word_mean(cls, word):
|
|
||||||
"""
|
|
||||||
Calculate the mid-score given by all buses.
|
|
||||||
"""
|
|
||||||
buses = cls.get_algorithm_class().get_buses()
|
|
||||||
return sum([cls.get_algorithm_class().get_bus_information(bus).scores[word] for bus in buses]) / buses.count()
|
|
||||||
|
|
||||||
@lru_cache()
|
|
||||||
def score(self, bus):
|
|
||||||
if not self.is_complete():
|
|
||||||
raise ValueError("Survey is not ended, can't calculate score")
|
|
||||||
|
|
||||||
bus_info = self.get_algorithm_class().get_bus_information(bus)
|
|
||||||
# Score is the given score by the bus subtracted to the mid-score of the buses.
|
|
||||||
s = sum(bus_info.scores[getattr(self.information, 'word' + str(i))]
|
|
||||||
- self.word_mean(getattr(self.information, 'word' + str(i))) for i in range(1, 21)) / 20
|
|
||||||
return s
|
|
||||||
|
|
||||||
@lru_cache()
|
|
||||||
def scores_per_bus(self):
|
|
||||||
return {bus: self.score(bus) for bus in self.get_algorithm_class().get_buses()}
|
|
||||||
|
|
||||||
@lru_cache()
|
|
||||||
def ordered_buses(self):
|
|
||||||
values = list(self.scores_per_bus().items())
|
|
||||||
values.sort(key=lambda item: -item[1])
|
|
||||||
return values
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def clear_cache(cls):
|
|
||||||
cls.word_mean.cache_clear()
|
|
||||||
return super().clear_cache()
|
|
||||||
|
|
||||||
|
|
||||||
class WEISurveyAlgorithm2023(WEISurveyAlgorithm):
|
|
||||||
"""
|
|
||||||
The algorithm class for the year 2023.
|
|
||||||
We use Gale-Shapley algorithm to attribute 1y students into buses.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_survey_class(cls):
|
|
||||||
return WEISurvey2023
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_bus_information_class(cls):
|
|
||||||
return WEIBusInformation2023
|
|
||||||
|
|
||||||
def run_algorithm(self, display_tqdm=False):
|
|
||||||
"""
|
|
||||||
Gale-Shapley algorithm implementation.
|
|
||||||
We modify it to allow buses to have multiple "weddings".
|
|
||||||
"""
|
|
||||||
surveys = list(self.get_survey_class()(r) for r in self.get_registrations()) # All surveys
|
|
||||||
surveys = [s for s in surveys if s.is_complete()] # Don't consider invalid surveys
|
|
||||||
# Don't manage hardcoded people
|
|
||||||
surveys = [s for s in surveys if not hasattr(s.information, 'hardcoded') or not s.information.hardcoded]
|
|
||||||
|
|
||||||
# Reset previous algorithm run
|
|
||||||
for survey in surveys:
|
|
||||||
survey.free()
|
|
||||||
survey.save()
|
|
||||||
|
|
||||||
non_men = [s for s in surveys if s.registration.gender != 'male']
|
|
||||||
men = [s for s in surveys if s.registration.gender == 'male']
|
|
||||||
|
|
||||||
quotas = {}
|
|
||||||
registrations = self.get_registrations()
|
|
||||||
non_men_total = registrations.filter(~Q(gender='male')).count()
|
|
||||||
for bus in self.get_buses():
|
|
||||||
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
|
|
||||||
# Remove hardcoded people
|
|
||||||
free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
|
|
||||||
registration__information_json__icontains="hardcoded").count()
|
|
||||||
quotas[bus] = 4 + int(non_men_total / registrations.count() * free_seats)
|
|
||||||
|
|
||||||
tqdm_obj = None
|
|
||||||
if display_tqdm:
|
|
||||||
from tqdm import tqdm
|
|
||||||
tqdm_obj = tqdm(total=len(non_men), desc="Non-hommes")
|
|
||||||
|
|
||||||
# Repartition for non men people first
|
|
||||||
self.make_repartition(non_men, quotas, tqdm_obj=tqdm_obj)
|
|
||||||
|
|
||||||
quotas = {}
|
|
||||||
for bus in self.get_buses():
|
|
||||||
free_seats = bus.size - WEIMembership.objects.filter(bus=bus, registration__first_year=False).count()
|
|
||||||
free_seats -= sum(1 for s in non_men if s.information.selected_bus_pk == bus.pk)
|
|
||||||
# Remove hardcoded people
|
|
||||||
free_seats -= WEIMembership.objects.filter(bus=bus, registration__first_year=True,
|
|
||||||
registration__information_json__icontains="hardcoded").count()
|
|
||||||
quotas[bus] = free_seats
|
|
||||||
|
|
||||||
if display_tqdm:
|
|
||||||
tqdm_obj.close()
|
|
||||||
|
|
||||||
from tqdm import tqdm
|
|
||||||
tqdm_obj = tqdm(total=len(men), desc="Hommes")
|
|
||||||
|
|
||||||
self.make_repartition(men, quotas, tqdm_obj=tqdm_obj)
|
|
||||||
|
|
||||||
if display_tqdm:
|
|
||||||
tqdm_obj.close()
|
|
||||||
|
|
||||||
# Clear cache information after running algorithm
|
|
||||||
WEISurvey2023.clear_cache()
|
|
||||||
|
|
||||||
def make_repartition(self, surveys, quotas=None, tqdm_obj=None):
|
|
||||||
free_surveys = surveys.copy() # Remaining surveys
|
|
||||||
while free_surveys: # Some students are not affected
|
|
||||||
survey = free_surveys[0]
|
|
||||||
buses = survey.ordered_buses() # Preferences of the student
|
|
||||||
for bus, current_score in buses:
|
|
||||||
if self.get_bus_information(bus).has_free_seats(surveys, quotas):
|
|
||||||
# Selected bus has free places. Put student in the bus
|
|
||||||
survey.select_bus(bus)
|
|
||||||
survey.save()
|
|
||||||
free_surveys.remove(survey)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Current bus has not enough places. Remove the least preferred student from the bus if existing
|
|
||||||
least_preferred_survey = None
|
|
||||||
least_score = -1
|
|
||||||
# Find the least student in the bus that has a lower score than the current student
|
|
||||||
for survey2 in surveys:
|
|
||||||
if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
|
|
||||||
continue
|
|
||||||
score2 = survey2.score(bus)
|
|
||||||
if current_score <= score2: # Ignore better students
|
|
||||||
continue
|
|
||||||
if least_preferred_survey is None or score2 < least_score:
|
|
||||||
least_preferred_survey = survey2
|
|
||||||
least_score = score2
|
|
||||||
|
|
||||||
if least_preferred_survey is not None:
|
|
||||||
# Remove the least student from the bus and put the current student in.
|
|
||||||
# If it does not exist, choose the next bus.
|
|
||||||
least_preferred_survey.free()
|
|
||||||
least_preferred_survey.save()
|
|
||||||
free_surveys.append(least_preferred_survey)
|
|
||||||
survey.select_bus(bus)
|
|
||||||
survey.save()
|
|
||||||
free_surveys.remove(survey)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError(f"User {survey.registration.user} has no free seat")
|
|
||||||
|
|
||||||
if tqdm_obj is not None:
|
|
||||||
tqdm_obj.n = len(surveys) - len(free_surveys)
|
|
||||||
tqdm_obj.refresh()
|
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 2.2.26 on 2022-09-04 21:25
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('wei', '0003_bus_size'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='weiclub',
|
|
||||||
name='year',
|
|
||||||
field=models.PositiveIntegerField(default=2022, unique=True, verbose_name='year'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,18 +0,0 @@
|
|||||||
# 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'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -209,9 +209,7 @@ class WEIRegistration(models.Model):
|
|||||||
choices=(
|
choices=(
|
||||||
('male', _("Male")),
|
('male', _("Male")),
|
||||||
('female', _("Female")),
|
('female', _("Female")),
|
||||||
('unisex', _("Unisex")),
|
|
||||||
),
|
),
|
||||||
default='unisex',
|
|
||||||
verbose_name=_("clothing cut"),
|
verbose_name=_("clothing cut"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -237,7 +235,6 @@ class WEIRegistration(models.Model):
|
|||||||
emergency_contact_name = models.CharField(
|
emergency_contact_name = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
verbose_name=_("emergency contact name"),
|
verbose_name=_("emergency contact name"),
|
||||||
help_text=_("The emergency contact must not be a WEI participant")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
emergency_contact_phone = PhoneNumberField(
|
emergency_contact_phone = PhoneNumberField(
|
||||||
|
@ -56,7 +56,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
<dd class="col-xl-6">{{ registration.get_gender_display }}</dd>
|
<dd class="col-xl-6">{{ registration.get_gender_display }}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'clothing cut'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'clothing cut'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ registration.get_clothing_cut_display }}</dd>
|
<dd class="col-xl-6">{{ registration.clothing_cut }}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'clothing size'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'clothing size'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ registration.clothing_size }}</dd>
|
<dd class="col-xl-6">{{ registration.clothing_size }}</dd>
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
|
|
||||||
import random
|
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
from ..forms.surveys.wei2023 import WEIBusInformation2023, WEISurvey2023, WORDS, WEISurveyInformation2023
|
|
||||||
from ..models import Bus, WEIClub, WEIRegistration
|
|
||||||
|
|
||||||
|
|
||||||
class TestWEIAlgorithm(TestCase):
|
|
||||||
"""
|
|
||||||
Run some tests to ensure that the WEI algorithm is working well.
|
|
||||||
"""
|
|
||||||
fixtures = ('initial',)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""
|
|
||||||
Create some test data, with one WEI and 10 buses with random score attributions.
|
|
||||||
"""
|
|
||||||
self.wei = WEIClub.objects.create(
|
|
||||||
name="WEI 2023",
|
|
||||||
email="wei2023@example.com",
|
|
||||||
date_start='2023-09-16',
|
|
||||||
date_end='2023-09-18',
|
|
||||||
year=2023,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.buses = []
|
|
||||||
for i in range(10):
|
|
||||||
bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10)
|
|
||||||
self.buses.append(bus)
|
|
||||||
information = WEIBusInformation2023(bus)
|
|
||||||
for word in WORDS:
|
|
||||||
information.scores[word] = random.randint(0, 101)
|
|
||||||
information.save()
|
|
||||||
bus.save()
|
|
||||||
|
|
||||||
def test_survey_algorithm_small(self):
|
|
||||||
"""
|
|
||||||
There are only a few people in each bus, ensure that each person has its best bus
|
|
||||||
"""
|
|
||||||
# Add a few users
|
|
||||||
for i in range(10):
|
|
||||||
user = User.objects.create(username=f"user{i}")
|
|
||||||
registration = WEIRegistration.objects.create(
|
|
||||||
user=user,
|
|
||||||
wei=self.wei,
|
|
||||||
first_year=True,
|
|
||||||
birth_date='2000-01-01',
|
|
||||||
)
|
|
||||||
information = WEISurveyInformation2023(registration)
|
|
||||||
for j in range(1, 21):
|
|
||||||
setattr(information, f'word{j}', random.choice(WORDS))
|
|
||||||
information.step = 20
|
|
||||||
information.save(registration)
|
|
||||||
registration.save()
|
|
||||||
|
|
||||||
# Run algorithm
|
|
||||||
WEISurvey2023.get_algorithm_class()().run_algorithm()
|
|
||||||
|
|
||||||
# Ensure that everyone has its first choice
|
|
||||||
for r in WEIRegistration.objects.filter(wei=self.wei).all():
|
|
||||||
survey = WEISurvey2023(r)
|
|
||||||
preferred_bus = survey.ordered_buses()[0][0]
|
|
||||||
chosen_bus = survey.information.get_selected_bus()
|
|
||||||
self.assertEqual(preferred_bus, chosen_bus)
|
|
||||||
|
|
||||||
def test_survey_algorithm_full(self):
|
|
||||||
"""
|
|
||||||
Buses are full of first year people, ensure that they are happy
|
|
||||||
"""
|
|
||||||
# Add a lot of users
|
|
||||||
for i in range(95):
|
|
||||||
user = User.objects.create(username=f"user{i}")
|
|
||||||
registration = WEIRegistration.objects.create(
|
|
||||||
user=user,
|
|
||||||
wei=self.wei,
|
|
||||||
first_year=True,
|
|
||||||
birth_date='2000-01-01',
|
|
||||||
)
|
|
||||||
information = WEISurveyInformation2023(registration)
|
|
||||||
for j in range(1, 21):
|
|
||||||
setattr(information, f'word{j}', random.choice(WORDS))
|
|
||||||
information.step = 20
|
|
||||||
information.save(registration)
|
|
||||||
registration.save()
|
|
||||||
|
|
||||||
# Run algorithm
|
|
||||||
WEISurvey2023.get_algorithm_class()().run_algorithm()
|
|
||||||
|
|
||||||
penalty = 0
|
|
||||||
# Ensure that everyone seems to be happy
|
|
||||||
# We attribute a penalty for each user that didn't have its first choice
|
|
||||||
# The penalty is the square of the distance between the score of the preferred bus
|
|
||||||
# and the score of the attributed bus
|
|
||||||
# We consider it acceptable if the mean of this distance is lower than 5 %
|
|
||||||
for r in WEIRegistration.objects.filter(wei=self.wei).all():
|
|
||||||
survey = WEISurvey2023(r)
|
|
||||||
chosen_bus = survey.information.get_selected_bus()
|
|
||||||
buses = survey.ordered_buses()
|
|
||||||
score = min(v for bus, v in buses if bus == chosen_bus)
|
|
||||||
max_score = buses[0][1]
|
|
||||||
penalty += (max_score - score) ** 2
|
|
||||||
|
|
||||||
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
|
|
||||||
|
|
||||||
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
|
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (C) 2018-2023 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -782,7 +782,7 @@ class TestDefaultWEISurvey(TestCase):
|
|||||||
WEISurvey.update_form(None, None)
|
WEISurvey.update_form(None, None)
|
||||||
|
|
||||||
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
|
self.assertEqual(CurrentSurvey.get_algorithm_class().get_survey_class(), CurrentSurvey)
|
||||||
self.assertEqual(CurrentSurvey.get_year(), 2023)
|
self.assertEqual(CurrentSurvey.get_year(), 2022)
|
||||||
|
|
||||||
|
|
||||||
class TestWeiAPI(TestAPI):
|
class TestWeiAPI(TestAPI):
|
||||||
|
@ -448,10 +448,6 @@ Options
|
|||||||
"value": "female",
|
"value": "female",
|
||||||
"display_name": "Femme"
|
"display_name": "Femme"
|
||||||
}
|
}
|
||||||
{
|
|
||||||
"value": "unisex",
|
|
||||||
"display_name": "Unisexe"
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"clothing_size": {
|
"clothing_size": {
|
||||||
|
@ -118,13 +118,13 @@ Exemples
|
|||||||
{"F": [
|
{"F": [
|
||||||
"ADD",
|
"ADD",
|
||||||
["F", "source__balance"],
|
["F", "source__balance"],
|
||||||
2000]
|
5000]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
| 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 €,
|
| 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 -20 €.
|
autrement dit le solde final est au-dessus de -50 €.
|
||||||
|
|
||||||
|
|
||||||
Masques de permissions
|
Masques de permissions
|
||||||
|
@ -83,6 +83,13 @@ 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é
|
bien sûr permis pour faciliter des transferts. Tout abus de droits constaté
|
||||||
pourra mener à des sanctions prises par le bureau du BDE.
|
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
|
Je suis trésorier d'un club, je n'arrive pas à voir le solde du club / faire des transactions
|
||||||
---------------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -18,7 +18,7 @@ MAILTO=notekfet2020@lists.crans.org
|
|||||||
# Spammer les gens en négatif
|
# 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
|
00 5 * * 2 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --spam --negative-amount 1 -v 0
|
||||||
# Envoyer le rapport mensuel aux trésoriers et respos info
|
# Envoyer le rapport mensuel aux trésoriers et respos info
|
||||||
00 8 * * 5 root cd /var/www/note_kfet && env/bin/python manage.py send_mail_to_negative_balances --report --add-years 1 -v 0
|
00 8 6 * * 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
|
# Envoyer les rapports aux gens
|
||||||
55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0
|
55 6 * * * root cd /var/www/note_kfet && env/bin/python manage.py send_reports -v 0
|
||||||
# Mettre à jour les boutons mis en avant
|
# Mettre à jour les boutons mis en avant
|
||||||
|
@ -252,7 +252,7 @@ REST_FRAMEWORK = {
|
|||||||
'rest_framework.authentication.TokenAuthentication',
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||||
],
|
],
|
||||||
'DEFAULT_PAGINATION_CLASS': 'apps.api.pagination.CustomPagination',
|
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
|
||||||
'PAGE_SIZE': 20,
|
'PAGE_SIZE': 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
72
note_kfet/static/css/custom.css
Executable file → Normal file
72
note_kfet/static/css/custom.css
Executable file → Normal file
@ -65,10 +65,7 @@ mark {
|
|||||||
|
|
||||||
/* Last BDE colors */
|
/* Last BDE colors */
|
||||||
.bg-primary {
|
.bg-primary {
|
||||||
/* background-color: rgb(18, 67, 4) !important; */
|
background-color: rgb(18, 67, 4) !important;
|
||||||
/* MODE VIEUXCON=ON */
|
|
||||||
/* background-color: rgb(166, 0, 2) !important; */
|
|
||||||
background-color: rgb(0, 0, 0) !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
@ -83,15 +80,15 @@ body {
|
|||||||
.btn-outline-primary:hover,
|
.btn-outline-primary:hover,
|
||||||
.btn-outline-primary:not(:disabled):not(.disabled).active,
|
.btn-outline-primary:not(:disabled):not(.disabled).active,
|
||||||
.btn-outline-primary:not(:disabled):not(.disabled):active {
|
.btn-outline-primary:not(:disabled):not(.disabled):active {
|
||||||
color: rgb(241, 229, 52);
|
color: #fff;
|
||||||
background-color: rgb(228, 35, 132);
|
background-color: rgb(18, 67, 46);
|
||||||
border-color: rgb(228, 35, 132);
|
border-color: rgb(18, 67, 46);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-primary {
|
.btn-outline-primary {
|
||||||
color: #fff;
|
color: rgb(18, 67, 46);
|
||||||
background-color: #000;
|
background-color: rgba(248, 249, 250, 0.9);
|
||||||
border-color: #464647;
|
border-color: rgb(18, 67, 46);
|
||||||
}
|
}
|
||||||
|
|
||||||
.turbolinks-progress-bar {
|
.turbolinks-progress-bar {
|
||||||
@ -101,63 +98,36 @@ body {
|
|||||||
.btn-primary:hover,
|
.btn-primary:hover,
|
||||||
.btn-primary:not(:disabled):not(.disabled).active,
|
.btn-primary:not(:disabled):not(.disabled).active,
|
||||||
.btn-primary:not(:disabled):not(.disabled):active {
|
.btn-primary:not(:disabled):not(.disabled):active {
|
||||||
color: rgb(241, 229, 52);
|
color: #fff;
|
||||||
background-color: rgb(228, 35, 132);
|
background-color: rgb(18, 67, 46);
|
||||||
border-color: rgb(228, 35, 132);
|
border-color: rgb(18, 67, 46);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
color: #fff;
|
color: rgba(248, 249, 250, 0.9);
|
||||||
background-color: #000;
|
background-color: rgb(28, 114, 10);
|
||||||
border-color: #adb5bd;
|
border-color: rgb(18, 67, 46);
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-primary {
|
.border-primary {
|
||||||
border-color: rgb(228, 35, 132) !important;
|
border-color: rgb(28, 114, 10) !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 {
|
a {
|
||||||
color: rgb(228, 35, 132);
|
color: rgb(28, 114, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: rgb(228, 35, 132);
|
color: rgb(122, 163, 75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
box-shadow: 0 0 0 0.25rem rgb(228 35 132 / 50%);
|
box-shadow: 0 0 0 0.25rem rgba(122, 163, 75, 0.25);
|
||||||
border-color: rgb(228, 35, 132);
|
border-color: rgb(122, 163, 75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-primary.focus {
|
.btn-outline-primary.focus {
|
||||||
box-shadow: 0 0 0 0.25rem rgb(228 35 132 / 10%);
|
box-shadow: 0 0 0 0.25rem rgba(122, 163, 75, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,29 +13,21 @@ $(document).ready(function () {
|
|||||||
$('#' + prefix + '_reset').removeClass('d-none')
|
$('#' + prefix + '_reset').removeClass('d-none')
|
||||||
|
|
||||||
$.getJSON(api_url + (api_url.includes('?') ? '&' : '?') + 'format=json&search=^' + input + api_url_suffix, function (objects) {
|
$.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">'
|
let html = ''
|
||||||
|
|
||||||
objects.results.forEach(function (obj) {
|
objects.results.forEach(function (obj) {
|
||||||
html += li(prefix + '_' + obj.id, obj[name_field])
|
html += li(prefix + '_' + obj.id, obj[name_field])
|
||||||
})
|
})
|
||||||
html += '</ul>'
|
|
||||||
|
|
||||||
target.tooltip({
|
const results_list = $('#' + prefix + '_list')
|
||||||
html: true,
|
results_list.html(html)
|
||||||
placement: 'bottom',
|
|
||||||
trigger: 'manual',
|
|
||||||
container: target.parent(),
|
|
||||||
fallbackPlacement: 'clockwise'
|
|
||||||
})
|
|
||||||
|
|
||||||
target.attr("data-original-title", html).tooltip("show")
|
|
||||||
|
|
||||||
objects.results.forEach(function (obj) {
|
objects.results.forEach(function (obj) {
|
||||||
$('#' + prefix + '_' + obj.id).click(function () {
|
$('#' + prefix + '_' + obj.id).click(function () {
|
||||||
target.val(obj[name_field])
|
target.val(obj[name_field])
|
||||||
$('#' + prefix + '_pk').val(obj.id)
|
$('#' + prefix + '_pk').val(obj.id)
|
||||||
|
|
||||||
target.tooltip("hide")
|
results_list.html('')
|
||||||
target.removeClass('is-invalid')
|
target.removeClass('is-invalid')
|
||||||
target.addClass('is-valid')
|
target.addClass('is-valid')
|
||||||
|
|
||||||
@ -45,8 +37,8 @@ $(document).ready(function () {
|
|||||||
if (input === obj[name_field]) { $('#' + prefix + '_pk').val(obj.id) }
|
if (input === obj[name_field]) { $('#' + prefix + '_pk').val(obj.id) }
|
||||||
})
|
})
|
||||||
|
|
||||||
if (objects.results.length === 1 && e.originalEvent.keyCode >= 32) {
|
if (results_list.children().length === 1 && e.originalEvent.keyCode >= 32) {
|
||||||
$('#' + prefix + '_' + objects.results[0].id).trigger('click')
|
results_list.children().first().trigger('click')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -96,7 +96,7 @@ function displayStyle (note) {
|
|||||||
if (!note) { return '' }
|
if (!note) { return '' }
|
||||||
const balance = note.balance
|
const balance = note.balance
|
||||||
var css = ''
|
var css = ''
|
||||||
if (balance < -2000) { css += ' text-danger bg-dark' }
|
if (balance < -5000) { css += ' text-danger bg-dark' }
|
||||||
else if (balance < -1000) { css += ' text-danger' }
|
else if (balance < -1000) { css += ' text-danger' }
|
||||||
else if (balance < 0) { css += ' text-warning' }
|
else if (balance < 0) { css += ' text-warning' }
|
||||||
if (!note.email_confirmed) { css += ' bg-primary' }
|
if (!note.email_confirmed) { css += ' bg-primary' }
|
||||||
|
@ -9,9 +9,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
name="{{ widget.name }}_name" autocomplete="off"
|
name="{{ widget.name }}_name" autocomplete="off"
|
||||||
{% for name, value in widget.attrs.items %}
|
{% for name, value in widget.attrs.items %}
|
||||||
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
|
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
|
||||||
{% endfor %}
|
{% endfor %}>
|
||||||
aria-describedby="{{widget.attrs.id}}_tooltip">
|
|
||||||
{% if widget.resetable %}
|
{% 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{% if not widget.value %} d-none{% endif %}">{% trans "Reset" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<ul class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
|
||||||
|
</ul>
|
||||||
|
@ -194,8 +194,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
class="text-muted">{% trans "Contact us" %}</a> —
|
class="text-muted">{% trans "Contact us" %}</a> —
|
||||||
<a href="mailto:{{ "SUPPORT_EMAIL" | getenv }}"
|
<a href="mailto:{{ "SUPPORT_EMAIL" | getenv }}"
|
||||||
class="text-muted">{% trans "Technical Support" %}</a> —
|
class="text-muted">{% trans "Technical Support" %}</a> —
|
||||||
<a href="https://note.crans.org/doc/faq/"
|
|
||||||
class="text-muted">{% trans "FAQ (FR)" %}</a> —
|
|
||||||
</span>
|
</span>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<select title="language" name="language"
|
<select title="language" name="language"
|
||||||
|
@ -23,11 +23,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{ form|crispy }}
|
{{ form|crispy }}
|
||||||
{{ profile_form|crispy }}
|
{{ profile_form|crispy }}
|
||||||
{% comment "Soge not for membership (only WEI)" %} {{ soge_form|crispy }} {% endcomment %}
|
{{ soge_form|crispy }}
|
||||||
<button class="btn btn-success" type="submit">
|
<button class="btn btn-success" type="submit">
|
||||||
{% trans "Sign up" %}
|
{% trans "Sign up" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
Reference in New Issue
Block a user