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 %} - + - - - - {% block javascript %}{% endblock %} - - - diff --git a/note_kfet/templates/registration/signup.html b/note_kfet/templates/registration/signup.html index 268ba7ff..0a15fa07 100644 --- a/note_kfet/templates/registration/signup.html +++ b/note_kfet/templates/registration/signup.html @@ -23,6 +23,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% csrf_token %} {{ form|crispy }} {{ profile_form|crispy }} + {{ soge_form|crispy }} diff --git a/note_kfet/urls.py b/note_kfet/urls.py index ae6bf3db..d4341bc6 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -5,15 +5,14 @@ from django.conf import settings from django.conf.urls.static import static from django.urls import path, include from django.views.defaults import bad_request, permission_denied, page_not_found, server_error -from django.views.generic import RedirectView - from member.views import CustomLoginView from .admin import admin_site +from .views import IndexView urlpatterns = [ # Dev so redirect to something random - path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'), + path('', IndexView.as_view(), name='index'), # Include project routers path('note/', include('note.urls')), @@ -40,12 +39,11 @@ urlpatterns = [ if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - -if "cas_server" in settings.INSTALLED_APPS: - urlpatterns += [ - # Include CAS Server routers - path('cas/', include('cas_server.urls', namespace="cas_server")), - ] +if "oauth2_provider" in settings.INSTALLED_APPS: + # OAuth2 provider + urlpatterns.append( + path('o/', include('oauth2_provider.urls', namespace='oauth2_provider')) + ) if "debug_toolbar" in settings.INSTALLED_APPS: import debug_toolbar diff --git a/note_kfet/views.py b/note_kfet/views.py new file mode 100644 index 00000000..bd2b2424 --- /dev/null +++ b/note_kfet/views.py @@ -0,0 +1,30 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse +from django.views.generic import RedirectView +from note.models import Alias +from permission.backends import PermissionBackend + + +class IndexView(LoginRequiredMixin, RedirectView): + def get_redirect_url(self, *args, **kwargs): + """ + Calculate the index page according to the roles. + A normal user will have access to the transfer page. + A non-Kfet member will have access to its user detail page. + The user "note" will display the consumption interface. + """ + user = self.request.user + + # The account note will have the consumption page as default page + if not PermissionBackend.check_perm(user, "auth.view_user", user): + return reverse("note:consos") + + # People that can see the alias BDE are Kfet members + if PermissionBackend.check_perm(user, "alias.view_alias", Alias.objects.get(name="BDE")): + return reverse("note:transfer") + + # Non-Kfet members will don't see the transfer page, but their profile page + return reverse("member:user_detail", args=(user.pk,)) diff --git a/requirements.txt b/requirements.txt index dccb8988..d889dd54 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,18 @@ beautifulsoup4~=4.7.1 Django~=2.2.15 django-bootstrap-datepicker-plus~=3.0.5 -django-cas-server>=1.2.0 django-colorfield~=0.3.2 django-crispy-forms~=1.7.2 django-extensions~=2.1.4 django-filter~=2.1.0 django-htcpcp-tea~=0.3.1 django-mailer~=2.0.1 +django-oauth-toolkit~=1.3.3 django-phonenumber-field~=5.0.0 django-polymorphic~=2.0.3 djangorestframework~=3.9.0 django-rest-polymorphic~=0.1.9 django-tables2~=2.3.1 +python-memcached~=1.59 phonenumbers~=8.9.10 Pillow>=5.4.1