mirror of https://gitlab.crans.org/bde/nk20
Compare commits
9 Commits
6c9cf73848
...
24ac3ce45f
Author | SHA1 | Date |
---|---|---|
Yohann D'ANELLO | 24ac3ce45f | |
Yohann D'ANELLO | 018ca84e2d | |
Yohann D'ANELLO | 2851d7764c | |
Yohann D'ANELLO | c205219d47 | |
Yohann D'ANELLO | b0398e59b8 | |
Yohann D'ANELLO | 9c3e978a41 | |
Yohann D'ANELLO | 21f1347a60 | |
Yohann D'ANELLO | af857d6fae | |
Yohann D'ANELLO | acf7ecc4ae |
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,30 +88,33 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||
data=self.request.POST,
|
||||
instance=self.object.profile,
|
||||
)
|
||||
if form.is_valid() and profile_form.is_valid():
|
||||
new_username = form.data['username']
|
||||
alias = Alias.objects.filter(name=new_username)
|
||||
# Si le nouveau pseudo n'est pas un de nos alias,
|
||||
# on supprime éventuellement un alias similaire pour le remplacer
|
||||
if not alias.exists():
|
||||
similar = Alias.objects.filter(
|
||||
normalized_name=Alias.normalize(new_username))
|
||||
if similar.exists():
|
||||
similar.delete()
|
||||
profile_form.full_clean()
|
||||
if not profile_form.is_valid():
|
||||
return super().form_invalid(form)
|
||||
|
||||
olduser = User.objects.get(pk=form.instance.pk)
|
||||
new_username = form.data['username']
|
||||
alias = Alias.objects.filter(name=new_username)
|
||||
# Si le nouveau pseudo n'est pas un de nos alias,
|
||||
# on supprime éventuellement un alias similaire pour le remplacer
|
||||
if not alias.exists():
|
||||
similar = Alias.objects.filter(
|
||||
normalized_name=Alias.normalize(new_username))
|
||||
if similar.exists():
|
||||
similar.delete()
|
||||
|
||||
user = form.save(commit=False)
|
||||
profile = profile_form.save(commit=False)
|
||||
profile.user = user
|
||||
profile.save()
|
||||
user.save()
|
||||
olduser = User.objects.get(pk=form.instance.pk)
|
||||
|
||||
if olduser.email != user.email:
|
||||
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
|
||||
user.profile.email_confirmed = False
|
||||
user.profile.save()
|
||||
user.profile.send_email_validation_link()
|
||||
user = form.save(commit=False)
|
||||
profile = profile_form.save(commit=False)
|
||||
profile.user = user
|
||||
profile.save()
|
||||
user.save()
|
||||
|
||||
if olduser.email != user.email:
|
||||
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
|
||||
user.profile.email_confirmed = False
|
||||
user.profile.save()
|
||||
user.profile.send_email_validation_link()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,28 +164,9 @@ class Transaction(PolymorphicModel):
|
|||
models.Index(fields=['destination']),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""
|
||||
When saving, also transfer money between two notes
|
||||
"""
|
||||
|
||||
if not self.source.is_active or not self.destination.is_active:
|
||||
if 'force_insert' not in kwargs or not kwargs['force_insert']:
|
||||
if 'force_update' not in kwargs or not kwargs['force_update']:
|
||||
raise ValidationError(_("The transaction can't be saved since the source note "
|
||||
"or the destination note is not active."))
|
||||
|
||||
# If the aliases are not entered, we assume that the used alias is the name of the note
|
||||
if not self.source_alias:
|
||||
self.source_alias = str(self.source)
|
||||
|
||||
if not self.destination_alias:
|
||||
self.destination_alias = str(self.destination)
|
||||
|
||||
if self.source.pk == self.destination.pk:
|
||||
# When source == destination, no money is transferred
|
||||
super().save(*args, **kwargs)
|
||||
return
|
||||
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
|
||||
|
@ -204,14 +185,63 @@ class Transaction(PolymorphicModel):
|
|||
# previously invalid
|
||||
self.invalidity_reason = None
|
||||
|
||||
# We save first the transaction, in case of the user has no right to transfer money
|
||||
super().save(*args, **kwargs)
|
||||
source_balance = self.source.balance
|
||||
dest_balance = self.destination.balance
|
||||
|
||||
# Save notes
|
||||
self.source._force_save = True
|
||||
self.source.save()
|
||||
self.destination._force_save = True
|
||||
self.destination.save()
|
||||
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']:
|
||||
if 'force_update' not in kwargs or not kwargs['force_update']:
|
||||
raise ValidationError(_("The transaction can't be saved since the source note "
|
||||
"or the destination note is not active."))
|
||||
|
||||
# If the aliases are not entered, we assume that the used alias is the name of the note
|
||||
if not self.source_alias:
|
||||
self.source_alias = str(self.source)
|
||||
|
||||
if not self.destination_alias:
|
||||
self.destination_alias = str(self.destination)
|
||||
|
||||
if self.source.pk == self.destination.pk:
|
||||
# When source == destination, no money is transferred
|
||||
super().save(*args, **kwargs)
|
||||
return
|
||||
|
||||
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):
|
||||
"""
|
||||
|
|
|
@ -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,35 +43,28 @@ 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
|
||||
for o in self.model.model_class().objects.filter(pk=0).all():
|
||||
o._force_delete = True
|
||||
Model.delete(o)
|
||||
# An object with pk 0 wouldn't deleted. That's not normal, we alert admins.
|
||||
msg = "Lors de la vérification d'une permission d'ajout, un objet de clé primaire nulle était "\
|
||||
"encore présent.\n"\
|
||||
"Type de permission : " + self.type + "\n"\
|
||||
"Modèle : " + str(self.model) + "\n"\
|
||||
"Objet trouvé : " + str(model_to_dict(o)) + "\n\n"\
|
||||
"--\nLe BDE"
|
||||
mail_admins("[Note Kfet] Un objet a été supprimé de force", msg)
|
||||
with transaction.atomic():
|
||||
for o in self.model.model_class().objects.filter(pk=0).all():
|
||||
o._force_delete = True
|
||||
Model.delete(o)
|
||||
# An object with pk 0 wouldn't deleted. That's not normal, we alert admins.
|
||||
msg = "Lors de la vérification d'une permission d'ajout, un objet de clé primaire nulle était "\
|
||||
"encore présent.\n"\
|
||||
"Type de permission : " + self.type + "\n"\
|
||||
"Modèle : " + str(self.model) + "\n"\
|
||||
"Objet trouvé : " + str(model_to_dict(o)) + "\n\n"\
|
||||
"--\nLe BDE"
|
||||
mail_admins("[Note Kfet] Un objet a été supprimé de force", msg)
|
||||
|
||||
# Force insertion, no data verification, no trigger
|
||||
obj._force_save = True
|
||||
Model.save(obj, force_insert=True)
|
||||
# We don't want log anything
|
||||
obj._no_log = True
|
||||
ret = self.model.model_class().objects.filter(self.query & Q(pk=0)).exists()
|
||||
# Delete testing object
|
||||
obj._force_delete = True
|
||||
Model.delete(obj)
|
||||
# Force insertion, no data verification, no trigger
|
||||
obj._force_save = True
|
||||
Model.save(obj, force_insert=True)
|
||||
# We don't want log anything
|
||||
obj._no_log = True
|
||||
ret = self.model.model_class().objects.filter(self.query & Q(pk=0)).exists()
|
||||
# Delete testing object
|
||||
obj._force_delete = True
|
||||
Model.delete(obj)
|
||||
|
||||
with open("/tmp/log", "w") as f:
|
||||
f.write(str(obj) + ", " + str(obj.pk) + ", " + str(self.model.model_class().objects.filter(pk=0).exists()))
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -82,7 +82,7 @@ class Product(models.Model):
|
|||
verbose_name=_("Designation"),
|
||||
)
|
||||
|
||||
quantity = models.PositiveIntegerField(
|
||||
quantity = models.IntegerField(
|
||||
verbose_name=_("Quantity")
|
||||
)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
{# User details column #}
|
||||
<div class="col">
|
||||
<div class="card border-success shadow mb-4 text-center">
|
||||
<img src="/media/pic/default.png"
|
||||
id="profile_pic" alt="" class="card-img-top">
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<h2>{% trans "Account activation" %}</h2>
|
||||
|
|
Loading…
Reference in New Issue