diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cada9068..2181b4b5 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -16,8 +16,8 @@ py37-django22:
apt-get install --no-install-recommends -t buster-backports -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
- python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
- python3-babel python3-lockfile python3-pip python3-phonenumbers
+ python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
+ python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py37-django22
@@ -33,8 +33,8 @@ py38-django22:
apt-get install --no-install-recommends -y
python3-django python3-django-crispy-forms
python3-django-extensions python3-django-filters python3-django-polymorphic
- python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil
- python3-babel python3-lockfile python3-pip python3-phonenumbers
+ python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil
+ python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache
python3-bs4 python3-setuptools tox texlive-xetex
script: tox -e py38-django22
diff --git a/Dockerfile b/Dockerfile
index 0dd1ce8b..95a62437 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -8,8 +8,8 @@ RUN apt-get update && \
apt-get install --no-install-recommends -t buster-backports -y \
python3-django python3-django-crispy-forms \
python3-django-extensions python3-django-filters python3-django-polymorphic \
- python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil \
- python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
+ python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \
+ python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
python3-bs4 python3-setuptools \
uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome && \
diff --git a/README.md b/README.md
index b7c937c8..f2601943 100644
--- a/README.md
+++ b/README.md
@@ -93,10 +93,10 @@ Sinon vous pouvez suivre les étapes décrites ci-dessous.
$ sudo apt install --no-install-recommends -t buster-backports -y \
python3-django python3-django-crispy-forms \
python3-django-extensions python3-django-filters python3-django-polymorphic \
- python3-djangorestframework python3-django-cas-server python3-psycopg2 python3-pil \
- python3-babel python3-lockfile python3-pip python3-phonenumbers ipython3 \
- python3-bs4 python3-setuptools \
- uwsgi uwsgi-plugin-python3 \
+ python3-djangorestframework python3-django-oauth-toolkit python3-psycopg2 python3-pil \
+ python3-babel python3-lockfile python3-pip python3-phonenumbers python3-memcache ipython3 \
+ python3-bs4 python3-setuptools python3-docutils \
+ memcached uwsgi uwsgi-plugin-python3 \
texlive-xetex gettext libjs-bootstrap4 fonts-font-awesome \
nginx python3-venv git acl
```
diff --git a/ansible/roles/1-apt-basic/tasks/main.yml b/ansible/roles/1-apt-basic/tasks/main.yml
index 1ca1b3d6..9c01dd97 100644
--- a/ansible/roles/1-apt-basic/tasks/main.yml
+++ b/ansible/roles/1-apt-basic/tasks/main.yml
@@ -23,13 +23,14 @@
- python3-babel
- python3-bs4
- python3-django
- - python3-django-cas-server
- python3-django-crispy-forms
- python3-django-extensions
- python3-django-filters
+ - python3-django-oauth-toolkit
- python3-django-polymorphic
- python3-djangorestframework
- python3-lockfile
+ - python3-memcache
- python3-phonenumbers
- python3-pil
- python3-pip
@@ -40,6 +41,9 @@
# LaTeX (PDF generation)
- texlive-xetex
+ # Cache server
+ - memcached
+
# WSGI server
- uwsgi
- uwsgi-plugin-python3
diff --git a/apps/activity/views.py b/apps/activity/views.py
index 2f5c7e0b..4a152e07 100644
--- a/apps/activity/views.py
+++ b/apps/activity/views.py
@@ -12,8 +12,10 @@ from django.db.models import F, Q
from django.http import HttpResponse
from django.urls import reverse_lazy
from django.utils import timezone
+from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _
from django.views import View
+from django.views.decorators.cache import cache_page
from django.views.generic import DetailView, TemplateView, UpdateView
from django_tables2.views import SingleTableView
from note.models import Alias, NoteSpecial, NoteUser
@@ -288,6 +290,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
return context
+# Cache for 1 hour
+@method_decorator(cache_page(60 * 60), name='dispatch')
class CalendarView(View):
"""
Render an ICS calendar with all valid activities.
diff --git a/apps/member/forms.py b/apps/member/forms.py
index 5081a12e..05940a06 100644
--- a/apps/member/forms.py
+++ b/apps/member/forms.py
@@ -150,6 +150,7 @@ class ClubForm(forms.ModelForm):
"membership_fee_unpaid": AmountInput(),
"parent_club": Autocomplete(
Club,
+ resetable=True,
attrs={
'api_url': '/api/members/club/',
}
diff --git a/apps/member/migrations/0003_create_bde_and_kfet.py b/apps/member/migrations/0003_create_bde_and_kfet.py
index 5f218040..d2f80c2d 100644
--- a/apps/member/migrations/0003_create_bde_and_kfet.py
+++ b/apps/member/migrations/0003_create_bde_and_kfet.py
@@ -7,6 +7,7 @@ def create_bde_and_kfet(apps, schema_editor):
"""
Club = apps.get_model("member", "club")
NoteClub = apps.get_model("note", "noteclub")
+ Alias = apps.get_model("note", "alias")
ContentType = apps.get_model('contenttypes', 'ContentType')
polymorphic_ctype_id = ContentType.objects.get_for_model(NoteClub).id
@@ -45,6 +46,19 @@ def create_bde_and_kfet(apps, schema_editor):
polymorphic_ctype_id=polymorphic_ctype_id,
)
+ Alias.objects.get_or_create(
+ id=5,
+ note_id=5,
+ name="BDE",
+ normalized_name="bde",
+ )
+ Alias.objects.get_or_create(
+ id=6,
+ note_id=6,
+ name="Kfet",
+ normalized_name="kfet",
+ )
+
class Migration(migrations.Migration):
dependencies = [
diff --git a/apps/member/migrations/0006_create_note_account_bde_membership.py b/apps/member/migrations/0006_create_note_account_bde_membership.py
new file mode 100644
index 00000000..4a18301f
--- /dev/null
+++ b/apps/member/migrations/0006_create_note_account_bde_membership.py
@@ -0,0 +1,50 @@
+import sys
+
+from django.db import migrations
+
+
+def give_note_account_permissions(apps, schema_editor):
+ """
+ Automatically manage the membership of the Note account.
+ """
+ User = apps.get_model("auth", "user")
+ Membership = apps.get_model("member", "membership")
+ Role = apps.get_model("permission", "role")
+
+ note = User.objects.filter(username="note")
+ if not note.exists():
+ # We are in a test environment, don't log error message
+ if len(sys.argv) > 1 and sys.argv[1] == 'test':
+ return
+ print("Warning: Note account was not found. The note account was not imported.")
+ print("Make sure you have imported the NK15 database. The new import script handles correctly the permissions.")
+ print("This migration will be ignored, you can re-run it if you forgot the note account or ignore it if you "
+ "don't want this account.")
+ return
+
+ note = note.get()
+
+ # Set for the two clubs a large expiration date and the correct role.
+ for m in Membership.objects.filter(user_id=note.id).all():
+ m.date_end = "3142-12-12"
+ m.roles.set(Role.objects.filter(name="PC Kfet").all())
+ m.save()
+ # By default, the note account is only authorized to be logged from localhost.
+ note.password = "ipbased$127.0.0.1"
+ note.is_active = True
+ note.save()
+ # Ensure that the note of the account is disabled
+ note.note.inactivity_reason = 'forced'
+ note.note.is_active = False
+ note.save()
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('member', '0005_remove_null_tag_on_charfields'),
+ ('permission', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RunPython(give_note_account_permissions),
+ ]
diff --git a/apps/member/tables.py b/apps/member/tables.py
index 8c979f08..a9676928 100644
--- a/apps/member/tables.py
+++ b/apps/member/tables.py
@@ -43,8 +43,24 @@ class UserTable(tables.Table):
section = tables.Column(accessor='profile__section')
+ # Override the column to let replace the URL
+ email = tables.EmailColumn(linkify=lambda record: "mailto:{}".format(record.email))
+
balance = tables.Column(accessor='note__balance', verbose_name=_("Balance"))
+ def render_email(self, record, value):
+ # Replace the email by a dash if the user can't see the profile detail
+ # Replace also the URL
+ if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile):
+ value = "—"
+ record.email = value
+ return value
+
+ def render_section(self, record, value):
+ return value \
+ if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \
+ else "—"
+
def render_balance(self, record, value):
return pretty_money(value)\
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else "—"
@@ -112,7 +128,7 @@ class MembershipTable(tables.Table):
fee=0,
)
if PermissionBackend.check_perm(get_current_authenticated_user(),
- "member:add_membership", empty_membership): # If the user has right
+ "member.add_membership", empty_membership): # If the user has right
renew_url = reverse_lazy('member:club_renew_membership',
kwargs={"pk": record.pk})
t = format_html(
diff --git a/apps/member/templates/member/includes/profile_info.html b/apps/member/templates/member/includes/profile_info.html
index b7f2fe70..e008ec6a 100644
--- a/apps/member/templates/member/includes/profile_info.html
+++ b/apps/member/templates/member/includes/profile_info.html
@@ -25,25 +25,27 @@
-
{% trans 'section'|capfirst %}
- {{ user_object.profile.section }}
+ {% if "member.view_profile"|has_perm:user_object.profile %}
+ {% trans 'section'|capfirst %}
+ {{ user_object.profile.section }}
- {% trans 'email'|capfirst %}
- {{ user_object.email }}
+ {% trans 'email'|capfirst %}
+ {{ user_object.email }}
- {% trans 'phone number'|capfirst %}
- {{ user_object.profile.phone_number }}
-
+ {% trans 'phone number'|capfirst %}
+ {{ user_object.profile.phone_number }}
+
- {% trans 'address'|capfirst %}
- {{ user_object.profile.address }}
+ {% trans 'address'|capfirst %}
+ {{ user_object.profile.address }}
- {% if user_object.note and "note.view_note"|has_perm:user_object.note %}
- {% trans 'balance'|capfirst %}
- {{ user_object.note.balance | pretty_money }}
+ {% if user_object.note and "note.view_note"|has_perm:user_object.note %}
+ {% trans 'balance'|capfirst %}
+ {{ user_object.note.balance | pretty_money }}
- {% trans 'paid'|capfirst %}
- {{ user_object.profile.paid|yesno }}
+ {% trans 'paid'|capfirst %}
+ {{ user_object.profile.paid|yesno }}
+ {% endif %}
{% endif %}
diff --git a/apps/member/templates/member/user_list.html b/apps/member/templates/member/user_list.html
index a41d7e69..023aca16 100644
--- a/apps/member/templates/member/user_list.html
+++ b/apps/member/templates/member/user_list.html
@@ -5,7 +5,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% load i18n perms %}
{% block content %}
-{% if "member.change_profile_registration_valid"|has_perm:user %}
+{% if can_manage_registrations %}
{% trans "Registrations" %}
diff --git a/apps/member/templatetags/__init__.py b/apps/member/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/apps/member/templatetags/memberinfo.py b/apps/member/templatetags/memberinfo.py
new file mode 100644
index 00000000..d2b4986a
--- /dev/null
+++ b/apps/member/templatetags/memberinfo.py
@@ -0,0 +1,22 @@
+# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from datetime import date
+
+from django import template
+from django.contrib.auth.models import User
+
+from ..models import Club, Membership
+
+
+def is_member(user, club):
+ if isinstance(user, str):
+ club = User.objects.get(username=user)
+ if isinstance(club, str):
+ club = Club.objects.get(name=club)
+ return Membership.objects\
+ .filter(user=user, club=club, date_start__lte=date.today(), date_end__gte=date.today()).exists()
+
+
+register = template.Library()
+register.filter("is_member", is_member)
diff --git a/apps/member/tests/test_login.py b/apps/member/tests/test_login.py
index e022c4ea..c9fabf82 100644
--- a/apps/member/tests/test_login.py
+++ b/apps/member/tests/test_login.py
@@ -41,7 +41,7 @@ class TemplateLoggedInTests(TestCase):
password="adminadmin",
permission_mask=3,
))
- self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 200)
+ self.assertRedirects(response, settings.LOGIN_REDIRECT_URL, 302, 302)
def test_logout(self):
response = self.client.get(reverse("logout"))
diff --git a/apps/member/tests/test_memberships.py b/apps/member/tests/test_memberships.py
index 90b1f382..80c214f0 100644
--- a/apps/member/tests/test_memberships.py
+++ b/apps/member/tests/test_memberships.py
@@ -205,7 +205,7 @@ class TestMemberships(TestCase):
first_name="Toto",
bank="Le matelas",
))
- self.assertRedirects(response, club.get_absolute_url(), 302, 200)
+ self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
self.assertTrue(Membership.objects.filter(user=user, club=club).exists())
@@ -244,9 +244,9 @@ class TestMemberships(TestCase):
first_name="Toto",
bank="Bank",
))
- self.assertRedirects(response, club.get_absolute_url(), 302, 200)
+ self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
- response = self.client.get(user.profile.get_absolute_url())
+ response = self.client.get(club.get_absolute_url())
self.assertEqual(response.status_code, 200)
def test_auto_join_kfet_when_join_bde_with_soge(self):
@@ -273,7 +273,7 @@ class TestMemberships(TestCase):
first_name="Toto",
bank="Société générale",
))
- self.assertRedirects(response, bde.get_absolute_url(), 302, 200)
+ self.assertRedirects(response, user.profile.get_absolute_url(), 302, 200)
self.assertTrue(Membership.objects.filter(user=user, club=bde).exists())
self.assertTrue(Membership.objects.filter(user=user, club=kfet).exists())
diff --git a/apps/member/views.py b/apps/member/views.py
index 42bf98e4..73569c89 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -70,10 +70,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
form.fields['email'].required = True
form.fields['email'].help_text = _("This address must be valid.")
- context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
- data=self.request.POST if self.request.POST else None)
- if not self.object.profile.report_frequency:
- del context['profile_form'].fields["last_report"]
+ if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile):
+ context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
+ data=self.request.POST if self.request.POST else None)
+ if not self.object.profile.report_frequency:
+ del context['profile_form'].fields["last_report"]
return context
@@ -157,8 +158,12 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
context['history_list'] = history_table
- club_list = Membership.objects.filter(user=user, date_end__gte=date.today())\
- .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
+ club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
+ .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
+ .order_by("club__name", "-date_start")
+ # Display only the most recent membership
+ club_list = club_list.distinct("club__name")\
+ if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_list
membership_table = MembershipTable(data=club_list, prefix='membership-')
membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1))
context['club_list'] = membership_table
@@ -166,6 +171,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
# Check permissions to see if the authenticated user can lock/unlock the note
with transaction.atomic():
modified_note = NoteUser.objects.get(pk=user.note.pk)
+ # Don't log these tests
+ modified_note._no_signal = True
modified_note.is_active = True
modified_note.inactivity_reason = 'manual'
context["can_lock_note"] = user.note.is_active and PermissionBackend\
@@ -178,6 +185,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context["can_force_lock"] = user.note.is_active and PermissionBackend\
.check_perm(self.request.user, "note.change_note_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
@@ -227,6 +235,13 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
return qs
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
+ .filter(profile__registration_valid=False)
+ context["can_manage_registrations"] = pre_registered_users.exists()
+ return context
+
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
@@ -240,8 +255,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
- context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend
- .filter_queryset(self.request.user, Alias, "view")).all())
+ context["aliases"] = AliasTable(
+ note.alias_set.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note,
name="",
@@ -392,7 +407,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
club.update_membership_dates()
# 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())\
.order_by('user__last_name').all()
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
# transaction history
@@ -405,8 +421,12 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
# member list
club_member = Membership.objects.filter(
club=club,
- date_end__gte=date.today(),
- ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
+ date_end__gte=date.today() - timedelta(days=15),
+ ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
+ .order_by("user__username", "-date_start")
+ # Display only the most recent membership
+ club_member = club_member.distinct("user__username")\
+ if settings.DATABASES["default"]["ENGINE"] == 'django.db.backends.postgresql' else club_member
membership_table = MembershipTable(data=club_member, prefix="membership-")
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
@@ -438,8 +458,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
note = context['object'].note
- context["aliases"] = AliasTable(note.alias_set.filter(PermissionBackend
- .filter_queryset(self.request.user, Alias, "view")).all())
+ context["aliases"] = AliasTable(note.alias_set.filter(
+ PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
note=context["object"].note,
name="",
@@ -638,8 +658,8 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
if club.name != "Kfet" and club.parent_club and not Membership.objects.filter(
user=form.instance.user,
club=club.parent_club,
- date_start__lte=club.parent_club.membership_start,
- date_end__gte=club.parent_club.membership_end,
+ date_start__gte=club.parent_club.membership_start,
+ date_end__lte=club.parent_club.membership_end,
).exists():
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
error = True
@@ -658,11 +678,13 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"):
if not last_name:
form.add_error('last_name', _("This field is required."))
+ error = True
if not first_name:
form.add_error('first_name', _("This field is required."))
+ error = True
if not bank and credit_type.special_type == "Chèque":
form.add_error('bank', _("This field is required."))
- return self.form_invalid(form)
+ error = True
return not error
@@ -676,6 +698,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
.get(pk=self.kwargs["club_pk"])
user = form.instance.user
+ old_membership = None
else: # get from url for renewal
old_membership = self.get_queryset().get(pk=self.kwargs["pk"])
club = old_membership.club
@@ -750,6 +773,9 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
member_role = Role.objects.filter(Q(name="Adhérent BDE") | Q(name="Membre de club")).all() \
if club.name == "BDE" else Role.objects.filter(Q(name="Adhérent Kfet") | Q(name="Membre de club")).all() \
if club.name == "Kfet"else Role.objects.filter(name="Membre de club").all()
+ # Set the same roles as before
+ if old_membership:
+ member_role = member_role.union(old_membership.roles.all())
form.instance.roles.set(member_role)
form.instance._force_save = True
form.instance.save()
@@ -787,7 +813,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
return ret
def get_success_url(self):
- return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
+ return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
diff --git a/apps/note/apps.py b/apps/note/apps.py
index b3dc5a0f..af1dcc12 100644
--- a/apps/note/apps.py
+++ b/apps/note/apps.py
@@ -3,7 +3,7 @@
from django.apps import AppConfig
from django.conf import settings
-from django.db.models.signals import post_save, pre_delete
+from django.db.models.signals import pre_delete, pre_save, post_save
from django.utils.translation import gettext_lazy as _
from . import signals
@@ -17,6 +17,15 @@ class NoteConfig(AppConfig):
"""
Define app internal signals to interact with other apps
"""
+ pre_save.connect(
+ signals.pre_save_note,
+ sender="note.noteuser",
+ )
+ pre_save.connect(
+ signals.pre_save_note,
+ sender="note.noteclub",
+ )
+
post_save.connect(
signals.save_user_note,
sender=settings.AUTH_USER_MODEL,
diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py
index 49b9fd58..c649dbc9 100644
--- a/apps/note/models/notes.py
+++ b/apps/note/models/notes.py
@@ -159,20 +159,6 @@ class NoteUser(Note):
def pretty(self):
return _("%(user)s's note") % {'user': str(self.user)}
- @transaction.atomic
- def save(self, *args, **kwargs):
- if self.pk and self.balance < 0:
- old_note = NoteUser.objects.get(pk=self.pk)
- super().save(*args, **kwargs)
- if old_note.balance >= 0:
- # Passage en négatif
- self.last_negative = timezone.now()
- self._force_save = True
- self.save(*args, **kwargs)
- self.send_mail_negative_balance()
- else:
- super().save(*args, **kwargs)
-
def send_mail_negative_balance(self):
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self))
html = render_to_string("note/mails/negative_balance.html", dict(note=self))
@@ -201,20 +187,6 @@ class NoteClub(Note):
def pretty(self):
return _("Note of %(club)s club") % {'club': str(self.club)}
- @transaction.atomic
- def save(self, *args, **kwargs):
- if self.pk and self.balance < 0:
- old_note = NoteClub.objects.get(pk=self.pk)
- super().save(*args, **kwargs)
- if old_note.balance >= 0:
- # Passage en négatif
- self.last_negative = timezone.now()
- self._force_save = True
- self.save(*args, **kwargs)
- self.send_mail_negative_balance()
- else:
- super().save(*args, **kwargs)
-
def send_mail_negative_balance(self):
plain_text = render_to_string("note/mails/negative_balance.txt", dict(note=self))
html = render_to_string("note/mails/negative_balance.html", dict(note=self))
diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py
index bfe39a42..b204e623 100644
--- a/apps/note/models/transactions.py
+++ b/apps/note/models/transactions.py
@@ -217,6 +217,9 @@ class Transaction(PolymorphicModel):
# When source == destination, no money is transferred and no transaction is created
return
+ self.source = Note.objects.select_for_update().get(pk=self.source_id)
+ self.destination = Note.objects.select_for_update().get(pk=self.destination_id)
+
# Check that the amounts stay between big integer bounds
diff_source, diff_dest = self.validate()
diff --git a/apps/note/signals.py b/apps/note/signals.py
index c1545ec2..8c02b3a5 100644
--- a/apps/note/signals.py
+++ b/apps/note/signals.py
@@ -1,6 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
+from django.utils import timezone
+
def save_user_note(instance, raw, **_kwargs):
"""
@@ -25,6 +27,16 @@ def save_club_note(instance, raw, **_kwargs):
instance.note.save()
+def pre_save_note(instance, raw, **_kwargs):
+ if not raw and instance.pk and not hasattr(instance, "_no_signal") and instance.balance < 0:
+ from note.models import Note
+ old_note = Note.objects.get(pk=instance.pk)
+ if old_note.balance >= 0:
+ # Passage en négatif
+ instance.last_negative = timezone.now()
+ instance.send_mail_negative_balance()
+
+
def delete_transaction(instance, **_kwargs):
"""
Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first.
diff --git a/apps/note/static/note/js/transfer.js b/apps/note/static/note/js/transfer.js
index 5fe8553a..02388545 100644
--- a/apps/note/static/note/js/transfer.js
+++ b/apps/note/static/note/js/transfer.js
@@ -67,7 +67,11 @@ $(document).ready(function () {
last.quantity = 1
- if (!last.note.user) {
+ if (last.note.club) {
+ $('#last_name').val(last.note.name)
+ $('#first_name').val(last.note.name)
+ }
+ else if (!last.note.user) {
$.getJSON('/api/note/note/' + last.note.id + '/?format=json', function (note) {
last.note.user = note.user
$.getJSON('/api/user/' + last.note.user + '/', function (user) {
@@ -246,7 +250,7 @@ $('#btn_transfer').click(function () {
error = true
}
- if (!reason_field.val()) {
+ if (!reason_field.val() && $('#type_transfer').is(':checked')) {
reason_field.addClass('is-invalid')
$('#reason-required').html('' + gettext('This field is required.') + '')
error = true
@@ -377,7 +381,7 @@ $('#btn_transfer').click(function () {
alias = sources_notes_display[0].name
source_id = user_note.id
dest_id = special_note
- reason = 'Retrait ' + $('#credit_type option:selected').text().toLowerCase()
+ reason = 'Retrait ' + $('#debit_type option:selected').text().toLowerCase()
if (given_reason.length > 0) { reason += ' (' + given_reason + ')' }
}
$.post('/api/note/transaction/transaction/',
diff --git a/apps/note/templates/note/conso_form.html b/apps/note/templates/note/conso_form.html
index c8372e76..d6044b87 100644
--- a/apps/note/templates/note/conso_form.html
+++ b/apps/note/templates/note/conso_form.html
@@ -159,7 +159,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %}
{% block extrajavascript %}
-
+
-
-
- {% if settings.CAS_FAVICON_URL %}{% endif %}
-
-
-
-
-
- {% if auto_submit %}
{% endif %}
-
-
-
- {% if auto_submit %}
{% endif %}
- {% block content %}{% endblock %}
-
-
-
-
-
-
- {% if settings.CAS_SHOW_POWERED %}
-
- {% endif %}
-
-
-
-
- {% block javascript %}{% endblock %}
-
-