Compare commits

..

9 Commits

Author SHA1 Message Date
Yohann D'ANELLO 24ac3ce45f Display users that have surnormal roles 2020-08-05 21:07:31 +02:00
Yohann D'ANELLO 018ca84e2d Prevent superusers when they make a transaction with a non-member user 2020-08-05 20:40:30 +02:00
Yohann D'ANELLO 2851d7764c Profile pictures are clickable 2020-08-05 19:52:36 +02:00
Yohann D'ANELLO c205219d47 🐛 Fix transaction update concurency 2020-08-05 19:42:44 +02:00
Yohann D'ANELLO b0398e59b8 🐛 Fix treasury 2020-08-05 18:04:01 +02:00
Yohann D'ANELLO 9c3e978a41 🐛 Fix signup 2020-08-05 16:27:44 +02:00
Yohann D'ANELLO 21f1347a60 🐛 Fix signup 2020-08-05 16:26:44 +02:00
Yohann D'ANELLO af857d6fae 🐛 Prevent transactions where note balances go out integer bounds 2020-08-05 16:23:32 +02:00
Yohann D'ANELLO acf7ecc4ae Use phone number validator 2020-08-05 14:14:51 +02:00
27 changed files with 870 additions and 635 deletions

View File

@ -4,7 +4,7 @@
from django.contrib.contenttypes.models import ContentType
from permission.backends import PermissionBackend
from rest_framework import viewsets
from note_kfet.middlewares import get_current_authenticated_user
from note_kfet.middlewares import get_current_session
class ReadProtectedModelViewSet(viewsets.ModelViewSet):
@ -17,7 +17,8 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
user = get_current_authenticated_user()
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
@ -31,5 +32,6 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
def get_queryset(self):
user = get_current_authenticated_user()
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()

View File

@ -11,10 +11,10 @@ from django.db import models
from django.db.models import Q
from django.template import loader
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.encoding import force_bytes
from django.utils.http import urlsafe_base64_encode
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from permission.models import Role
from registration.tokens import email_validation_token
@ -34,7 +34,7 @@ class Profile(models.Model):
on_delete=models.CASCADE,
)
phone_number = models.CharField(
phone_number = PhoneNumberField(
verbose_name=_('phone number'),
max_length=50,
blank=True,

View File

@ -69,7 +69,9 @@ 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)
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
data=self.request.POST if self.request.POST else None)
return context
def form_valid(self, form):
@ -86,7 +88,10 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
data=self.request.POST,
instance=self.object.profile,
)
if form.is_valid() and profile_form.is_valid():
profile_form.full_clean()
if not profile_form.is_valid():
return super().form_invalid(form)
new_username = form.data['username']
alias = Alias.objects.filter(name=new_username)
# Si le nouveau pseudo n'est pas un de nos alias,

View File

@ -1,10 +1,15 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.utils import timezone
from rest_framework import serializers
from rest_framework.serializers import ListSerializer
from rest_polymorphic.serializers import PolymorphicSerializer
from member.api.serializers import MembershipSerializer
from member.models import Membership
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from rest_framework.utils import model_meta
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \
@ -108,6 +113,8 @@ class ConsumerSerializer(serializers.ModelSerializer):
email_confirmed = serializers.SerializerMethodField()
membership = serializers.SerializerMethodField()
class Meta:
model = Alias
fields = '__all__'
@ -126,6 +133,17 @@ class ConsumerSerializer(serializers.ModelSerializer):
return obj.note.user.profile.email_confirmed
return True
def get_membership(self, obj):
if isinstance(obj.note, NoteUser):
memberships = Membership.objects.filter(
PermissionBackend.filter_queryset(get_current_authenticated_user(), Membership, "view")).filter(
user=obj.note.user,
club=2, # Kfet
).order_by("-date_start")
if memberships.exists():
return MembershipSerializer().to_representation(memberships.first())
return None
class TemplateCategorySerializer(serializers.ModelSerializer):
"""
@ -209,5 +227,23 @@ class TransactionPolymorphicSerializer(PolymorphicSerializer):
except ImportError: # Activity app is not loaded
pass
def validate(self, attrs):
resource_type = attrs.pop(self.resource_type_field_name)
serializer = self._get_serializer_from_resource_type(resource_type)
if self.instance:
instance = self.instance
info = model_meta.get_field_info(instance)
for attr, value in attrs.items():
if attr in info.relations and info.relations[attr].to_many:
field = getattr(instance, attr)
field.set(value)
else:
setattr(instance, attr, value)
instance.validate(True)
else:
serializer.Meta.model(**attrs).validate(True)
attrs[self.resource_type_field_name] = resource_type
return super().validate(attrs)
class Meta:
model = Transaction

View File

@ -9,7 +9,7 @@ from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from note_kfet.middlewares import get_current_authenticated_user
from note_kfet.middlewares import get_current_session
from permission.backends import PermissionBackend
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
@ -154,5 +154,7 @@ class TransactionViewSet(ReadProtectedModelViewSet):
search_fields = ['$reason', ]
def get_queryset(self):
user = get_current_authenticated_user()
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))
user = self.request.user
get_current_session().setdefault("permission_mask", 42)
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))\
.order_by("created_at", "id")

View File

@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import ValidationError
from django.db import models
from django.db import models, transaction
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@ -164,10 +164,49 @@ class Transaction(PolymorphicModel):
models.Index(fields=['destination']),
]
def validate(self, reset=False):
previous_source_balance = self.source.balance
previous_dest_balance = self.destination.balance
created = self.pk is None
to_transfer = self.amount * self.quantity
if not created:
# Revert old transaction
old_transaction = Transaction.objects.get(pk=self.pk)
if old_transaction.valid:
self.source.balance += to_transfer
self.destination.balance -= to_transfer
if self.valid:
self.source.balance -= to_transfer
self.destination.balance += to_transfer
# When a transaction is declared valid, we ensure that the invalidity reason is null, if it was
# previously invalid
self.invalidity_reason = None
source_balance = self.source.balance
dest_balance = self.destination.balance
if reset:
self.source.balance = previous_source_balance
self.destination.balance = previous_dest_balance
if source_balance > 2147483647 or source_balance < -2147483648\
or dest_balance > 2147483647 or dest_balance < -2147483648:
raise ValidationError(_("The note balances must be between - 21 474 836.47 € and 21 474 836.47 €."))
@transaction.atomic
def save(self, *args, **kwargs):
"""
When saving, also transfer money between two notes
"""
with transaction.atomic():
if self.pk:
self.refresh_from_db()
self.source.refresh_from_db()
self.destination.refresh_from_db()
self.validate(False)
if not self.source.is_active or not self.destination.is_active:
if 'force_insert' not in kwargs or not kwargs['force_insert']:
@ -187,31 +226,22 @@ class Transaction(PolymorphicModel):
super().save(*args, **kwargs)
return
created = self.pk is None
to_transfer = self.amount * self.quantity
if not created:
# Revert old transaction
old_transaction = Transaction.objects.get(pk=self.pk)
if old_transaction.valid:
self.source.balance += to_transfer
self.destination.balance -= to_transfer
if self.valid:
self.source.balance -= to_transfer
self.destination.balance += to_transfer
# When a transaction is declared valid, we ensure that the invalidity reason is null, if it was
# previously invalid
self.invalidity_reason = None
self.log("Saving")
# We save first the transaction, in case of the user has no right to transfer money
super().save(*args, **kwargs)
self.log("Saved")
# Save notes
self.source._force_save = True
self.source.save()
self.log("Source saved")
self.destination._force_save = True
self.destination.save()
self.log("Destination saved")
def log(self, msg):
with open("/tmp/log", "a") as f:
f.write(msg + "\n")
def delete(self, **kwargs):
"""

View File

@ -10,7 +10,7 @@ from time import sleep
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.mail import mail_admins
from django.db import models
from django.db import models, transaction
from django.db.models import F, Q, Model
from django.forms import model_to_dict
from django.utils.translation import gettext_lazy as _
@ -43,14 +43,7 @@ class InstancedPermission:
obj = copy(obj)
obj.pk = 0
# Ensure previous models are deleted
for ignored in range(1000):
if self.model.model_class().objects.filter(pk=0).exists():
# If the object exists, that means that one permission is currently checked.
# We wait before the other permission, at most 1 second.
sleep(0.001)
continue
break
with transaction.atomic():
for o in self.model.model_class().objects.filter(pk=0).all():
o._force_delete = True
Model.delete(o)

51
apps/permission/tables.py Normal file
View File

@ -0,0 +1,51 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django.urls import reverse_lazy
from django.utils.html import format_html
from member.models import Membership
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
class RightsTable(tables.Table):
"""
List managers of a club.
"""
def render_user(self, value):
# If the user has the right, link the displayed user with the page of its detail.
s = value.username
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
return s
def render_club(self, value):
# If the user has the right, link the displayed user with the page of its detail.
s = value.name
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
return s
def render_roles(self, record):
# If the user has the right to manage the roles, display the link to manage them
roles = record.roles.all()
s = ", ".join(str(role) for role in roles)
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
+ "'>" + s + "</a>")
return s
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover',
'style': 'table-layout: fixed;'
}
template_name = 'django_tables2/bootstrap4.html'
fields = ('user.last_name', 'user.first_name', 'user', 'club', 'roles', )
model = Membership

View File

@ -2,13 +2,16 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import date
from django.db.models import Q
from django.forms import HiddenInput
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import UpdateView, TemplateView
from member.models import Membership
from .backends import PermissionBackend
from .models import Role
from .tables import RightsTable
class ProtectQuerysetMixin:
@ -59,4 +62,16 @@ class RightsView(TemplateView):
for role in roles:
role.clubs = [membership.club for membership in active_memberships if role in membership.roles.all()]
if self.request.user.is_authenticated:
special_memberships = Membership.objects.filter(
date_start__lte=timezone.now().date(),
date_end__gte=timezone.now().date(),
).filter(roles__in=Role.objects.filter(~(Q(name="Adhérent BDE")
| Q(name="Adhérent Kfet")
| Q(name="Membre de club")
| Q(name="Adhérent WEI")
| Q(name="1A")))).order_by("club", "user__last_name")\
.distinct().all()
context["special_memberships_table"] = RightsTable(special_memberships)
return context

View File

@ -39,7 +39,7 @@ class UserCreateView(CreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form()
context["profile_form"] = self.second_form(self.request.POST if self.request.POST else None)
del context["profile_form"].fields["section"]
return context

View File

@ -1,11 +1,10 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from django import forms
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import DatePickerInput, AmountInput
@ -19,12 +18,13 @@ class InvoiceForm(forms.ModelForm):
# Django forms don't support date fields. We have to add it manually
date = forms.DateField(
initial=datetime.date.today,
widget=DatePickerInput()
initial=timezone.now,
widget=DatePickerInput(),
)
def clean_date(self):
self.instance.date = self.data.get("date")
return self.instance.date
class Meta:
model = Invoice
@ -36,7 +36,11 @@ class ProductForm(forms.ModelForm):
model = Product
fields = '__all__'
widgets = {
"amount": AmountInput()
"amount": AmountInput(
attrs={
"negative": True,
}
)
}
@ -115,6 +119,12 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
"""
Attach a special transaction to a remittance.
"""
remittance = forms.ModelChoiceField(
queryset=Remittance.objects.none(),
label=_("Remittance"),
empty_label=_("No attached remittance"),
required=False,
)
# Since we use a proxy model for special transactions, we add manually the fields related to the transaction
last_name = forms.CharField(label=_("Last name"))
@ -123,7 +133,7 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
bank = forms.Field(label=_("Bank"))
amount = forms.IntegerField(label=_("Amount"), min_value=0)
amount = forms.IntegerField(label=_("Amount"), min_value=0, widget=AmountInput(), disabled=True, required=False)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -133,33 +143,19 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)
def clean_last_name(self):
"""
Replace the first name in the information of the transaction.
"""
self.instance.transaction.last_name = self.data.get("last_name")
self.instance.transaction.clean()
def clean(self):
cleaned_data = super().clean()
self.instance.transaction.last_name = cleaned_data["last_name"]
self.instance.transaction.first_name = cleaned_data["first_name"]
self.instance.transaction.bank = cleaned_data["bank"]
return cleaned_data
def clean_first_name(self):
def save(self, commit=True):
"""
Replace the last name in the information of the transaction.
Save the transaction and the remittance.
"""
self.instance.transaction.first_name = self.data.get("first_name")
self.instance.transaction.clean()
def clean_bank(self):
"""
Replace the bank in the information of the transaction.
"""
self.instance.transaction.bank = self.data.get("bank")
self.instance.transaction.clean()
def clean_amount(self):
"""
Replace the amount of the transaction.
"""
self.instance.transaction.amount = self.data.get("amount")
self.instance.transaction.clean()
self.instance.transaction.save()
return super().save(commit)
class Meta:
model = SpecialTransactionProxy

View File

@ -82,7 +82,7 @@ class Product(models.Model):
verbose_name=_("Designation"),
)
quantity = models.PositiveIntegerField(
quantity = models.IntegerField(
verbose_name=_("Quantity")
)

View File

@ -64,6 +64,7 @@ class RemittanceTable(tables.Table):
model = Remittance
template_name = 'django_tables2/bootstrap4.html'
fields = ('id', 'date', 'remittance_type', 'comment', 'count', 'amount', 'view',)
order_by = ('-date',)
class SpecialTransactionTable(tables.Table):
@ -100,7 +101,8 @@ class SpecialTransactionTable(tables.Table):
}
model = SpecialTransaction
template_name = 'django_tables2/bootstrap4.html'
fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',)
fields = ('created_at', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',)
order_by = ('-created_at',)
class SogeCreditTable(tables.Table):

View File

@ -238,7 +238,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
closed_remittances = RemittanceTable(
data=Remittance.objects.filter(closed=True).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all(),
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
prefix="closed-remittances-",
)
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
@ -281,8 +281,6 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["table"] = RemittanceTable(data=Remittance.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()
context["special_transactions"] = SpecialTransactionTable(

View File

@ -8,6 +8,8 @@ from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils.translation import gettext_lazy as _
from phonenumber_field.modelfields import PhoneNumberField
from member.models import Club, Membership
from note.models import MembershipTransaction
from permission.models import Role
@ -223,7 +225,7 @@ class WEIRegistration(models.Model):
verbose_name=_("emergency contact name"),
)
emergency_contact_phone = models.CharField(
emergency_contact_phone = PhoneNumberField(
max_length=32,
verbose_name=_("emergency contact phone"),
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,7 @@ INSTALLED_APPS = [
# External apps
'mailer',
'phonenumber_field',
'polymorphic',
'crispy_forms',
'django_tables2',
@ -195,3 +196,7 @@ MEDIA_URL = '/media/'
# Profile Picture Settings
PIC_WIDTH = 200
PIC_RATIO = 1
PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = 'FR'

View File

@ -7,11 +7,13 @@ django-crispy-forms==1.7.2
django-extensions==2.1.9
django-filter==2.2.0
django-mailer==2.0.1
django-phonenumber-field==4.0.0
django-polymorphic==2.0.3
django-tables2==2.1.0
docutils==0.14
idna==2.8
oauthlib==3.1.0
phonenumbers==8.12.7
Pillow==7.1.2
python3-openid==3.1.0
pytz==2019.1

View File

@ -105,8 +105,10 @@ function displayStyle(note) {
css += " text-danger";
else if (balance < 0)
css += " text-warning";
if (!note.email_confirmed)
else if (!note.email_confirmed)
css += " text-white bg-primary";
else if (note.membership && note.membership.date_end < new Date().toISOString())
css += "text-white bg-info";
return css;
}
@ -131,13 +133,9 @@ function displayNote(note, alias, user_note_field = null, profile_pic_field = nu
$("#" + user_note_field).text(alias + (note.balance == null ? "" : (" :\n" + pretty_money(note.balance))));
if (profile_pic_field != null) {
$("#" + profile_pic_field).attr('src', img);
$("#" + profile_pic_field).click(function () {
if (note.resourcetype === "NoteUser") {
document.location.href = "/accounts/user/" + note.user;
} else if (note.resourcetype === "NoteClub") {
document.location.href = "/accounts/club/" + note.club;
}
});
$("#" + profile_pic_field + "_link").attr('href', note.resourcetype === "NoteUser" ?
"/accounts/user/" + note.user : note.resourcetype === "NoteClub" ?
"/accounts/club/" + note.club : "#");
}
}
}
@ -267,6 +265,7 @@ function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_pr
consumers.results.forEach(function (consumer) {
let note = consumer.note;
note.email_confirmed = consumer.email_confirmed;
note.membership = consumer.membership;
let extra_css = displayStyle(note);
aliases_matched_html += li(alias_prefix + '_' + consumer.id,
consumer.name,
@ -371,8 +370,12 @@ function de_validate(id, validated) {
refreshHistory();
},
error: function (err) {
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"];
if (!error)
error = err.responseText;
addMsg("Une erreur est survenue lors de la validation/dévalidation " +
"de cette transaction : " + JSON.parse(err.responseText)["detail"], "danger", 10000);
"de cette transaction : " + error, "danger");
refreshBalance();
// error if this method doesn't exist. Please define it.

View File

@ -145,6 +145,7 @@ function reset() {
$("#consos_list").html("");
$("#user_note").text("");
$("#profile_pic").attr("src", "/media/pic/default.png");
$("#profile_pic_link").attr("href", "#");
refreshHistory();
refreshBalance();
}
@ -212,11 +213,14 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
if (newBalance <= -5000)
addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " +
"succès, mais la note émettrice " + source_alias + " est en négatif sévère.",
"danger", 10000);
"danger", 30000);
else if (newBalance < 0)
addMsg("Attention, La transaction depuis la note " + source_alias + " a été réalisée avec " +
"succès, mais la note émettrice " + source_alias + " est en négatif.",
"warning", 10000);
"warning", 30000);
if (source.note.membership && source.note.membership.date_end > new Date().toISOString())
addMsg("Attention : la note émettrice " + source.name + " n'est plus adhérente.",
"danger", 30000);
}
reset();
}).fail(function (e) {
@ -240,7 +244,7 @@ function consume(source, source_alias, dest, quantity, amount, reason, type, cat
addMsg("La transaction n'a pas pu être validée pour cause de solde insuffisant.", "danger", 10000);
}).fail(function () {
reset();
errMsg(e.responseJSON, 10000);
errMsg(e.responseJSON);
});
});
}

View File

@ -37,6 +37,7 @@ function reset(refresh=true) {
$("#bank").val("");
$("#user_note").val("");
$("#profile_pic").attr("src", "/media/pic/default.png");
$("#profile_pic_link").attr("href", "#");
if (refresh) {
refreshBalance();
refreshHistory();
@ -213,6 +214,13 @@ $("#btn_transfer").click(function() {
error = true;
}
let amount = Math.floor(100 * amount_field.val());
if (amount > 2147483647) {
amount_field.addClass('is-invalid');
$("#amount-required").html("<strong>Le montant ne doit pas excéder 21474836.47 €.</strong>");
error = true;
}
if (!reason_field.val()) {
reason_field.addClass('is-invalid');
$("#reason-required").html("<strong>Ce champ est requis.</strong>");
@ -232,7 +240,6 @@ $("#btn_transfer").click(function() {
if (error)
return;
let amount = 100 * amount_field.val();
let reason = reason_field.val();
if ($("#type_transfer").is(':checked')) {
@ -253,6 +260,13 @@ $("#btn_transfer").click(function() {
"destination": dest.note.id,
"destination_alias": dest.name
}).done(function () {
if (source.note.membership && source.note.membership.date_end > new Date().toISOString())
addMsg("Attention : la note émettrice " + source.name + " n'est plus adhérente.",
"danger", 30000);
if (dest.note.membership && dest.note.membership.date_end > new Date().toISOString())
addMsg("Attention : la note destination " + dest.name + " n'est plus adhérente.",
"danger", 30000);
if (!isNaN(source.note.balance)) {
let newBalance = source.note.balance - source.quantity * dest.quantity * amount;
if (newBalance <= -5000) {
@ -277,7 +291,15 @@ $("#btn_transfer").click(function() {
+ " vers la note " + dest.name + " a été fait avec succès !", "success", 10000);
reset();
}).fail(function () { // do it again but valid = false
}).fail(function (err) { // do it again but valid = false
let errObj = JSON.parse(err.responseText);
if (errObj["non_field_errors"]) {
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + errObj["non_field_errors"], "danger");
return;
}
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
@ -298,9 +320,13 @@ $("#btn_transfer").click(function() {
+ " vers la note " + dest.name + " a échoué : Solde insuffisant", "danger", 10000);
reset();
}).fail(function (err) {
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"]
if (!error)
error = err.responseText;
addMsg("Le transfert de "
+ pretty_money(source.quantity * dest.quantity * amount) + " de la note " + source.name
+ " vers la note " + dest.name + " a échoué : " + err.responseText, "danger");
+ " vers la note " + dest.name + " a échoué : " + error, "danger");
});
});
});
@ -308,19 +334,22 @@ $("#btn_transfer").click(function() {
} else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) {
let special_note = $("#credit_type").val();
let user_note;
let alias;
let given_reason = reason;
let source_id, dest_id;
if ($("#type_credit").is(':checked')) {
user_note = dests_notes_display[0].note.id;
user_note = dests_notes_display[0].note;
alias = dests_notes_display[0].name;
source_id = special_note;
dest_id = user_note;
dest_id = user_note.id;
reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
reason += " (" + given_reason + ")";
}
else {
user_note = sources_notes_display[0].note.id;
source_id = user_note;
user_note = sources_notes_display[0].note;
alias = sources_notes_display[0].name;
source_id = user_note.id;
dest_id = special_note;
reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase();
if (given_reason.length > 0)
@ -336,18 +365,23 @@ $("#btn_transfer").click(function() {
"polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE,
"resourcetype": "SpecialTransaction",
"source": source_id,
"source_alias": sources_notes_display.length ? sources_notes_display[0].name : null,
"source_alias": sources_notes_display.length ? alias : null,
"destination": dest_id,
"destination_alias": dests_notes_display.length ? dests_notes_display[0].name : null,
"destination_alias": dests_notes_display.length ? alias : null,
"last_name": $("#last_name").val(),
"first_name": $("#first_name").val(),
"bank": $("#bank").val()
}).done(function () {
addMsg("Le crédit/retrait a bien été effectué !", "success", 10000);
if (user_note.membership && user_note.membership.date_end > new Date().toISOString())
addMsg("Attention : la note " + alias + " n'est plus adhérente.", "danger", 10000);
reset();
}).fail(function (err) {
addMsg("Le crédit/retrait a échoué : " + JSON.parse(err.responseText)["detail"],
"danger", 10000);
let errObj = JSON.parse(err.responseText);
let error = errObj["detail"] ? errObj["detail"] : errObj["non_field_errors"]
if (!error)
error = err.responseText;
addMsg("Le crédit/retrait a échoué : " + error, "danger", 10000);
});
}
});

View File

@ -1,5 +1,5 @@
<div class="input-group">
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01"
<input class="form-control mx-auto d-block" type="number" {% if not widget.attrs.negative %}min="0"{% endif %} step="0.01"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
name="{{ widget.name }}"
{% for name, value in widget.attrs.items %}

View File

@ -12,8 +12,10 @@
{# User details column #}
<div class="col">
<div class="card border-success shadow mb-4 text-center">
<a id="profile_pic_link" href="#">
<img src="/media/pic/default.png"
id="profile_pic" alt="" class="card-img-top">
</a>
<div class="card-body text-center">
<span id="user_note"></span>
</div>

View File

@ -36,8 +36,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<div class="row">
<div class="col-md-3" id="note_infos_div">
<div class="card border-success shadow mb-4">
<img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block">
<a id="profile_pic_link" href="#"><img src="/media/pic/default.png"
id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block"></a>
<div class="card-body text-center">
<span id="user_note"></span>
</div>

View File

@ -1,8 +1,17 @@
{% extends "base.html" %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block content %}
{% if user.is_authenticated %}
<h2>{% trans "Users that have surnormal rights" %}</h2>
{% render_table special_memberships_table %}
<hr>
{% endif %}
<h2>{% trans "Roles description" %}</h2>
{% if user.is_authenticated %}
<div class="form-check">
<label for="owned_only" class="form-check-label">
@ -11,6 +20,7 @@
</label>
</div>
{% endif %}
<ul>
{% regroup active_memberships by roles as memberships_per_role %}
{% for role in roles %}

View File

@ -1,4 +1,5 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h2>{% trans "Account activation" %}</h2>