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

Merge branch 'master' into tranfer_front

# Conflicts:
#	static/js/base.js
This commit is contained in:
Yohann D'ANELLO
2020-04-09 22:49:52 +02:00
264 changed files with 5777 additions and 50313 deletions

View File

@ -8,7 +8,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
RecurrentTransaction, MembershipTransaction
RecurrentTransaction, MembershipTransaction, SpecialTransaction
class AliasInlines(admin.TabularInline):
@ -102,7 +102,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
"""
Admin customisation for Transaction
"""
child_models = (RecurrentTransaction, MembershipTransaction)
child_models = (RecurrentTransaction, MembershipTransaction, SpecialTransaction)
list_display = ('created_at', 'poly_source', 'poly_destination',
'quantity', 'amount', 'valid')
list_filter = ('valid',)
@ -138,6 +138,20 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
return []
@admin.register(MembershipTransaction)
class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
"""
Admin customisation for MembershipTransaction
"""
@admin.register(SpecialTransaction)
class SpecialTransactionAdmin(PolymorphicChildModelAdmin):
"""
Admin customisation for SpecialTransaction
"""
@admin.register(TransactionTemplate)
class TransactionTemplateAdmin(admin.ModelAdmin):
"""

View File

@ -90,7 +90,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
Note: NoteSerializer,
NoteUser: NoteUserSerializer,
NoteClub: NoteClubSerializer,
NoteSpecial: NoteSpecialSerializer
NoteSpecial: NoteSpecialSerializer,
}
class Meta:
@ -177,6 +177,7 @@ class SpecialTransactionSerializer(serializers.ModelSerializer):
fields = '__all__'
# noinspection PyUnresolvedReferences
class TransactionPolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
Transaction: TransactionSerializer,
@ -185,5 +186,12 @@ class TransactionPolymorphicSerializer(PolymorphicSerializer):
SpecialTransaction: SpecialTransactionSerializer,
}
try:
from activity.models import GuestTransaction
from activity.api.serializers import GuestTransactionSerializer
model_serializer_mapping[GuestTransaction] = GuestTransactionSerializer
except ImportError: # Activity app is not loaded
pass
class Meta:
model = Transaction

View File

@ -8,7 +8,6 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
@ -25,7 +24,8 @@ class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet):
"""
queryset = Note.objects.all()
serializer_class = NotePolymorphicSerializer
filter_backends = [SearchFilter, OrderingFilter]
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = ['polymorphic_ctype', 'is_active', ]
search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ]
ordering_fields = ['alias__name', 'alias__normalized_name']
@ -60,19 +60,19 @@ class AliasViewSet(ReadProtectedModelViewSet):
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
#alias owner cannot be change once establish
# alias owner cannot be change once establish
setattr(serializer_class.Meta, 'read_only_fields', ('note',))
return serializer_class
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
try:
self.perform_destroy(instance)
except ValidationError as e:
print(e)
return Response({e.code:e.message},status.HTTP_400_BAD_REQUEST)
return Response({e.code: e.message}, status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
def get_queryset(self):
"""
Parse query and apply filters.

View File

@ -1,12 +1,12 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django import forms
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import Autocomplete
from .models import Alias
from .models import TransactionTemplate
from .models import TransactionTemplate, NoteClub
class ImageForm(forms.Form):
@ -31,11 +31,14 @@ class TransactionTemplateForm(forms.ModelForm):
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
widgets = {
'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
Autocomplete(
NoteClub,
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
'api_url': '/api/note/note/',
# We don't evaluate the content type at launch because the DB might be not initialized
'api_url_suffix':
lambda: '&polymorphic_ctype=' + str(ContentType.objects.get_for_model(NoteClub).pk),
'placeholder': 'Note ...',
},
),
}

View File

@ -242,10 +242,10 @@ class Alias(models.Model):
pass
self.normalized_name = normalized_name
def save(self,*args,**kwargs):
def save(self, *args, **kwargs):
self.normalized_name = self.normalize(self.name)
super().save(*args,**kwargs)
super().save(*args, **kwargs)
def delete(self, using=None, keep_parents=False):
if self.name == str(self.note):
raise ValidationError(_("You can't delete your main alias."),

View File

@ -2,7 +2,6 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models
from django.db.models import F
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
@ -47,12 +46,14 @@ class TransactionTemplate(models.Model):
unique=True,
error_messages={'unique': _("A template with this name already exist")},
)
destination = models.ForeignKey(
NoteClub,
on_delete=models.PROTECT,
related_name='+', # no reverse
verbose_name=_('destination'),
)
amount = models.PositiveIntegerField(
verbose_name=_('amount'),
help_text=_('in centimes'),
@ -63,9 +64,12 @@ class TransactionTemplate(models.Model):
verbose_name=_('type'),
max_length=31,
)
display = models.BooleanField(
default=True,
verbose_name=_("display"),
)
description = models.CharField(
verbose_name=_('description'),
max_length=255,
@ -141,6 +145,7 @@ class Transaction(PolymorphicModel):
max_length=255,
default=None,
null=True,
blank=True,
)
class Meta:

View File

@ -2,7 +2,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
def save_user_note(instance, created, raw, **_kwargs):
def save_user_note(instance, raw, **_kwargs):
"""
Hook to create and save a note when an user is updated
"""
@ -10,10 +10,11 @@ def save_user_note(instance, created, raw, **_kwargs):
# When provisionning data, do not try to autocreate
return
if created:
from .models import NoteUser
NoteUser.objects.create(user=instance)
instance.note.save()
if (instance.is_superuser or instance.profile.registration_valid) and instance.is_active:
# Create note only when the registration is validated
from note.models import NoteUser
NoteUser.objects.get_or_create(user=instance)
instance.note.save()
def save_club_note(instance, created, raw, **_kwargs):

View File

@ -106,9 +106,8 @@ DELETE_TEMPLATE = """
class AliasTable(tables.Table):
class Meta:
attrs = {
'class':
'table table condensed table-striped table-hover',
'id':"alias_table"
'class': 'table table condensed table-striped table-hover',
'id': "alias_table"
}
model = Alias
fields = ('name',)
@ -118,9 +117,9 @@ class AliasTable(tables.Table):
name = tables.Column(attrs={'td': {'class': 'text-center'}})
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}})
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}},
verbose_name=_("Delete"),)
class ButtonTable(tables.Table):
@ -136,17 +135,20 @@ class ButtonTable(tables.Table):
}
model = TransactionTemplate
exclude = ('id',)
edit = tables.LinkColumn('note:template_update',
args=[A('pk')],
attrs={'td': {'class': 'col-sm-1'},
'a': {'class': 'btn btn-sm btn-primary'}},
text=_('edit'),
accessor='pk')
accessor='pk',
verbose_name=_("Edit"),)
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}})
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}},
verbose_name=_("Delete"),)
def render_amount(self, value):
return pretty_money(value)

View File

@ -18,10 +18,5 @@ def pretty_money(value):
)
def cents_to_euros(value):
return "{:.02f}".format(value / 100) if value else ""
register = template.Library()
register.filter('pretty_money', pretty_money)
register.filter('cents_to_euros', cents_to_euros)

View File

@ -4,7 +4,6 @@
from django.urls import path
from . import views
from .models import Note
app_name = 'note'
urlpatterns = [
@ -13,7 +12,4 @@ urlpatterns = [
path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'),
path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'),
path('consos/', views.ConsoView.as_view(), name='consos'),
# API for the note autocompleter
path('note-autocomplete/', views.NoteAutocomplete.as_view(model=Note), name='note_autocomplete'),
]

View File

@ -1,23 +1,24 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, UpdateView
from django_tables2 import SingleTableView
from django.urls import reverse_lazy
from note_kfet.inputs import AmountInput
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms import TransactionTemplateForm
from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial
from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
from .models.transactions import SpecialTransaction
from .tables import HistoryTable, ButtonTable
class TransactionCreateView(LoginRequiredMixin, SingleTableView):
class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`.
e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial`
@ -27,12 +28,9 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
model = Transaction
# Transaction history table
table_class = HistoryTable
table_pagination = {"per_page": 50}
def get_queryset(self):
return Transaction.objects.filter(PermissionBackend.filter_queryset(
self.request.user, Transaction, "view")
).order_by("-id").all()[:50]
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-id").all()[:20]
def get_context_data(self, **kwargs):
"""
@ -40,109 +38,62 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
"""
context = super().get_context_data(**kwargs)
context['title'] = _('Transfer money')
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
context['special_types'] = NoteSpecial.objects\
.filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
.order_by("special_type").all()
# Add a shortcut for entry page for open activities
if "activity" in settings.INSTALLED_APPS:
from activity.models import Activity
context["activities_open"] = Activity.objects.filter(open=True).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter(
PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all()
return context
class NoteAutocomplete(autocomplete.Select2QuerySetView):
class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Auto complete note by aliases. Used in every search field for note
ex: :view:`ConsoView`, :view:`TransactionCreateView`
"""
def get_queryset(self):
"""
When someone look for an :models:`note.Alias`, a query is sent to the dedicated API.
This function handles the result and return a filtered list of aliases.
"""
# Un utilisateur non connecté n'a accès à aucune information
if not self.request.user.is_authenticated:
return Alias.objects.none()
qs = Alias.objects.all()
# self.q est le paramètre de la recherche
if self.q:
qs = qs.filter(Q(name__regex="^" + self.q) | Q(normalized_name__regex="^" + Alias.normalize(self.q))) \
.order_by('normalized_name').distinct()
# Filtrage par type de note (user, club, special)
note_type = self.forwarded.get("note_type", None)
if note_type:
types = str(note_type).lower()
if "user" in types:
qs = qs.filter(note__polymorphic_ctype__model="noteuser")
elif "club" in types:
qs = qs.filter(note__polymorphic_ctype__model="noteclub")
elif "special" in types:
qs = qs.filter(note__polymorphic_ctype__model="notespecial")
else:
qs = qs.none()
return qs
def get_result_label(self, result):
"""
Show the selected alias and the username associated
<Alias> (aka. <Username> )
"""
# Gère l'affichage de l'alias dans la recherche
res = result.name
note_name = str(result.note)
if res != note_name:
res += " (aka. " + note_name + ")"
return res
def get_result_value(self, result):
"""
The value used for the transactions will be the id of the Note.
"""
return str(result.note.pk)
class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
"""
Create TransactionTemplate
Create Transaction template
"""
model = TransactionTemplate
form_class = TransactionTemplateForm
success_url = reverse_lazy('note:template_list')
class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List TransactionsTemplates
List Transaction templates
"""
model = TransactionTemplate
table_class = ButtonTable
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update Transaction template
"""
model = TransactionTemplate
form_class = TransactionTemplateForm
success_url = reverse_lazy('note:template_list')
class ConsoView(LoginRequiredMixin, SingleTableView):
class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
The Magic View that make people pay their beer and burgers.
(Most of the magic happens in the dark world of Javascript see consos.js)
"""
model = Transaction
template_name = "note/conso_form.html"
# Transaction history table
table_class = HistoryTable
table_pagination = {"per_page": 50}
def get_queryset(self):
return Transaction.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
).order_by("-id").all()[:50]
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-id").all()[:20]
def get_context_data(self, **kwargs):
"""