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

Compare commits

..

1 Commits

Author SHA1 Message Date
466cbd9878 Replace Font Awesome with inline SVG icons
Font Awesome 4 adds 106kB of dependencies on each page and require to
query multiple assets. It also sometimes causes icons to appear after
page loading. Font Awesome 4 is deprecated and replaced by version 5
which is not packaged in every GNU/Linux distributions.

This commit replaces icons with inline SVG which does not require
external assets, does not require an additionnal dependency and is
widely supported by modern browsers. It makes the page loading faster
and enables us to no longer require fonts-font-awesome Debian package.
2021-10-06 17:15:33 +02:00
100 changed files with 1727 additions and 4105 deletions

1
.gitignore vendored
View File

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

View File

@ -12,7 +12,7 @@ RUN apt-get update && \
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \ python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools \ python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \ uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \ texlive-xetex gettext libjs-bootstrap4 && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Instal PyPI requirements # Instal PyPI requirements

View File

@ -1,8 +1,8 @@
# NoteKfet 2020 # NoteKfet 2020
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt)
[![pipeline status](https://gitlab.crans.org/bde/nk20/badges/main/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/main) [![pipeline status](https://gitlab.crans.org/bde/nk20/badges/master/pipeline.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
[![coverage report](https://gitlab.crans.org/bde/nk20/badges/main/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/main) [![coverage report](https://gitlab.crans.org/bde/nk20/badges/master/coverage.svg)](https://gitlab.crans.org/bde/nk20/commits/master)
## Table des matières ## Table des matières
@ -23,7 +23,7 @@ Bien que cela permette de créer une instance sur toutes les distributions,
$ sudo apt update $ sudo apt update
$ sudo apt install --no-install-recommends -y \ $ sudo apt install --no-install-recommends -y \
ipython3 python3-setuptools python3-venv python3-dev \ ipython3 python3-setuptools python3-venv python3-dev \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git texlive-xetex gettext libjs-bootstrap4 git
``` ```
2. **Clonage du dépot** là où vous voulez : 2. **Clonage du dépot** là où vous voulez :
@ -115,7 +115,7 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \ python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools python3-docutils \ python3-bs4 python3-setuptools python3-docutils \
memcached uwsgi uwsgi-plugin-python3 \ memcached uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \ texlive-xetex gettext libjs-bootstrap4 \
nginx python3-venv git acl nginx python3-venv git acl
``` ```

View File

@ -7,7 +7,7 @@
prompt: "Password of the database (leave it blank to skip database init)" prompt: "Password of the database (leave it blank to skip database init)"
private: yes private: yes
vars: vars:
mirror: eclats.crans.org mirror: mirror.crans.org
roles: roles:
- 1-apt-basic - 1-apt-basic
- 2-nk20 - 2-nk20

View File

@ -1,7 +1,7 @@
--- ---
note: note:
server_name: note.crans.org server_name: note.crans.org
git_branch: main git_branch: master
serve_static: true serve_static: true
cron_enabled: true cron_enabled: true
email: notekfet2020@lists.crans.org email: notekfet2020@lists.crans.org

View File

@ -1,15 +1,14 @@
--- ---
- name: Add buster-backports to apt sources if needed - name: Add buster-backports to apt sources
apt_repository: apt_repository:
repo: deb http://{{ mirror }}/debian buster-backports main repo: deb http://{{ mirror }}/debian buster-backports main
state: present state: present
when: when: ansible_facts['distribution'] == "Debian"
- ansible_distribution == "Debian"
- ansible_distribution_major_version | int == 10
- name: Install note_kfet APT dependencies - name: Install note_kfet APT dependencies
apt: apt:
update_cache: true update_cache: true
default_release: "{{ 'buster-backports' if ansible_facts['distribution'] == 'Debian' }}"
install_recommends: false install_recommends: false
name: name:
# Common tools # Common tools
@ -18,7 +17,6 @@
- ipython3 - ipython3
# Front-end dependencies # Front-end dependencies
- fonts-font-awesome
- libjs-bootstrap4 - libjs-bootstrap4
# Python dependencies # Python dependencies

View File

@ -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
}
} }
] ]

View File

@ -34,7 +34,9 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %} {% endif %}
<div class="card-footer"> <div class="card-footer">
<a class="btn btn-sm btn-success" href="{% url 'activity:activity_create' %}" data-turbolinks="false"> <a class="btn btn-sm btn-success" href="{% url 'activity:activity_create' %}" data-turbolinks="false">
<i class="fa fa-calendar-plus-o" aria-hidden="true"></i> <svg class="bi bi-calendar-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 .5a.5.5 0 0 0-1 0V1H2a2 2 0 0 0-2 2v1h16V3a2 2 0 0 0-2-2h-1V.5a.5.5 0 0 0-1 0V1H4V.5zM16 14V5H0v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2zM8.5 8.5V10H10a.5.5 0 0 1 0 1H8.5v1.5a.5.5 0 0 1-1 0V11H6a.5.5 0 0 1 0-1h1.5V8.5a.5.5 0 0 1 1 0z"/>
</svg>
{% trans 'New activity' %} {% trans 'New activity' %}
</a> </a>
</div> </div>

View File

@ -168,9 +168,6 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
Don't display the entry interface if the user has no right to see it (no right to add an entry for itself), Don't display the entry interface if the user has no right to see it (no right to add an entry for itself),
it is closed or doesn't manage entries. it is closed or doesn't manage entries.
""" """
if not self.request.user.is_authenticated:
return self.handle_no_permission()
activity = Activity.objects.get(pk=self.kwargs["pk"]) activity = Activity.objects.get(pk=self.kwargs["pk"])
sample_entry = Entry(activity=activity, note=self.request.user.note) sample_entry = Entry(activity=activity, note=self.request.user.note)

View File

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

View File

@ -7,11 +7,8 @@ from django.contrib.auth.models import User
from django.utils import timezone from django.utils import timezone
from rest_framework import serializers from rest_framework import serializers
from member.api.serializers import ProfileSerializer, MembershipSerializer from member.api.serializers import ProfileSerializer, MembershipSerializer
from member.models import Membership
from note.api.serializers import NoteSerializer from note.api.serializers import NoteSerializer
from note.models import Alias from note.models import Alias
from note_kfet.middlewares import get_current_request
from permission.backends import PermissionBackend
class UserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer):
@ -48,30 +45,18 @@ class OAuthSerializer(serializers.ModelSerializer):
""" """
normalized_name = serializers.SerializerMethodField() normalized_name = serializers.SerializerMethodField()
profile = serializers.SerializerMethodField() profile = ProfileSerializer()
note = serializers.SerializerMethodField() note = NoteSerializer()
memberships = serializers.SerializerMethodField() memberships = serializers.SerializerMethodField()
def get_normalized_name(self, obj): def get_normalized_name(self, obj):
return Alias.normalize(obj.username) return Alias.normalize(obj.username)
def get_profile(self, obj):
# Display the profile of the user only if we have rights to see it.
return ProfileSerializer().to_representation(obj.profile) \
if PermissionBackend.check_perm(get_current_request(), 'member.view_profile', obj.profile) else None
def get_note(self, obj):
# Display the note of the user only if we have rights to see it.
return NoteSerializer().to_representation(obj.note) \
if PermissionBackend.check_perm(get_current_request(), 'note.view_note', obj.note) else None
def get_memberships(self, obj): def get_memberships(self, obj):
# Display only memberships that we are allowed to see.
return serializers.ListSerializer(child=MembershipSerializer()).to_representation( return serializers.ListSerializer(child=MembershipSerializer()).to_representation(
obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()) obj.memberships.filter(date_start__lte=timezone.now(), date_end__gte=timezone.now()))
.filter(PermissionBackend.filter_queryset(get_current_request(), Membership, 'view')))
class Meta: class Meta:
model = User model = User

View File

@ -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:

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.24 on 2021-10-05 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('member', '0007_auto_20210313_1235'),
]
operations = [
migrations.AlterField(
model_name='profile',
name='department',
field=models.CharField(choices=[('A0', 'Informatics (A0)'), ('A1', 'Mathematics (A1)'), ('A2', 'Physics (A2)'), ("A'2", "Applied physics (A'2)"), ("A''2", "Chemistry (A''2)"), ('A3', 'Biology (A3)'), ('B1234', 'SAPHIRE (B1234)'), ('B1', 'Mechanics (B1)'), ('B2', 'Civil engineering (B2)'), ('B3', 'Mechanical engineering (B3)'), ('B4', 'EEA (B4)'), ('C', 'Design (C)'), ('D2', 'Economy-management (D2)'), ('D3', 'Social sciences (D3)'), ('E', 'English (E)'), ('EXT', 'External (EXT)')], max_length=8, verbose_name='department'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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):
""" """
@ -263,16 +258,14 @@ class Club(models.Model):
This function is called each time the club detail view is displayed. This function is called each time the club detail view is displayed.
Update the year of the membership dates. Update the year of the membership dates.
""" """
if not self.membership_start or not self.membership_end: if not self.membership_start:
return return
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:
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)
if self.membership_end:
self.membership_end = datetime.date(self.membership_end.year + 1, self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day) self.membership_end.month, self.membership_end.day)
self._force_save = True self._force_save = True

View File

@ -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)
})

View File

@ -120,7 +120,7 @@ class MembershipTable(tables.Table):
club=record.club, club=record.club,
user=record.user, user=record.user,
date_start__gte=record.club.membership_start, date_start__gte=record.club.membership_start,
date_end__lte=record.club.membership_end or date(9999, 12, 31), date_end__lte=record.club.membership_end,
).exists(): # If the renew is not yet performed ).exists(): # If the renew is not yet performed
empty_membership = Membership( empty_membership = Membership(
club=record.club, club=record.club,

View File

@ -45,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-footer"> <div class="card-footer">
{% if user_object %} {% if user_object %}
<a class="btn btn-sm btn-secondary" href="{% url 'member:user_update_profile' user_object.pk %}"> <a class="btn btn-sm btn-secondary" href="{% url 'member:user_update_profile' user_object.pk %}">
<i class="fa fa-edit"></i> {% trans 'Update Profile' %} <svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans 'Update Profile' %}
</a> </a>
{% url 'member:user_detail' user_object.pk as user_profile_url %} {% url 'member:user_detail' user_object.pk as user_profile_url %}
{% if request.path_info != user_profile_url %} {% if request.path_info != user_profile_url %}
@ -59,7 +62,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if ".change_"|has_perm:club %} {% if ".change_"|has_perm:club %}
<a class="btn btn-sm btn-secondary" href="{% url 'member:club_update' pk=club.pk %}" <a class="btn btn-sm btn-secondary" href="{% url 'member:club_update' pk=club.pk %}"
data-turbolinks="false"> data-turbolinks="false">
<i class="fa fa-edit"></i> {% trans 'Update Profile' %} <svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans 'Update Profile' %}
</a> </a>
{% endif %} {% endif %}
{% url 'member:club_detail' club.pk as club_detail_url %} {% url 'member:club_detail' club.pk as club_detail_url %}

View File

@ -10,7 +10,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold"> <a class="font-weight-bold">
<i class="fa fa-users"></i> {% trans "Club managers" %} <svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
{% trans "Club managers" %}
</a> </a>
</div> </div>
{% render_table managers %} {% render_table managers %}
@ -23,7 +26,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}"> <a class="stretched-link font-weight-bold" href="{% url 'member:club_members' pk=club.pk %}">
<i class="fa fa-users"></i> {% trans "Club members" %} <svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</svg>
{% trans "Club members" %}
</a> </a>
</div> </div>
{% render_table member_list %} {% render_table member_list %}
@ -37,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-header position-relative" id="historyListHeading"> <div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %} <a class="stretched-link font-weight-bold" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}> href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %} <svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
</svg>
{% trans "Transaction history" %}
</a> </a>
</div> </div>
<div id="history_list"> <div id="history_list">

View File

@ -47,7 +47,9 @@
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
<dd class="col-xl-6"> <dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}"> <a class="badge badge-secondary" href="{% url 'member:club_alias' club.pk %}">
<i class="fa fa-edit"></i> <svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans 'Manage aliases' %} ({{ club.note.alias.all|length }}) {% trans 'Manage aliases' %} ({{ club.note.alias.all|length }})
</a> </a>
</dd> </dd>

View File

@ -11,7 +11,9 @@
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
<dd class="col-xl-6"> <dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'password_change' %}"> <a class="badge badge-secondary" href="{% url 'password_change' %}">
<i class="fa fa-lock"></i> <svg class="bi bi-lock" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2z"/>
</svg>
{% trans 'Change password' %} {% trans 'Change password' %}
</a> </a>
</dd> </dd>
@ -20,19 +22,13 @@
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
<dd class="col-xl-6"> <dd class="col-xl-6">
<a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}"> <a class="badge badge-secondary" href="{% url 'member:user_alias' user_object.pk %}">
<i class="fa fa-edit"></i> <svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }}) {% trans 'Manage aliases' %} ({{ user_object.note.alias.all|length }})
</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 +56,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' %} <svg class="bi bi-cogs" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
</svg>
{% trans 'API token' %}
</a> </a>
</div> </div>
{% endif %} {% endif %}

View File

@ -18,7 +18,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card bg-light mb-3"> <div class="card bg-light mb-3">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold"> <a class="font-weight-bold">
<i class="fa fa-users"></i> {% trans "View my memberships" %} <svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
{% trans "View my memberships" %}
</a> </a>
</div> </div>
{% render_table club_list %} {% render_table club_list %}
@ -29,7 +32,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a class="stretched-link font-weight-bold text-decoration-none" <a class="stretched-link font-weight-bold text-decoration-none"
{% if "note.view_note"|has_perm:user_object.note %} {% if "note.view_note"|has_perm:user_object.note %}
href="{% url 'note:transactions' pk=user_object.note.pk %}" {% endif %}> href="{% url 'note:transactions' pk=user_object.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %} <svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
</svg>
{% trans "Transaction history" %}
</a> </a>
</div> </div>
<div id="history_list"> <div id="history_list">

View File

@ -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%}

View File

@ -7,7 +7,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block content %} {% block content %}
{% if can_manage_registrations %} {% if can_manage_registrations %}
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}"> <a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
<i class="fa fa-user-plus"></i> {% trans "Registrations" %} <svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
</svg>
{% trans "Registrations" %}
</a> </a>
{% endif %} {% endif %}

View File

@ -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())

View File

@ -23,6 +23,5 @@ 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'),
] ]

View File

@ -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
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
@ -175,7 +174,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note = NoteUser.objects.get(pk=user.note.pk) modified_note = NoteUser.objects.get(pk=user.note.pk)
# Don't log these tests # Don't log these tests
modified_note._no_signal = True modified_note._no_signal = True
modified_note.is_active = False modified_note.is_active = True
modified_note.inactivity_reason = 'manual' modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\ context["can_lock_note"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note) .check_perm(self.request, "note.change_noteuser_is_active", modified_note)
@ -184,14 +183,14 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
modified_note._force_save = True modified_note._force_save = True
modified_note.save() modified_note.save()
context["can_force_lock"] = user.note.is_active and PermissionBackend\ context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note) .check_perm(self.request, "note.change_note_is_active", modified_note)
old_note._force_save = True old_note._force_save = True
old_note._no_signal = True old_note._no_signal = True
old_note.save() old_note.save()
modified_note.refresh_from_db() modified_note.refresh_from_db()
modified_note.is_active = True modified_note.is_active = True
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\ context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
.check_perm(self.request, "note.change_noteuser_is_active", modified_note) .check_perm(self.request, "note.change_note_is_active", modified_note)
return context return context
@ -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.
@ -290,8 +256,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
note = context['object'].note note = context['object'].note
context["aliases"] = AliasTable( context["aliases"] = AliasTable(
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct() note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
.order_by('normalized_name').all())
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias( context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
note=context["object"].note, note=context["object"].note,
name="", name="",
@ -438,12 +403,9 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
""" """
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
club = self.object club = context["club"]
context["note"] = club.note
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club): if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
club.update_membership_dates() club.update_membership_dates()
# managers list # managers list
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club", managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
date_start__lte=date.today(), date_end__gte=date.today())\ date_start__lte=date.today(), date_end__gte=date.today())\
@ -481,29 +443,6 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["can_add_members"] = PermissionBackend()\ context["can_add_members"] = PermissionBackend()\
.has_perm(self.request.user, "member.add_membership", empty_membership) .has_perm(self.request.user, "member.add_membership", empty_membership)
# Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic():
modified_note = NoteClub.objects.get(pk=club.note.pk)
# Don't log these tests
modified_note._no_signal = True
modified_note.is_active = False
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
old_note = NoteClub.objects.select_for_update().get(pk=club.note.pk)
modified_note.inactivity_reason = 'forced'
modified_note._force_save = True
modified_note.save()
context["can_force_lock"] = club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
old_note._force_save = True
old_note._no_signal = True
old_note.save()
modified_note.refresh_from_db()
modified_note.is_active = True
context["can_unlock_note"] = not club.note.is_active and PermissionBackend \
.check_perm(self.request, "note.change_noteclub_is_active", modified_note)
return context return context
@ -753,10 +692,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

View File

@ -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.

View File

@ -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)

View File

@ -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

View File

@ -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')},
},
),
]

View File

@ -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',

View File

@ -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.

View File

@ -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):

View File

@ -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) {

View File

@ -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()

View File

@ -4,13 +4,13 @@
import html import html
import django_tables2 as tables import django_tables2 as tables
from django.utils.html import format_html, mark_safe from django.utils.html import format_html
from django_tables2.utils import A from django_tables2.utils import A
from django.utils.translation import gettext_lazy as _ 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 = {
@ -222,17 +197,6 @@ class ButtonTable(tables.Table):
verbose_name=_("Edit"), verbose_name=_("Edit"),
) )
hideshow = tables.Column(
verbose_name=_("Hide/Show"),
accessor="pk",
attrs={
'td': {
'class': 'col-sm-1',
'id': lambda record: "hideshow_" + str(record.pk),
}
},
)
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')}, extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}}, attrs={'td': {'class': 'col-sm-1'}},
@ -240,16 +204,3 @@ class ButtonTable(tables.Table):
def render_amount(self, value): def render_amount(self, value):
return pretty_money(value) return pretty_money(value)
def order_category(self, queryset, is_descending):
return queryset.order_by(f"{'-' if is_descending else ''}category__name"), True
def render_hideshow(self, record):
val = '<button id="'
val += str(record.pk)
val += '" class="btn btn-secondary btn-sm" \
onclick="hideshow(' + str(record.id) + ',' + \
str(record.display).lower() + ')">'
val += str(_("Hide/Show"))
val += '</button>'
return mark_safe(val)

View File

@ -129,7 +129,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
{# Mode switch #} {# Mode switch #}
<div class="card-footer border-primary"> <div class="card-footer border-primary">
<a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}"> <a class="btn btn-sm btn-secondary float-left" href="{% url 'note:template_list' %}">
<i class="fa fa-edit"></i> {% trans "Edit" %} <svg class="bi bi-edit" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{% trans "Edit" %}
</a> </a>
<div class="btn-group btn-group-toggle float-right" data-toggle="buttons"> <div class="btn-group btn-group-toggle float-right" data-toggle="buttons">
<label for="single_conso" class="btn btn-sm btn-outline-primary active"> <label for="single_conso" class="btn btn-sm btn-outline-primary active">

View File

@ -31,9 +31,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% block extrajavascript %} {% block extrajavascript %}
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() {
let searchbar_obj = $("#search_field");
let timer_on = false;
let timer;
function refreshMatchedWords() { function refreshMatchedWords() {
$("tr").each(function() { $("tr").each(function() {
let pattern = $('#search_field').val(); let pattern = searchbar_obj.val();
if (pattern) { if (pattern) {
$(this).find("td:eq(0), td:eq(1), td:eq(3), td:eq(6)").each(function () { $(this).find("td:eq(0), td:eq(1), td:eq(3), td:eq(6)").each(function () {
$(this).html($(this).text().replace(new RegExp(pattern, 'i'), "<mark>$&</mark>")); $(this).html($(this).text().replace(new RegExp(pattern, 'i'), "<mark>$&</mark>"));
@ -42,18 +47,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
}); });
} }
refreshMatchedWords();
function reloadTable() { function reloadTable() {
let pattern = $('#search_field').val(); let pattern = searchbar_obj.val();
$("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table", refreshMatchedWords); $("#buttons_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #buttons_table", refreshMatchedWords);
} }
$(document).ready(function() {
let searchbar_obj = $("#search_field");
let timer_on = false;
let timer;
refreshMatchedWords();
searchbar_obj.keyup(function() { searchbar_obj.keyup(function() {
if (timer_on) if (timer_on)
clearTimeout(timer); clearTimeout(timer);
@ -77,28 +77,5 @@ SPDX-License-Identifier: GPL-3.0-or-later
addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger') addMsg('{% trans "Unable to delete button "%} #' + button_id, 'danger')
}); });
} }
// on click of button "hide/show", call the API
function hideshow(id, displayed) {
$.ajax({
url: '/api/note/transaction/template/' + id + '/',
type: 'PATCH',
dataType: 'json',
headers: {
'X-CSRFTOKEN': CSRF_TOKEN
},
data: {
display: !displayed
},
success: function() {
if(displayed)
addMsg("{% trans "Button hidden"%}", 'success', 1000)
else addMsg("{% trans "Button displayed"%}", 'success', 1000)
reloadTable()
},
error: function (err) {
addMsg("{% trans "An error occured"%}", 'danger')
}})
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -90,9 +90,9 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
if "search" in self.request.GET: if "search" in self.request.GET:
pattern = self.request.GET["search"] pattern = self.request.GET["search"]
qs = qs.filter( qs = qs.filter(
Q(name__iregex=pattern) Q(name__iregex="^" + pattern)
| Q(destination__club__name__iregex=pattern) | Q(destination__club__name__iregex="^" + pattern)
| Q(category__name__iregex=pattern) | Q(category__name__iregex="^" + pattern)
| Q(description__iregex=pattern) | Q(description__iregex=pattern)
) )

File diff suppressed because it is too large Load Diff

View File

@ -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'),
),
]

View File

@ -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,
) )

View File

@ -1,6 +1,6 @@
# 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 oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.scopes import BaseScopes from oauth2_provider.scopes import BaseScopes
from member.models import Club from member.models import Club
from note_kfet.middlewares import get_current_request from note_kfet.middlewares import get_current_request
@ -32,26 +32,3 @@ class PermissionScopes(BaseScopes):
return [] return []
return [f"{p.id}_{p.membership.club.id}" return [f"{p.id}_{p.membership.club.id}"
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')] for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
class PermissionOAuth2Validator(OAuth2Validator):
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
"""
User can request as many scope as he wants, including invalid scopes,
but it will have only the permissions he has.
This allows clients to request more permission to get finally a
subset of permissions.
"""
valid_scopes = set()
for t in Permission.PERMISSION_TYPES:
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0]):
scope = f"{p.id}_{p.membership.club.id}"
if scope in scopes:
valid_scopes.add(scope)
request.scopes = valid_scopes
return valid_scopes

View File

@ -11,25 +11,25 @@
<div class="accordion" id="accordionApps"> <div class="accordion" id="accordionApps">
{% for app, app_scopes in scopes.items %} {% for app, app_scopes in scopes.items %}
<div class="card"> <div class="card">
<div class="card-header" id="app-{{ app.name|slugify }}-title"> <div class="card-header" id="app-{{ app.name.lower }}-title">
<a class="text-decoration-none collapsed" href="#" data-toggle="collapse" <a class="text-decoration-none collapsed" href="#" data-toggle="collapse"
data-target="#app-{{ app.name|slugify }}" aria-expanded="false" data-target="#app-{{ app.name.lower }}" aria-expanded="false"
aria-controls="app-{{ app.name|slugify }}"> aria-controls="app-{{ app.name.lower }}">
{{ app.name }} {{ app.name }}
</a> </a>
</div> </div>
<div class="collapse" id="app-{{ app.name|slugify }}" aria-labelledby="app-{{ app.name|slugify }}" data-target="#accordionApps"> <div class="collapse" id="app-{{ app.name.lower }}" aria-labelledby="app-{{ app.name.lower }}" data-target="#accordionApps">
<div class="card-body"> <div class="card-body">
{% for scope_id, scope_desc in app_scopes.items %} {% for scope_id, scope_desc in app_scopes.items %}
<div class="form-group"> <div class="form-group">
<label class="form-check-label" for="scope-{{ app.name|slugify }}-{{ scope_id }}"> <label class="form-check-label" for="scope-{{ app.name.lower }}-{{ scope_id }}">
<input type="checkbox" id="scope-{{ app.name|slugify }}-{{ scope_id }}" <input type="checkbox" id="scope-{{ app.name.lower }}-{{ scope_id }}"
name="scope-{{ app.name|slugify }}" class="checkboxinput form-check-input" value="{{ scope_id }}"> name="scope-{{ app.name.lower }}" class="checkboxinput form-check-input" value="{{ scope_id }}">
{{ scope_desc }} {{ scope_desc }}
</label> </label>
</div> </div>
{% endfor %} {% endfor %}
<p id="url-{{ app.name|slugify }}"> <p id="url-{{ app.name.lower }}">
<a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code" target="_blank"> <a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code" target="_blank">
{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code {{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code
</a> </a>
@ -51,10 +51,11 @@
{% block extrajavascript %} {% block extrajavascript %}
<script> <script>
{% for app in scopes.keys %} {% for app in scopes.keys %}
for (let element of document.getElementsByName("scope-{{ app.name|slugify }}")) { let elements = document.getElementsByName("scope-{{ app.name.lower }}");
for (let element of elements) {
element.onchange = function (event) { element.onchange = function (event) {
let scope = "" let scope = ""
for (let element of document.getElementsByName("scope-{{ app.name|slugify }}")) { for (let element of elements) {
if (element.checked) { if (element.checked) {
scope += element.value + " " scope += element.value + " "
} }
@ -62,7 +63,7 @@
scope = scope.substr(0, scope.length - 1) scope = scope.substr(0, scope.length - 1)
document.getElementById("url-{{ app.name|slugify }}").innerHTML = 'Scopes : ' + scope document.getElementById("url-{{ app.name.lower }}").innerHTML = 'Scopes : ' + scope
+ '<br><a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='+ scope.replaceAll(' ', '%20') + '<br><a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='+ scope.replaceAll(' ', '%20')
+ '" target="_blank">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope=' + '" target="_blank">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='
+ scope.replaceAll(' ', '%20') + '</a>' + scope.replaceAll(' ', '%20') + '</a>'

View File

@ -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,
)

View File

@ -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 %}

View File

@ -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):
""" """

View File

@ -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

View File

@ -1,18 +0,0 @@
# Generated by Django 2.2.24 on 2021-10-05 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('treasury', '0003_auto_20210321_1034'),
]
operations = [
migrations.AlterField(
model_name='sogecredit',
name='transactions',
field=models.ManyToManyField(blank=True, related_name='_sogecredit_transactions_+', to='note.MembershipTransaction', verbose_name='membership transactions'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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

View File

@ -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
} }
} }

View File

@ -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)

View File

@ -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,

View File

@ -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 .wei2021 import WEISurvey2021
__all__ = [ __all__ = [
'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', 'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey',
] ]
CurrentSurvey = WEISurvey2023 CurrentSurvey = WEISurvey2021

View File

@ -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 WEISurveyForm2022(forms.Form):
"""
Survey form for the year 2022.
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 = WEISurveyInformation2022(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 = WEISurveyAlgorithm2022.get_buses()
informations = {bus: WEIBusInformation2022(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 WEIBusInformation2022(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 WEISurveyInformation2022(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 WEISurvey2022(WEISurvey):
"""
Survey for the year 2022.
"""
@classmethod
def get_year(cls):
return 2022
@classmethod
def get_survey_information_class(cls):
return WEISurveyInformation2022
def get_form_class(self):
return WEISurveyForm2022
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 WEISurveyAlgorithm2022
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 WEISurveyAlgorithm2022(WEISurveyAlgorithm):
"""
The algorithm class for the year 2022.
We use Gale-Shapley algorithm to attribute 1y students into buses.
"""
@classmethod
def get_survey_class(cls):
return WEISurvey2022
@classmethod
def get_bus_information_class(cls):
return WEIBusInformation2022
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
WEISurvey2022.clear_cache()
def make_repartition(self, surveys, quotas=None, tqdm_obj=None):
free_surveys = surveys.copy() # Remaining surveys
while free_surveys: # Some students are not affected
survey = free_surveys[0]
buses = survey.ordered_buses() # Preferences of the student
for bus, current_score in buses:
if self.get_bus_information(bus).has_free_seats(surveys, quotas):
# Selected bus has free places. Put student in the bus
survey.select_bus(bus)
survey.save()
free_surveys.remove(survey)
break
else:
# Current bus has not enough places. Remove the least preferred student from the bus if existing
least_preferred_survey = None
least_score = -1
# Find the least student in the bus that has a lower score than the current student
for survey2 in surveys:
if not survey2.information.valid or survey2.information.get_selected_bus() != bus:
continue
score2 = survey2.score(bus)
if current_score <= score2: # Ignore better students
continue
if least_preferred_survey is None or score2 < least_score:
least_preferred_survey = survey2
least_score = score2
if least_preferred_survey is not None:
# Remove the least student from the bus and put the current student in.
# If it does not exist, choose the next bus.
least_preferred_survey.free()
least_preferred_survey.save()
free_surveys.append(least_preferred_survey)
survey.select_bus(bus)
survey.save()
free_surveys.remove(survey)
break
else:
raise ValueError(f"User {survey.registration.user} has no free seat")
if tqdm_obj is not None:
tqdm_obj.n = len(surveys) - len(free_surveys)
tqdm_obj.refresh()

View File

@ -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()

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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(

View File

@ -29,7 +29,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold"> <a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Teams" %} <svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
</svg>
{% trans "Teams" %}
</a> </a>
</div> </div>
{% render_table teams %} {% render_table teams %}
@ -42,7 +45,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold"> <a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Members" %} <svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
</svg>
{% trans "Members" %}
</a> </a>
</div> </div>
{% render_table memberships %} {% render_table memberships %}
@ -51,7 +57,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<hr> <hr>
<a href="{% url 'wei:wei_memberships_bus_pdf' wei_pk=club.pk bus_pk=object.pk %}" data-turbolinks="false"> <a href="{% url 'wei:wei_memberships_bus_pdf' wei_pk=club.pk bus_pk=object.pk %}" data-turbolinks="false">
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button> <button class="btn btn-block btn-danger">
<svg class="bi bi-file-pdf" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.523 12.424c.14-.082.293-.162.459-.238a7.878 7.878 0 0 1-.45.606c-.28.337-.498.516-.635.572a.266.266 0 0 1-.035.012.282.282 0 0 1-.026-.044c-.056-.11-.054-.216.04-.36.106-.165.319-.354.647-.548zm2.455-1.647c-.119.025-.237.05-.356.078a21.148 21.148 0 0 0 .5-1.05 12.045 12.045 0 0 0 .51.858c-.217.032-.436.07-.654.114zm2.525.939a3.881 3.881 0 0 1-.435-.41c.228.005.434.022.612.054.317.057.466.147.518.209a.095.095 0 0 1 .026.064.436.436 0 0 1-.06.2.307.307 0 0 1-.094.124.107.107 0 0 1-.069.015c-.09-.003-.258-.066-.498-.256zM8.278 6.97c-.04.244-.108.524-.2.829a4.86 4.86 0 0 1-.089-.346c-.076-.353-.087-.63-.046-.822.038-.177.11-.248.196-.283a.517.517 0 0 1 .145-.04c.013.03.028.092.032.198.005.122-.007.277-.038.465z"/>
<path fill-rule="evenodd" d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3zM4.165 13.668c.09.18.23.343.438.419.207.075.412.04.58-.03.318-.13.635-.436.926-.786.333-.401.683-.927 1.021-1.51a11.651 11.651 0 0 1 1.997-.406c.3.383.61.713.91.95.28.22.603.403.934.417a.856.856 0 0 0 .51-.138c.155-.101.27-.247.354-.416.09-.181.145-.37.138-.563a.844.844 0 0 0-.2-.518c-.226-.27-.596-.4-.96-.465a5.76 5.76 0 0 0-1.335-.05 10.954 10.954 0 0 1-.98-1.686c.25-.66.437-1.284.52-1.794.036-.218.055-.426.048-.614a1.238 1.238 0 0 0-.127-.538.7.7 0 0 0-.477-.365c-.202-.043-.41 0-.601.077-.377.15-.576.47-.651.823-.073.34-.04.736.046 1.136.088.406.238.848.43 1.295a19.697 19.697 0 0 1-1.062 2.227 7.662 7.662 0 0 0-1.482.645c-.37.22-.699.48-.897.787-.21.326-.275.714-.08 1.103z"/>
</svg>
{% trans "View as PDF" %}
</button>
</a> </a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -47,7 +47,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card"> <div class="card">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="font-weight-bold"> <a class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Teams" %} <svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
</svg>
{% trans "Teams" %}
</a> </a>
</div> </div>
{% render_table memberships %} {% render_table memberships %}
@ -57,7 +60,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<a href="{% url 'wei:wei_memberships_team_pdf' wei_pk=club.pk bus_pk=object.bus.pk team_pk=object.pk %}" <a href="{% url 'wei:wei_memberships_team_pdf' wei_pk=club.pk bus_pk=object.bus.pk team_pk=object.pk %}"
data-turbolinks="false"> data-turbolinks="false">
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button> <button class="btn btn-block btn-danger">
<svg class="bi bi-file-pdf" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.523 12.424c.14-.082.293-.162.459-.238a7.878 7.878 0 0 1-.45.606c-.28.337-.498.516-.635.572a.266.266 0 0 1-.035.012.282.282 0 0 1-.026-.044c-.056-.11-.054-.216.04-.36.106-.165.319-.354.647-.548zm2.455-1.647c-.119.025-.237.05-.356.078a21.148 21.148 0 0 0 .5-1.05 12.045 12.045 0 0 0 .51.858c-.217.032-.436.07-.654.114zm2.525.939a3.881 3.881 0 0 1-.435-.41c.228.005.434.022.612.054.317.057.466.147.518.209a.095.095 0 0 1 .026.064.436.436 0 0 1-.06.2.307.307 0 0 1-.094.124.107.107 0 0 1-.069.015c-.09-.003-.258-.066-.498-.256zM8.278 6.97c-.04.244-.108.524-.2.829a4.86 4.86 0 0 1-.089-.346c-.076-.353-.087-.63-.046-.822.038-.177.11-.248.196-.283a.517.517 0 0 1 .145-.04c.013.03.028.092.032.198.005.122-.007.277-.038.465z"/>
<path fill-rule="evenodd" d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3zM4.165 13.668c.09.18.23.343.438.419.207.075.412.04.58-.03.318-.13.635-.436.926-.786.333-.401.683-.927 1.021-1.51a11.651 11.651 0 0 1 1.997-.406c.3.383.61.713.91.95.28.22.603.403.934.417a.856.856 0 0 0 .51-.138c.155-.101.27-.247.354-.416.09-.181.145-.37.138-.563a.844.844 0 0 0-.2-.518c-.226-.27-.596-.4-.96-.465a5.76 5.76 0 0 0-1.335-.05 10.954 10.954 0 0 1-.98-1.686c.25-.66.437-1.284.52-1.794.036-.218.055-.426.048-.614a1.238 1.238 0 0 0-.127-.538.7.7 0 0 0-.477-.365c-.202-.043-.41 0-.601.077-.377.15-.576.47-.651.823-.073.34-.04.736.046 1.136.088.406.238.848.43 1.295a19.697 19.697 0 0 1-1.062 2.227 7.662 7.662 0 0 0-1.482.645c-.37.22-.699.48-.897.787-.21.326-.275.714-.08 1.103z"/>
</svg>
{% trans "View as PDF" %}
</button>
</a> </a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -48,7 +48,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card bg-white mb-3"> <div class="card bg-white mb-3">
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<span class="font-weight-bold"> <span class="font-weight-bold">
<i class="fa fa-bus"></i> {% trans "Buses" %} <svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
</svg>
{% trans "Buses" %}
</span> </span>
</div> </div>
{% render_table buses %} {% render_table buses %}
@ -60,7 +63,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-header position-relative" id="clubListHeading"> <div class="card-header position-relative" id="clubListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" <a class="stretched-link font-weight-bold text-decoration-none"
href="{% url "wei:wei_memberships" pk=club.pk %}"> href="{% url "wei:wei_memberships" pk=club.pk %}">
<i class="fa fa-users"></i> {% trans "Members of the WEI" %} <svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</svg>
{% trans "Members of the WEI" %}
</a> </a>
</div> </div>
{% render_table member_list %} {% render_table member_list %}
@ -72,7 +80,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-header position-relative" id="historyListHeading"> <div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %} <a class="stretched-link font-weight-bold text-decoration-none" {% if "note.view_note"|has_perm:club.note %}
href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}> href="{% url 'note:transactions' pk=club.note.pk %}" {% endif %}>
<i class="fa fa-euro"></i> {% trans "Transaction history" %} <svg class="bi bi-euro" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M4 9.42h1.063C5.4 12.323 7.317 14 10.34 14c.622 0 1.167-.068 1.659-.185v-1.3c-.484.119-1.045.17-1.659.17-2.1 0-3.455-1.198-3.775-3.264h4.017v-.928H6.497v-.936c0-.11 0-.219.008-.329h4.078v-.927H6.618c.388-1.898 1.719-2.985 3.723-2.985.614 0 1.175.05 1.659.177V2.194A6.617 6.617 0 0 0 10.341 2c-2.928 0-4.82 1.569-5.244 4.3H4v.928h1.01v1.265H4v.928z"/>
</svg>
{% trans "Transaction history" %}
</a> </a>
</div> </div>
<div id="history_list"> <div id="history_list">
@ -86,7 +97,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-header position-relative" id="historyListHeading"> <div class="card-header position-relative" id="historyListHeading">
<a class="stretched-link font-weight-bold text-decoration-none" <a class="stretched-link font-weight-bold text-decoration-none"
href="{% url 'wei:wei_registrations' pk=club.pk %}"> href="{% url 'wei:wei_registrations' pk=club.pk %}">
<i class="fa fa-user-plus"></i> {% trans "Unvalidated registrations" %} <svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
</svg>
{% trans "Unvalidated registrations" %}
</a> </a>
</div> </div>
<div id="history_list"> <div id="history_list">

View File

@ -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>

View File

@ -28,7 +28,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
</a> </a>
<hr> <hr>
<a href="{% url 'wei:wei_memberships_pdf' wei_pk=club.pk %}" data-turbolinks="false"> <a href="{% url 'wei:wei_memberships_pdf' wei_pk=club.pk %}" data-turbolinks="false">
<button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button> <button class="btn btn-block btn-danger">
<svg class="bi bi-file-pdf" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M5.523 12.424c.14-.082.293-.162.459-.238a7.878 7.878 0 0 1-.45.606c-.28.337-.498.516-.635.572a.266.266 0 0 1-.035.012.282.282 0 0 1-.026-.044c-.056-.11-.054-.216.04-.36.106-.165.319-.354.647-.548zm2.455-1.647c-.119.025-.237.05-.356.078a21.148 21.148 0 0 0 .5-1.05 12.045 12.045 0 0 0 .51.858c-.217.032-.436.07-.654.114zm2.525.939a3.881 3.881 0 0 1-.435-.41c.228.005.434.022.612.054.317.057.466.147.518.209a.095.095 0 0 1 .026.064.436.436 0 0 1-.06.2.307.307 0 0 1-.094.124.107.107 0 0 1-.069.015c-.09-.003-.258-.066-.498-.256zM8.278 6.97c-.04.244-.108.524-.2.829a4.86 4.86 0 0 1-.089-.346c-.076-.353-.087-.63-.046-.822.038-.177.11-.248.196-.283a.517.517 0 0 1 .145-.04c.013.03.028.092.032.198.005.122-.007.277-.038.465z"/>
<path fill-rule="evenodd" d="M4 0h5.293A1 1 0 0 1 10 .293L13.707 4a1 1 0 0 1 .293.707V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm5.5 1.5v2a1 1 0 0 0 1 1h2l-3-3zM4.165 13.668c.09.18.23.343.438.419.207.075.412.04.58-.03.318-.13.635-.436.926-.786.333-.401.683-.927 1.021-1.51a11.651 11.651 0 0 1 1.997-.406c.3.383.61.713.91.95.28.22.603.403.934.417a.856.856 0 0 0 .51-.138c.155-.101.27-.247.354-.416.09-.181.145-.37.138-.563a.844.844 0 0 0-.2-.518c-.226-.27-.596-.4-.96-.465a5.76 5.76 0 0 0-1.335-.05 10.954 10.954 0 0 1-.98-1.686c.25-.66.437-1.284.52-1.794.036-.218.055-.426.048-.614a1.238 1.238 0 0 0-.127-.538.7.7 0 0 0-.477-.365c-.202-.043-.41 0-.601.077-.377.15-.576.47-.651.823-.073.34-.04.736.046 1.136.088.406.238.848.43 1.295a19.697 19.697 0 0 1-1.062 2.227 7.662 7.662 0 0 0-1.482.645c-.37.22-.699.48-.897.787-.21.326-.275.714-.08 1.103z"/>
</svg>
{% trans "View as PDF" %}
</button>
</a> </a>
</div> </div>
</div> </div>

View File

@ -25,7 +25,6 @@ class TestWEIAlgorithm(TestCase):
email="wei2021@example.com", email="wei2021@example.com",
date_start='2021-09-17', date_start='2021-09-17',
date_end='2021-09-19', date_end='2021-09-19',
year=2021,
) )
self.buses = [] self.buses = []

View File

@ -1,110 +0,0 @@
# Copyright (C) 2018-2022 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.wei2022 import WEIBusInformation2022, WEISurvey2022, WORDS, WEISurveyInformation2022
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 2022",
email="wei2022@example.com",
date_start='2022-09-16',
date_end='2022-09-18',
year=2022,
)
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 = WEIBusInformation2022(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 = WEISurveyInformation2022(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
WEISurvey2022.get_algorithm_class()().run_algorithm()
# Ensure that everyone has its first choice
for r in WEIRegistration.objects.filter(wei=self.wei).all():
survey = WEISurvey2022(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 = WEISurveyInformation2022(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
WEISurvey2022.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 = WEISurvey2022(r)
chosen_bus = survey.information.get_selected_bus()
buses = survey.ordered_buses()
score = min(v for bus, v in buses if bus == chosen_bus)
max_score = buses[0][1]
penalty += (max_score - score) ** 2
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %

View File

@ -1,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 %

View File

@ -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(), 2021)
class TestWeiAPI(TestAPI): class TestWeiAPI(TestAPI):

View File

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

View File

@ -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

View File

@ -86,7 +86,7 @@ Génération
Les factures peuvent s'exporter au format PDF (là est tout leur intérêt). Pour cela, on utilise le template LaTeX Les factures peuvent s'exporter au format PDF (là est tout leur intérêt). Pour cela, on utilise le template LaTeX
présent à l'adresse suivante : présent à l'adresse suivante :
`/templates/treasury/invoice_sample.tex <https://gitlab.crans.org/bde/nk20/-/tree/main/templates/treasury/invoice_sample.tex>`_ `/templates/treasury/invoice_sample.tex <https://gitlab.crans.org/bde/nk20/-/tree/master/templates/treasury/invoice_sample.tex>`_
On le remplit avec les données de la facture et les données du BDE, hard-codées. On copie le template rempli dans un On le remplit avec les données de la facture et les données du BDE, hard-codées. On copie le template rempli dans un
ficher tex dans un dossier temporaire. On fait ensuite 2 appels à ``pdflatex`` pour générer la facture au format PDF. ficher tex dans un dossier temporaire. On fait ensuite 2 appels à ``pdflatex`` pour générer la facture au format PDF.

View File

@ -41,14 +41,8 @@ On a ensuite besoin de définir nos propres scopes afin d'avoir des permissions
OAUTH2_PROVIDER = { OAUTH2_PROVIDER = {
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes', 'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
} }
Cela a pour effet d'avoir des scopes sous la forme ``PERMISSION_CLUB``,
et de demander des scopes facultatives (voir plus bas).
Un jeton de rafraîchissement expire de plus au bout de 14 jours, si non-renouvelé.
On ajoute enfin les routes dans ``urls.py`` : On ajoute enfin les routes dans ``urls.py`` :
.. code:: python .. code:: python
@ -100,27 +94,6 @@ du format renvoyé.
Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
jetons. jetons.
.. danger::
Demander des scopes n'implique pas de les avoir.
Lorsque des scopes sont demandées par un client, la Note
va considérer l'ensemble des permissions accessibles parmi
ce qui est demandé. Dans vos programmes, vous devrez donc
vérifier les permissions acquises (communiquées lors de la
récupération du jeton d'accès à partir du grant code),
et prévoir un comportement dans le cas où des permissions
sont manquantes.
Cela offre un intérêt supérieur par rapport au protocole
OAuth2 classique, consistant à demander trop de permissions
et agir en conséquence.
Par exemple, vous pourriez demander la permission d'accéder
aux membres d'un club ou de faire des transactions, et agir
uniquement dans le cas où l'utilisateur connecté possède la
permission problématique.
Avec Django-allauth Avec Django-allauth
################### ###################
@ -143,7 +116,6 @@ installées (sur votre propre client), puis de bien ajouter l'application social
SOCIALACCOUNT_PROVIDERS = { SOCIALACCOUNT_PROVIDERS = {
'notekfet': { 'notekfet': {
# 'DOMAIN': 'note.crans.org', # 'DOMAIN': 'note.crans.org',
'SCOPE': ['1_1', '2_1'],
}, },
... ...
} }
@ -151,10 +123,6 @@ installées (sur votre propre client), puis de bien ajouter l'application social
Le paramètre ``DOMAIN`` permet de changer d'instance de Note Kfet. Par défaut, il Le paramètre ``DOMAIN`` permet de changer d'instance de Note Kfet. Par défaut, il
se connectera à ``note.crans.org`` si vous ne renseignez rien. se connectera à ``note.crans.org`` si vous ne renseignez rien.
Le paramètre ``SCOPE`` permet de définir les scopes à demander.
Dans l'exemple ci-dessous, les permissions d'accéder à l'utilisateur
et au profil sont demandées.
En créant l'application sur la note, vous pouvez renseigner En créant l'application sur la note, vous pouvez renseigner
``https://monsite.example.com/accounts/notekfet/login/callback/`` en URL de redirection, ``https://monsite.example.com/accounts/notekfet/login/callback/`` en URL de redirection,
à adapter selon votre configuration. à adapter selon votre configuration.

View File

@ -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
--------------------------------------------------------------------------------------------------- ---------------------------------------------------------------------------------------------------

View File

@ -23,7 +23,7 @@ Sur un Ubuntu/Debian :
$ sudo apt update $ sudo apt update
$ sudo apt install --no-install-recommends -y \ $ sudo apt install --no-install-recommends -y \
python3-setuptools python3-venv python3-dev \ python3-setuptools python3-venv python3-dev \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome git texlive-xetex gettext libjs-bootstrap4 git
Pour Arch Linux : Pour Arch Linux :

View File

@ -62,7 +62,7 @@ plus propre. On peut donc installer tout ce dont on a besoin, depuis buster-back
$ sudo apt update $ sudo apt update
$ sudo apt install -t buster-backports --no-install-recommends \ $ sudo apt install -t buster-backports --no-install-recommends \
gettext git ipython3 \ # Dépendances basiques gettext git ipython3 \ # Dépendances basiques
fonts-font-awesome libjs-bootstrap4 \ # Pour l'affichage web libjs-bootstrap4 \ # Pour l'affichage web
python3-bs4 python3-django python3-django-crispy-forms python3-django-extensions \ python3-bs4 python3-django python3-django-crispy-forms python3-django-extensions \
python3-django-filters python3-django-oauth-toolkit python3-django-polymorphic \ python3-django-filters python3-django-oauth-toolkit python3-django-polymorphic \
python3-djangorestframework python3-memcache python3-phonenumbers \ python3-djangorestframework python3-memcache python3-phonenumbers \
@ -88,7 +88,7 @@ On clone donc le dépôt en tant que ``www-data`` :
$ sudo -u www-data git clone https://gitlab.crans.org/bde/nk20.git /var/www/note_kfet $ sudo -u www-data git clone https://gitlab.crans.org/bde/nk20.git /var/www/note_kfet
Par défaut, le dépôt est configuré pour suivre la branche ``main``, qui est la branche Par défaut, le dépôt est configuré pour suivre la branche ``master``, qui est la branche
stable, notamment installée sur `<https://note.crans.org/>`_. Pour changer de branche, stable, notamment installée sur `<https://note.crans.org/>`_. Pour changer de branche,
notamment passer sur la branche ``beta`` sur un serveur de pré-production (un peu comme notamment passer sur la branche ``beta`` sur un serveur de pré-production (un peu comme
`<https://note-dev.crans.org/>`_), on peut faire : `<https://note-dev.crans.org/>`_), on peut faire :
@ -587,7 +587,7 @@ Dans ce fichier, remplissez :
--- ---
note: note:
server_name: note.crans.org server_name: note.crans.org
git_branch: main git_branch: master
cron_enabled: true cron_enabled: true
email: notekfet2020@lists.crans.org email: notekfet2020@lists.crans.org

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

View File

@ -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

View File

@ -7,8 +7,6 @@
import os import os
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
from datetime import timedelta
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
@ -24,15 +22,6 @@ ALLOWED_HOSTS = [
os.getenv('NOTE_URL', 'localhost'), os.getenv('NOTE_URL', 'localhost'),
] ]
# Use secure cookies in production
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
# Remember HTTPS for 1 year
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Application definition # Application definition
@ -252,15 +241,13 @@ 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,
} }
# OAuth2 Provider # OAuth2 Provider
OAUTH2_PROVIDER = { OAUTH2_PROVIDER = {
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes', 'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
'OAUTH2_VALIDATOR_CLASS': "permission.scopes.PermissionOAuth2Validator",
'REFRESH_TOKEN_EXPIRE_SECONDS': timedelta(days=14),
} }
# Take control on how widget templates are sourced # Take control on how widget templates are sourced

72
note_kfet/static/css/custom.css Executable file → Normal file
View 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);
} }

View File

@ -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')
} }
}) })
}) })

View File

@ -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' }

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -24,16 +24,16 @@ SPDX-License-Identifier: GPL-3.0-or-later
<meta name="msapplication-config" content="{% static "favicon/browserconfig.xml" %}"> <meta name="msapplication-config" content="{% static "favicon/browserconfig.xml" %}">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
{# Bootstrap, Font Awesome and custom CSS #} {# Load CSS #}
<link rel="stylesheet" href="{% static "bootstrap4/css/bootstrap.min.css" %}"> <link rel="stylesheet" href="{% static "bootstrap4/css/bootstrap.min.css" %}">
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
<link rel="stylesheet" href="{% static "css/custom.css" %}"> <link rel="stylesheet" href="{% static "css/custom.css" %}">
{# JQuery, Bootstrap and Turbolinks JavaScript #} {# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="{% static "jquery/jquery.min.js" %}"></script> <script src="{% static "jquery/jquery.min.js" %}"></script>
<script src="{% static "popper.js/umd/popper.min.js" %}"></script> <script src="{% static "popper.js/umd/popper.min.js" %}"></script>
<script src="{% static "bootstrap4/js/bootstrap.min.js" %}"></script> <script src="{% static "bootstrap4/js/bootstrap.min.js" %}"></script>
<script src="{% static "js/turbolinks.js" %}"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
crossorigin="anonymous"></script>
<script src="{% static "js/base.js" %}"></script> <script src="{% static "js/base.js" %}"></script>
<script src="{% static "js/konami.js" %}"></script> <script src="{% static "js/konami.js" %}"></script>
@ -63,54 +63,101 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if "note.transactiontemplate"|not_empty_model_list %} {% if "note.transactiontemplate"|not_empty_model_list %}
<li class="nav-item"> <li class="nav-item">
{% url 'note:consos' as url %} {% url 'note:consos' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
<svg class="bi bi-mug" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M1 2a1 1 0 0 1 1-1h11a1 1 0 0 1 1 1v1h.5A1.5 1.5 0 0 1 16 4.5v7a1.5 1.5 0 0 1-1.5 1.5h-.55a2.5 2.5 0 0 1-2.45 2h-8A2.5 2.5 0 0 1 1 12.5V2zm13 10h.5a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.5-.5H14v8z"/>
</svg>
{% trans 'Consumptions' %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if user.is_authenticated and user|is_member:"Kfet" %} {% if user.is_authenticated and user|is_member:"Kfet" %}
<li class="nav-item"> <li class="nav-item">
{% url 'note:transfer' as url %} {% url 'note:transfer' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-exchange"></i> {% trans 'Transfer' %} </a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
<svg class="bi bi-exchange" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 11.5a.5.5 0 0 0 .5.5h11.793l-3.147 3.146a.5.5 0 0 0 .708.708l4-4a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 11H1.5a.5.5 0 0 0-.5.5zm14-7a.5.5 0 0 1-.5.5H2.707l3.147 3.146a.5.5 0 1 1-.708.708l-4-4a.5.5 0 0 1 0-.708l4-4a.5.5 0 1 1 .708.708L2.707 4H14.5a.5.5 0 0 1 .5.5z"/>
</svg>
{% trans 'Transfer' %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if "auth.user"|model_list_length >= 2 %} {% if "auth.user"|model_list_length >= 2 %}
<li class="nav-item"> <li class="nav-item">
{% url 'member:user_list' as url %} {% url 'member:user_list' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-user"></i> {% trans 'Users' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
<svg class="bi bi-user" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
{% trans 'Users' %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if "member.club"|not_empty_model_list %} {% if "member.club"|not_empty_model_list %}
<li class="nav-item"> <li class="nav-item">
{% url 'member:club_list' as url %} {% url 'member:club_list' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
<svg class="bi bi-users" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7 14s-1 0-1-1 1-4 5-4 5 3 5 4-1 1-1 1H7zm4-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M5.216 14A2.238 2.238 0 0 1 5 13c0-1.355.68-2.75 1.936-3.72A6.325 6.325 0 0 0 5 9c-4 0-5 3-5 4s1 1 1 1h4.216z"/>
<path d="M4.5 8a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z"/>
</svg>
{% trans 'Clubs' %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if "activity.activity"|not_empty_model_list %} {% if "activity.activity"|not_empty_model_list %}
<li class="nav-item"> <li class="nav-item">
{% url 'activity:activity_list' as url %} {% url 'activity:activity_list' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
<svg class="bi bi-calendar" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V5h16V4H0V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5z"/>
</svg>
{% trans 'Activities' %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if "treasury.invoice"|not_empty_model_list %} {% if "treasury.invoice"|not_empty_model_list %}
<li class="nav-item"> <li class="nav-item">
{% url 'treasury:invoice_list' as url %} {% url 'treasury:invoice_list' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-credit-card"></i> {% trans 'Treasury' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
<svg class="bi bi-credit-card" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M0 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v1H0V4zm0 3v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7H0zm3 2h1a1 1 0 0 1 1 1v1a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1v-1a1 1 0 0 1 1-1z"/>
</svg>
{% trans 'Treasury' %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if "wei.weiclub"|not_empty_model_list %} {% if "wei.weiclub"|not_empty_model_list %}
<li class="nav-item"> <li class="nav-item">
{% url 'wei:current_wei_detail' as url %} {% url 'wei:current_wei_detail' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-bus"></i> {% trans 'WEI' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
<svg class="bi bi-signpost" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M7.293.707A1 1 0 0 0 7 1.414V4H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h5v6h2v-6h3.532a1 1 0 0 0 .768-.36l1.933-2.32a.5.5 0 0 0 0-.64L13.3 4.36a1 1 0 0 0-.768-.36H9V1.414A1 1 0 0 0 7.293.707z"/>
</svg>
{% trans 'WEI' %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li class="nav-item"> <li class="nav-item">
{% url 'permission:rights' as url %} {% url 'permission:rights' as url %}
<a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}"><i class="fa fa-balance-scale"></i> {% trans 'Rights' %}</a> <a class="nav-link {% if request.path_info == url %}active{% endif %}" href="{{ url }}">
<svg class="bi bi-shield" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M8 0c-.69 0-1.843.265-2.928.56-1.11.3-2.229.655-2.887.87a1.54 1.54 0 0 0-1.044 1.262c-.596 4.477.787 7.795 2.465 9.99a11.777 11.777 0 0 0 2.517 2.453c.386.273.744.482 1.048.625.28.132.581.24.829.24s.548-.108.829-.24a7.159 7.159 0 0 0 1.048-.625 11.775 11.775 0 0 0 2.517-2.453c1.678-2.195 3.061-5.513 2.465-9.99a1.541 1.541 0 0 0-1.044-1.263 62.467 62.467 0 0 0-2.887-.87C9.843.266 8.69 0 8 0zm0 5a1.5 1.5 0 0 1 .5 2.915l.385 1.99a.5.5 0 0 1-.491.595h-.788a.5.5 0 0 1-.49-.595l.384-1.99A1.5 1.5 0 0 1 8 5z"/>
</svg>
{% trans 'Rights' %}
</a>
</li> </li>
{% endif %} {% endif %}
{% if request.user.is_staff and ""|has_perm:user %} {% if request.user.is_staff and ""|has_perm:user %}
<li class="nav-item"> <li class="nav-item">
<a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fa fa-cogs"></i> {% trans 'Admin' %}</a> <a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}">
<svg class="bi bi-cog" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.86 2.929 2.929 0 0 1 0 5.858z"/>
</svg>
{% trans 'Admin' %}
</a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>
@ -118,16 +165,25 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if request.user.is_authenticated %} {% if request.user.is_authenticated %}
<li class="dropdown"> <li class="dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fa fa-user"></i> <svg class="bi bi-user" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
<span id="user_balance">{{ request.user.username }} ({{ request.user.note.balance | pretty_money }})</span> <span id="user_balance">{{ request.user.username }} ({{ request.user.note.balance | pretty_money }})</span>
</a> </a>
<div class="dropdown-menu dropdown-menu-right" <div class="dropdown-menu dropdown-menu-right"
aria-labelledby="navbarDropdownMenuLink"> aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}"> <a class="dropdown-item" href="{% url 'member:user_detail' pk=request.user.pk %}">
<i class="fa fa-user"></i> {% trans "My account" %} <svg class="bi bi-user" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
</svg>
{% trans "My account" %}
</a> </a>
<a class="dropdown-item" href="{% url 'logout' %}"> <a class="dropdown-item" href="{% url 'logout' %}">
<i class="fa fa-sign-out"></i> {% trans "Log out" %} <svg class="bi bi-signout" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M10 12.5a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v2a.5.5 0 0 0 1 0v-2A1.5 1.5 0 0 0 9.5 2h-8A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-2a.5.5 0 0 0-1 0v2z"/>
<path fill-rule="evenodd" d="M15.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 0 0-.708.708L14.293 7.5H5.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
</svg>
{% trans "Log out" %}
</a> </a>
</div> </div>
</li> </li>
@ -135,14 +191,22 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% if request.path != "/registration/signup/" %} {% if request.path != "/registration/signup/" %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'registration:signup' %}"> <a class="nav-link" href="{% url 'registration:signup' %}">
<i class="fa fa-user-plus"></i> {% trans "Sign up" %} <svg class="bi bi-user-plus" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path d="M1 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/>
<path fill-rule="evenodd" d="M13.5 5a.5.5 0 0 1 .5.5V7h1.5a.5.5 0 0 1 0 1H14v1.5a.5.5 0 0 1-1 0V8h-1.5a.5.5 0 0 1 0-1H13V5.5a.5.5 0 0 1 .5-.5z"/>
</svg>
{% trans "Sign up" %}
</a> </a>
</li> </li>
{% endif %} {% endif %}
{% if request.path != "/accounts/login/" %} {% if request.path != "/accounts/login/" %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{% url 'login' %}"> <a class="nav-link" href="{% url 'login' %}">
<i class="fa fa-sign-in"></i> {% trans "Log in" %} <svg class="bi bi-login" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z"/>
<path fill-rule="evenodd" d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
</svg>
{% trans "Log in" %}
</a> </a>
</li> </li>
{% endif %} {% endif %}
@ -194,8 +258,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
class="text-muted">{% trans "Contact us" %}</a> &mdash; class="text-muted">{% trans "Contact us" %}</a> &mdash;
<a href="mailto:{{ "SUPPORT_EMAIL" | getenv }}" <a href="mailto:{{ "SUPPORT_EMAIL" | getenv }}"
class="text-muted">{% trans "Technical Support" %}</a> &mdash; class="text-muted">{% trans "Technical Support" %}</a> &mdash;
<a href="https://note.crans.org/doc/faq/"
class="text-muted">{% trans "FAQ (FR)" %}</a> &mdash;
</span> </span>
{% csrf_token %} {% csrf_token %}
<select title="language" name="language" <select title="language" name="language"

View File

@ -23,7 +23,7 @@ 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>

View File

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