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

Merge branch 'beta' into JS_translations

# Conflicts:
#	apps/note/static/note/js/consos.js
#	locale/de/LC_MESSAGES/django.po
#	locale/es/LC_MESSAGES/django.po
#	locale/fr/LC_MESSAGES/django.po
This commit is contained in:
Yohann D'ANELLO
2020-11-16 00:59:26 +01:00
53 changed files with 1121 additions and 684 deletions

View File

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

View File

@ -150,6 +150,7 @@ class ClubForm(forms.ModelForm):
"membership_fee_unpaid": AmountInput(),
"parent_club": Autocomplete(
Club,
resetable=True,
attrs={
'api_url': '/api/members/club/',
}

View File

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

View File

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

View File

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

View File

@ -25,25 +25,27 @@
</a>
</dd>
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
{% if "member.view_profile"|has_perm:user_object.profile %}
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.section }}</dd>
<dt class="col-xl-6">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ user_object.email }}">{{ user_object.email }}</a></dd>
<dt class="col-xl-6">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ user_object.email }}">{{ user_object.email }}</a></dd>
<dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt>
<dd class="col-xl-6"><a href="tel:{{ user_object.profile.phone_number }}">{{ user_object.profile.phone_number }}</a>
</dd>
<dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt>
<dd class="col-xl-6"><a href="tel:{{ user_object.profile.phone_number }}">{{ user_object.profile.phone_number }}</a>
</dd>
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.address }}</dd>
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
{% if user_object.note and "note.view_note"|has_perm:user_object.note %}
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.note.balance | pretty_money }}</dd>
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
<dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt>
<dd class="col-xl-6">{{ user_object.profile.paid|yesno }}</dd>
{% endif %}
{% endif %}
</dl>

View File

@ -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 %}
<a class="btn btn-block btn-secondary mb-3" href="{% url 'registration:future_user_list' %}">
<i class="fa fa-user-plus"></i> {% trans "Registrations" %}
</a>

View File

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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('<strong>' + gettext('This field is required.') + '</strong>')
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/',

View File

@ -159,7 +159,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript" src="{% static "note/js/consos.js" 'javascript-catalog' %}"></script>
<script type="text/javascript" src="{% static "note/js/consos.js" %}"></script>
<script type="text/javascript">
{% for button in highlighted %}
{% if button.display %}

View File

@ -115,7 +115,7 @@
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"permanent": false,
"description": "Voir les aliases des notes des clubs et des adhérents du club Kfet"
}
},
@ -799,12 +799,12 @@
"member",
"membership"
],
"query": "{\"club\": [\"club\"]}",
"query": "{}",
"type": "change",
"mask": 3,
"field": "roles",
"permanent": false,
"description": "Modifier les rôles d'un adhérent d'un club"
"description": "Modifier les rôles d'une adhésion"
}
},
{
@ -2081,7 +2081,7 @@
],
"query": "{}",
"type": "change",
"mask": 1,
"mask": 2,
"field": "invalidity_reason",
"permanent": false,
"description": "Modifier la raison d'invalidité d'une transaction"
@ -2791,6 +2791,86 @@
"description": "Voir tous les alias, y compris ceux des non adhérents"
}
},
{
"model": "permission.permission",
"pk": 179,
"fields": {
"model": [
"note",
"alias"
],
"query": "{\"note__noteuser__user\": [\"user\"]}",
"type": "view",
"mask": 1,
"field": "",
"permanent": true,
"description": "Voir ses propres alias, pour toujours"
}
},
{
"model": "permission.permission",
"pk": 180,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"profile__registration_valid\": false}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel utilisateur non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 181,
"fields": {
"model": [
"member",
"profile"
],
"query": "{\"registration_valid\": false}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel profil non encore inscrit"
}
},
{
"model": "permission.permission",
"pk": 182,
"fields": {
"model": [
"auth",
"user"
],
"query": "{\"memberships__club__name\": \"BDE\", \"memberships__roles__name\": \"Adhérent BDE\", \"memberships__date_start__lte\": [\"today\"], \"memberships__date_end__gte\": [\"today\"]}",
"type": "view",
"mask": 2,
"field": "",
"permanent": false,
"description": "Voir n'importe quel utilisateur qui est adhérent BDE"
}
},
{
"model": "permission.permission",
"pk": 183,
"fields": {
"model": [
"note",
"note"
],
"query": "{}",
"type": "change",
"mask": 1,
"field": "display_image",
"permanent": false,
"description": "Changer l'image de n'importe quelle note"
}
},
{
"model": "permission.role",
"pk": 1,
@ -2861,7 +2941,8 @@
157,
158,
159,
160
160,
179
]
}
},
@ -2922,14 +3003,14 @@
62,
127,
133,
135,
136,
141,
142,
150,
166,
167,
168
168,
182
]
}
},
@ -2965,6 +3046,7 @@
31,
32,
33,
51,
53,
54,
55,
@ -2988,6 +3070,7 @@
137,
138,
139,
140,
143,
146,
147,
@ -3003,7 +3086,8 @@
175,
176,
177,
178
178,
183
]
}
},
@ -3186,7 +3270,12 @@
175,
176,
177,
178
178,
179,
180,
181,
182,
183
]
}
},
@ -3220,7 +3309,12 @@
170,
171,
176,
177
177,
178,
179,
180,
181,
182
]
}
},
@ -3383,7 +3477,6 @@
135,
136,
137,
138,
139,
140,
143,
@ -3396,6 +3489,41 @@
]
}
},
{
"model": "permission.role",
"pk": 20,
"fields": {
"for_club": 2,
"name": "PC Kfet",
"permissions": [
6,
22,
24,
25,
26,
27,
30,
49,
50,
55,
56,
57,
58,
137,
143,
147,
150,
166,
167,
168,
176,
177,
180,
181,
182
]
}
},
{
"model": "wei.weirole",
"pk": 12,

View File

@ -43,7 +43,9 @@ class InstancedPermission:
obj = copy(obj)
obj.pk = 0
with transaction.atomic():
sid = transaction.savepoint()
for o in self.model.model_class().objects.filter(pk=0).all():
o._no_signal = True
o._force_delete = True
Model.delete(o)
# An object with pk 0 wouldn't deleted. That's not normal, we alert admins.
@ -61,9 +63,7 @@ class InstancedPermission:
obj._no_signal = True
Model.save(obj, force_insert=True)
ret = self.model.model_class().objects.filter(self.query & Q(pk=0)).exists()
# Delete testing object
obj._force_delete = True
Model.delete(obj)
transaction.savepoint_rollback(sid)
return ret

View File

@ -51,8 +51,10 @@ class ProtectQuerysetMixin:
# No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make
# a custom request.
# We could also delete the field, but some views might be affected.
meta = form.instance._meta
for key in form.base_fields:
if not PermissionBackend.check_perm(self.request.user, "wei.change_weiregistration_" + key, self.object):
if not PermissionBackend.check_perm(self.request.user,
f"{meta.app_label}.change_{meta.model_name}_" + key, self.object):
form.fields[key].widget = HiddenInput()
return form

View File

@ -44,6 +44,15 @@ class SignUpForm(UserCreationForm):
fields = ('first_name', 'last_name', 'username', 'email', )
class DeclareSogeAccountOpenedForm(forms.Form):
soge_account = forms.BooleanField(
label=_("I declare that I opened a bank account in the Société générale with the BDE partnership."),
help_text=_("Warning: this engages you to open your bank account. If you finally decides to don't open your "
"account, you will have to pay the BDE membership."),
required=False,
)
class WEISignupForm(forms.Form):
wei_registration = forms.BooleanField(
label=_("Register to the WEI"),

View File

@ -4,6 +4,8 @@
import django_tables2 as tables
from django.contrib.auth.models import User
from treasury.models import SogeCredit
class FutureUserTable(tables.Table):
"""
@ -21,6 +23,7 @@ class FutureUserTable(tables.Table):
fields = ('last_name', 'first_name', 'username', 'email', )
model = User
row_attrs = {
'class': 'table-row',
'class': lambda record: 'table-row'
+ (' bg-warning' if SogeCredit.objects.filter(user=record).exists() else ''),
'data-href': lambda record: record.pk
}

View File

@ -56,6 +56,13 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="card-header text-center" >
<h4> {% trans "Validate account" %}</h4>
</div>
{% if declare_soge_account %}
<div class="alert alert-info">
{% trans "The user declared that he/she opened a bank account in the Société générale." %}
</div>
{% endif %}
<div class="card-body" id="profile_infos">
{% csrf_token %}
{{ form|crispy }}
@ -104,7 +111,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
soge_field.change(fillFields);
{% if object.profile.soge %}
{% if declare_soge_account %}
soge_field.attr('checked', true);
fillFields();
{% endif %}

View File

@ -24,7 +24,7 @@ from permission.models import Role
from permission.views import ProtectQuerysetMixin
from treasury.models import SogeCredit
from .forms import SignUpForm, ValidationForm
from .forms import SignUpForm, ValidationForm, DeclareSogeAccountOpenedForm
from .tables import FutureUserTable
from .tokens import email_validation_token
@ -42,6 +42,7 @@ class UserCreateView(CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form(self.request.POST if self.request.POST else None)
context["soge_form"] = DeclareSogeAccountOpenedForm(self.request.POST if self.request.POST else None)
del context["profile_form"].fields["section"]
del context["profile_form"].fields["report_frequency"]
del context["profile_form"].fields["last_report"]
@ -72,6 +73,13 @@ class UserCreateView(CreateView):
user.profile.send_email_validation_link()
soge_form = DeclareSogeAccountOpenedForm(self.request.POST)
if "soge_account" in soge_form.data and soge_form.data["soge_account"]:
# If the user declares that a bank account got opened, prepare the soge credit to warn treasurers
soge_credit = SogeCredit(user=user)
soge_credit._force_save = True
soge_credit.save()
return super().form_valid(form)
def get_success_url(self):
@ -182,7 +190,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
| Q(username__iregex="^" + pattern)
)
return qs[:20]
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@ -227,6 +235,8 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
fee += 8000
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
return ctx
def get_form(self, form_class=None):
@ -307,6 +317,13 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
user.profile.save()
user.refresh_from_db()
if not soge and SogeCredit.objects.filter(user=user).exists():
# If the user declared that a bank account was opened but in the validation form the SoGé case was
# unchecked, delete the associated credit
soge_credit = SogeCredit.objects.get(user=user)
soge_credit._force_delete = True
soge_credit.delete()
if credit_type is not None and credit_amount > 0:
# Credit the note
SpecialTransaction.objects.create(
@ -373,6 +390,8 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
user = User.objects.filter(profile__registration_valid=False)\
.filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\
.get(pk=self.kwargs["pk"])
# Delete associated soge credits before
SogeCredit.objects.filter(user=user).delete()
user.delete()

View File

@ -28,6 +28,8 @@ class TreasuryConfig(AppConfig):
source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy=None,
):
SpecialTransactionProxy.objects.create(transaction=transaction, remittance=None)
proxy = SpecialTransactionProxy(transaction=transaction, remittance=None)
proxy._force_save = True
proxy.save()
post_migrate.connect(setup_specialtransactions_proxies, sender=SpecialTransactionProxy)

View File

@ -10,7 +10,7 @@ from django.db.models import Q
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction
from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction, NoteUser
class Invoice(models.Model):
@ -335,6 +335,11 @@ class SogeCredit(models.Model):
@transaction.atomic
def save(self, *args, **kwargs):
# This is a pre-registered user that declared that a SoGé account was opened.
# No note exists yet.
if not NoteUser.objects.filter(user=self.user).exists():
return super().save(*args, **kwargs)
if not self.credit_transaction:
credit_transaction = SpecialTransaction(
source=NoteSpecial.objects.get(special_type="Virement bancaire"),

View File

@ -10,9 +10,8 @@ def save_special_transaction(instance, created, **kwargs):
"""
if not hasattr(instance, "_no_signal"):
if instance.is_credit():
if created and RemittanceType.objects.filter(note=instance.source).exists():
SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save()
else:
if created and RemittanceType.objects.filter(note=instance.destination).exists():
SpecialTransactionProxy.objects.create(transaction=instance, remittance=None).save()
if created and RemittanceType.objects.filter(
note=instance.source if instance.is_credit() else instance.destination).exists():
proxy = SpecialTransactionProxy(transaction=instance, remittance=None)
proxy._force_save = True
proxy.save()

View File

@ -147,4 +147,4 @@ class SogeCreditTable(tables.Table):
class Meta:
model = SogeCredit
fields = ('user', 'amount', 'valid', )
fields = ('user', 'user__last_name', 'user__first_name', 'amount', 'valid', )

View File

@ -11,8 +11,14 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-right">{% trans 'user'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user }}</a></dd>
<dt class="col-xl-6 text-right">{% trans 'last name'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.last_name }}</dd>
<dt class="col-xl-6 text-right">{% trans 'first name'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.first_name }}</dd>
<dt class="col-xl-6 text-right">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user.username }}</a></dd>
{% if "note.view_note_balance"|has_perm:object.user.note %}
<dt class="col-xl-6 text-right">{% trans 'balance'|capfirst %}</dt>

View File

@ -60,7 +60,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
let pattern = searchbar_obj.val();
$("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (
invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table");
invalid_only_obj.is(':checked') ? "" : "&valid=1") + " #credits_table");
$(".table-row").click(function () {
window.document.location = $(this).data("href");

View File

@ -431,7 +431,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
if "valid" not in self.request.GET or not self.request.GET["valid"]:
qs = qs.filter(credit_transaction__valid=False)
return qs[:20]
return qs
class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView):