diff --git a/apps/member/views.py b/apps/member/views.py index 82c15b99..dacfde33 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -124,7 +124,7 @@ class UserDetailView(LoginRequiredMixin, DetailView): context = super().get_context_data(**kwargs) user = context['user_object'] history_list = \ - Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)) + Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id") context['history_list'] = HistoryTable(history_list) club_list = \ Membership.objects.all().filter(user=user).only("club") diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 73beead1..85f500ed 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -6,7 +6,7 @@ from rest_polymorphic.serializers import PolymorphicSerializer from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ - TemplateTransaction + TemplateTransaction, SpecialTransaction class NoteSerializer(serializers.ModelSerializer): @@ -18,12 +18,6 @@ class NoteSerializer(serializers.ModelSerializer): class Meta: model = Note fields = '__all__' - extra_kwargs = { - 'url': { - 'view_name': 'project-detail', - 'lookup_field': 'pk' - }, - } class NoteClubSerializer(serializers.ModelSerializer): @@ -31,44 +25,60 @@ class NoteClubSerializer(serializers.ModelSerializer): REST API Serializer for Club's notes. The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API. """ + name = serializers.SerializerMethodField() class Meta: model = NoteClub fields = '__all__' + def get_name(self, obj): + return str(obj) + class NoteSpecialSerializer(serializers.ModelSerializer): """ REST API Serializer for special notes. The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API. """ + name = serializers.SerializerMethodField() class Meta: model = NoteSpecial fields = '__all__' + def get_name(self, obj): + return str(obj) + class NoteUserSerializer(serializers.ModelSerializer): """ REST API Serializer for User's notes. The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API. """ + name = serializers.SerializerMethodField() class Meta: model = NoteUser fields = '__all__' + def get_name(self, obj): + return str(obj) + class AliasSerializer(serializers.ModelSerializer): """ REST API Serializer for Aliases. The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API. """ + note = serializers.SerializerMethodField() class Meta: model = Alias fields = '__all__' + def get_note(self, alias): + return NotePolymorphicSerializer().to_representation(alias.note) + class NotePolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { @@ -134,9 +144,21 @@ class MembershipTransactionSerializer(serializers.ModelSerializer): fields = '__all__' +class SpecialTransactionSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Special transactions. + The djangorestframework plugin will analyse the model `SpecialTransaction` and parse all fields in the API. + """ + + class Meta: + model = SpecialTransaction + fields = '__all__' + + class TransactionPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { Transaction: TransactionSerializer, TemplateTransaction: TemplateTransactionSerializer, MembershipTransaction: MembershipTransactionSerializer, + SpecialTransaction: SpecialTransactionSerializer, } diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 14f64003..29c79bd8 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -4,7 +4,7 @@ from django.db.models import Q from django_filters.rest_framework import DjangoFilterBackend from rest_framework import viewsets -from rest_framework.filters import SearchFilter +from rest_framework.filters import OrderingFilter, SearchFilter from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ NoteUserSerializer, AliasSerializer, \ @@ -61,6 +61,9 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet): """ queryset = Note.objects.all() serializer_class = NotePolymorphicSerializer + filter_backends = [SearchFilter, OrderingFilter] + search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ] + ordering_fields = ['alias__name', 'alias__normalized_name'] def get_queryset(self): """ @@ -82,12 +85,11 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet): elif "club" in types: queryset = queryset.filter(polymorphic_ctype__model="noteclub") elif "special" in types: - queryset = queryset.filter( - polymorphic_ctype__model="notespecial") + queryset = queryset.filter(polymorphic_ctype__model="notespecial") else: queryset = queryset.none() - return queryset + return queryset.distinct() class AliasViewSet(viewsets.ModelViewSet): @@ -98,6 +100,9 @@ class AliasViewSet(viewsets.ModelViewSet): """ queryset = Alias.objects.all() serializer_class = AliasSerializer + filter_backends = [SearchFilter, OrderingFilter] + search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] + ordering_fields = ['name', 'normalized_name'] def get_queryset(self): """ diff --git a/apps/note/forms.py b/apps/note/forms.py index 2e8e4456..ac6adaaf 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -6,7 +6,7 @@ from django import forms from django.utils.translation import gettext_lazy as _ from .models import Alias -from .models import Transaction, TransactionTemplate +from .models import TransactionTemplate class AliasForm(forms.ModelForm): @@ -50,52 +50,3 @@ class TransactionTemplateForm(forms.ModelForm): }, ), } - - -class TransactionForm(forms.ModelForm): - def save(self, commit=True): - super().save(commit) - - def clean(self): - """ - If the user has no right to transfer funds, then it will be the source of the transfer by default. - Transactions between a note and the same note are not authorized. - """ - - cleaned_data = super().clean() - if "source" not in cleaned_data: # TODO Replace it with "if %user has no right to transfer funds" - cleaned_data["source"] = self.user.note - - if cleaned_data["source"].pk == cleaned_data["destination"].pk: - self.add_error("destination", _("Source and destination must be different.")) - - return cleaned_data - - class Meta: - model = Transaction - fields = ( - 'source', - 'destination', - 'reason', - 'amount', - ) - - # Voir ci-dessus - widgets = { - 'source': - autocomplete.ModelSelect2( - url='note:note_autocomplete', - attrs={ - 'data-placeholder': 'Note ...', - 'data-minimum-input-length': 1, - }, - ), - 'destination': - autocomplete.ModelSelect2( - url='note:note_autocomplete', - attrs={ - 'data-placeholder': 'Note ...', - 'data-minimum-input-length': 1, - }, - ), - } diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 809e7c44..86c00737 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -7,7 +7,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from polymorphic.models import PolymorphicModel -from .notes import Note, NoteClub +from .notes import Note, NoteClub, NoteSpecial """ Defines transactions @@ -68,6 +68,7 @@ class TransactionTemplate(models.Model): description = models.CharField( verbose_name=_('description'), max_length=255, + blank=True, ) class Meta: @@ -106,7 +107,10 @@ class Transaction(PolymorphicModel): verbose_name=_('quantity'), default=1, ) - amount = models.PositiveIntegerField(verbose_name=_('amount'), ) + amount = models.PositiveIntegerField( + verbose_name=_('amount'), + ) + reason = models.CharField( verbose_name=_('reason'), max_length=255, @@ -132,6 +136,7 @@ class Transaction(PolymorphicModel): if self.source.pk == self.destination.pk: # When source == destination, no money is transfered + super().save(*args, **kwargs) return created = self.pk is None @@ -156,11 +161,14 @@ class Transaction(PolymorphicModel): def total(self): return self.amount * self.quantity + @property + def type(self): + return _('Transfer') + class TemplateTransaction(Transaction): """ Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`. - """ template = models.ForeignKey( @@ -173,6 +181,36 @@ class TemplateTransaction(Transaction): on_delete=models.PROTECT, ) + @property + def type(self): + return _('Template') + + +class SpecialTransaction(Transaction): + """ + Special type of :model:`note.Transaction` associated to transactions with special notes + """ + + last_name = models.CharField( + max_length=255, + verbose_name=_("name"), + ) + + first_name = models.CharField( + max_length=255, + verbose_name=_("first_name"), + ) + + bank = models.CharField( + max_length=255, + verbose_name=_("bank"), + blank=True, + ) + + @property + def type(self): + return _('Credit') if isinstance(self.source, NoteSpecial) else _("Debit") + class MembershipTransaction(Transaction): """ @@ -189,3 +227,7 @@ class MembershipTransaction(Transaction): class Meta: verbose_name = _("membership transaction") verbose_name_plural = _("membership transactions") + + @property + def type(self): + return _('membership transaction') diff --git a/apps/note/tables.py b/apps/note/tables.py index d26ffedc..b9dac051 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -1,9 +1,12 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import html + import django_tables2 as tables from django.db.models import F from django_tables2.utils import A +from django.utils.translation import gettext_lazy as _ from .models.notes import Alias from .models.transactions import Transaction @@ -17,17 +20,25 @@ class HistoryTable(tables.Table): 'table table-condensed table-striped table-hover' } model = Transaction - exclude = ("polymorphic_ctype", ) + exclude = ("id", "polymorphic_ctype", ) template_name = 'django_tables2/bootstrap4.html' - sequence = ('...', 'total', 'valid') + sequence = ('...', 'type', 'total', 'valid', ) + orderable = False + + type = tables.Column() total = tables.Column() # will use Transaction.total() !! + valid = tables.Column(attrs={"td": {"id": lambda record: "validate_" + str(record.id), + "class": lambda record: str(record.valid).lower() + ' validate', + "onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + + str(record.valid).lower() + ')'}}) + def order_total(self, queryset, is_descending): # needed for rendering queryset = queryset.annotate(total=F('amount') * F('quantity')) \ .order_by(('-' if is_descending else '') + 'total') - return (queryset, True) + return queryset, True def render_amount(self, value): return pretty_money(value) @@ -35,6 +46,16 @@ class HistoryTable(tables.Table): def render_total(self, value): return pretty_money(value) + def render_type(self, value): + return _(value) + + # Django-tables escape strings. That's a wrong thing. + def render_reason(self, value): + return html.unescape(value) + + def render_valid(self, value): + return "✔" if value else "✖" + class AliasTable(tables.Table): class Meta: diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index 12530c6e..265870a8 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -11,7 +11,7 @@ def pretty_money(value): abs(value) // 100, ) else: - return "{:s}{:d} € {:02d}".format( + return "{:s}{:d}.{:02d} €".format( "- " if value < 0 else "", abs(value) // 100, abs(value) % 100, diff --git a/apps/note/views.py b/apps/note/views.py index 16e2e39b..31a79be7 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -3,53 +3,43 @@ from dal import autocomplete from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.contenttypes.models import ContentType from django.db.models import Q -from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, ListView, UpdateView from django_tables2 import SingleTableView -from .forms import TransactionForm, TransactionTemplateForm -from .models import Transaction, TransactionTemplate, Alias +from .forms import TransactionTemplateForm +from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial +from .models.transactions import SpecialTransaction from .tables import HistoryTable -class TransactionCreate(LoginRequiredMixin, CreateView): +class TransactionCreate(LoginRequiredMixin, SingleTableView): """ Show transfer page TODO: If user have sufficient rights, they can transfer from an other note """ - model = Transaction - form_class = TransactionForm + queryset = Transaction.objects.order_by("-id").all()[:50] + template_name = "note/transaction_form.html" + + # Transaction history table + table_class = HistoryTable + table_pagination = {"per_page": 50} def get_context_data(self, **kwargs): """ Add some context variables in template such as page title """ context = super().get_context_data(**kwargs) - context['title'] = _('Transfer money from your account ' - 'to one or others') - - context['no_cache'] = True + context['title'] = _('Transfer money') + 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() return context - def get_form(self, form_class=None): - """ - If the user has no right to transfer funds, then it won't have the choice of the source of the transfer. - """ - form = super().get_form(form_class) - - if False: # TODO: fix it with "if %user has no right to transfer funds" - del form.fields['source'] - form.user = self.request.user - - return form - - def get_success_url(self): - return reverse('note:transfer') - class NoteAutocomplete(autocomplete.Select2QuerySetView): """ @@ -127,21 +117,25 @@ class ConsoView(LoginRequiredMixin, SingleTableView): """ Consume """ - model = Transaction + queryset = Transaction.objects.order_by("-id").all()[:50] template_name = "note/conso_form.html" # Transaction history table table_class = HistoryTable - table_pagination = {"per_page": 10} + table_pagination = {"per_page": 50} def get_context_data(self, **kwargs): """ Add some context variables in template such as page title """ context = super().get_context_data(**kwargs) - context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \ - .order_by('category') + from django.db.models import Count + buttons = TransactionTemplate.objects.filter(display=True) \ + .annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name') + context['transaction_templates'] = buttons + context['most_used'] = buttons.order_by('-clicks', 'name')[:10] context['title'] = _("Consumptions") + context['polymorphic_ctype'] = ContentType.objects.get_for_model(TemplateTransaction).pk # select2 compatibility context['no_cache'] = True diff --git a/apps/scripts b/apps/scripts deleted file mode 160000 index 123466cf..00000000 --- a/apps/scripts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 123466cfa914422422cd372197e64adf65ef05f7 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 6c60a9fe..e61efb2a 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-11 11:44+0100\n" +"POT-Creation-Date: 2020-03-16 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -23,9 +23,10 @@ msgid "activity" msgstr "" #: apps/activity/models.py:19 apps/activity/models.py:44 -#: apps/member/models.py:60 apps/member/models.py:111 +#: apps/member/models.py:61 apps/member/models.py:112 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 -#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202 +#: templates/member/profile_detail.html:15 msgid "name" msgstr "" @@ -50,7 +51,7 @@ msgid "description" msgstr "" #: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:62 +#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 msgid "type" msgstr "" @@ -86,7 +87,7 @@ msgstr "" msgid "API" msgstr "" -#: apps/logs/apps.py:10 +#: apps/logs/apps.py:11 msgid "Logs" msgstr "" @@ -166,73 +167,73 @@ msgstr "" msgid "user profile" msgstr "" -#: apps/member/models.py:65 +#: apps/member/models.py:66 msgid "email" msgstr "" -#: apps/member/models.py:70 +#: apps/member/models.py:71 msgid "membership fee" msgstr "" -#: apps/member/models.py:74 +#: apps/member/models.py:75 msgid "membership duration" msgstr "" -#: apps/member/models.py:75 +#: apps/member/models.py:76 msgid "The longest time a membership can last (NULL = infinite)." msgstr "" -#: apps/member/models.py:80 +#: apps/member/models.py:81 msgid "membership start" msgstr "" -#: apps/member/models.py:81 +#: apps/member/models.py:82 msgid "How long after January 1st the members can renew their membership." msgstr "" -#: apps/member/models.py:86 +#: apps/member/models.py:87 msgid "membership end" msgstr "" -#: apps/member/models.py:87 +#: apps/member/models.py:88 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." msgstr "" -#: apps/member/models.py:93 apps/note/models/notes.py:139 +#: apps/member/models.py:94 apps/note/models/notes.py:139 msgid "club" msgstr "" -#: apps/member/models.py:94 +#: apps/member/models.py:95 msgid "clubs" msgstr "" -#: apps/member/models.py:117 +#: apps/member/models.py:118 msgid "role" msgstr "" -#: apps/member/models.py:118 +#: apps/member/models.py:119 msgid "roles" msgstr "" -#: apps/member/models.py:142 +#: apps/member/models.py:143 msgid "membership starts on" msgstr "" -#: apps/member/models.py:145 +#: apps/member/models.py:146 msgid "membership ends on" msgstr "" -#: apps/member/models.py:149 +#: apps/member/models.py:150 msgid "fee" msgstr "" -#: apps/member/models.py:153 +#: apps/member/models.py:154 msgid "membership" msgstr "" -#: apps/member/models.py:154 +#: apps/member/models.py:155 msgid "memberships" msgstr "" @@ -253,12 +254,12 @@ msgstr "" msgid "Alias successfully deleted" msgstr "" -#: apps/note/admin.py:120 apps/note/models/transactions.py:93 +#: apps/note/admin.py:120 apps/note/models/transactions.py:94 msgid "source" msgstr "" #: apps/note/admin.py:128 apps/note/admin.py:156 -#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99 +#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100 msgid "destination" msgstr "" @@ -278,10 +279,6 @@ msgstr "" msgid "Maximal size: 2MB" msgstr "" -#: apps/note/forms.py:70 -msgid "Source and destination must be different." -msgstr "" - #: apps/note/models/notes.py:27 msgid "account balance" msgstr "" @@ -312,7 +309,7 @@ msgstr "" msgid "display image" msgstr "" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:102 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103 msgid "created at" msgstr "" @@ -374,15 +371,15 @@ msgstr "" msgid "aliases" msgstr "" -#: apps/note/models/notes.py:229 +#: apps/note/models/notes.py:233 msgid "Alias is too long." msgstr "" -#: apps/note/models/notes.py:234 +#: apps/note/models/notes.py:238 msgid "An alias with a similar name already exists: {} " msgstr "" -#: apps/note/models/notes.py:243 +#: apps/note/models/notes.py:247 msgid "You can't delete your main alias." msgstr "" @@ -398,7 +395,7 @@ msgstr "" msgid "A template with this name already exist" msgstr "" -#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109 +#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111 msgid "amount" msgstr "" @@ -406,51 +403,81 @@ msgstr "" msgid "in centimes" msgstr "" -#: apps/note/models/transactions.py:74 +#: apps/note/models/transactions.py:75 msgid "transaction template" msgstr "" -#: apps/note/models/transactions.py:75 +#: apps/note/models/transactions.py:76 msgid "transaction templates" msgstr "" -#: apps/note/models/transactions.py:106 +#: apps/note/models/transactions.py:107 msgid "quantity" msgstr "" -#: apps/note/models/transactions.py:111 -msgid "reason" +#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 +msgid "Gift" msgstr "" -#: apps/note/models/transactions.py:115 -msgid "valid" +#: apps/note/models/transactions.py:118 templates/base.html:90 +#: templates/note/transaction_form.html:19 +#: templates/note/transaction_form.html:126 +msgid "Transfer" msgstr "" -#: apps/note/models/transactions.py:120 -msgid "transaction" +#: apps/note/models/transactions.py:119 +msgid "Template" msgstr "" -#: apps/note/models/transactions.py:121 -msgid "transactions" +#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 +msgid "Credit" msgstr "" -#: apps/note/models/transactions.py:185 +#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27 +msgid "Debit" +msgstr "" + +#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:186 +#: apps/note/models/transactions.py:129 +msgid "reason" +msgstr "" + +#: apps/note/models/transactions.py:133 +msgid "valid" +msgstr "" + +#: apps/note/models/transactions.py:138 +msgid "transaction" +msgstr "" + +#: apps/note/models/transactions.py:139 +msgid "transactions" +msgstr "" + +#: apps/note/models/transactions.py:207 +msgid "first_name" +msgstr "" + +#: apps/note/models/transactions.py:212 +msgid "bank" +msgstr "" + +#: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "" -#: apps/note/views.py:29 -msgid "Transfer money from your account to one or others" +#: apps/note/views.py:31 +msgid "Transfer money" msgstr "" -#: apps/note/views.py:139 -msgid "Consommations" +#: apps/note/views.py:132 templates/base.html:78 +msgid "Consumptions" msgstr "" -#: note_kfet/settings/__init__.py:63 +#: note_kfet/settings/__init__.py:61 msgid "" "The Central Authentication Service grants you access to most of our websites " "by authenticating only once, so you don't need to type your credentials " @@ -473,24 +500,16 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "" -#: templates/base.html:70 -msgid "Consumptions" -msgstr "" - -#: templates/base.html:73 +#: templates/base.html:81 msgid "Clubs" msgstr "" -#: templates/base.html:76 +#: templates/base.html:84 msgid "Activities" msgstr "" -#: templates/base.html:79 -msgid "Button" -msgstr "" - -#: templates/base.html:82 templates/note/transaction_form.html:35 -msgid "Transfer" +#: templates/base.html:87 +msgid "Buttons" msgstr "" #: templates/cas_server/base.html:7 @@ -549,6 +568,7 @@ msgid "Field filters" msgstr "" #: templates/django_filters/rest_framework/form.html:5 +#: templates/member/club_form.html:10 msgid "Submit" msgstr "" @@ -572,6 +592,14 @@ msgstr "" msgid "Transaction history" msgstr "" +#: templates/member/club_form.html:6 +msgid "Clubs list" +msgstr "" + +#: templates/member/club_list.html:8 +msgid "New club" +msgstr "" + #: templates/member/manage_auth_tokens.html:16 msgid "Token" msgstr "" @@ -620,8 +648,87 @@ msgstr "" msgid "Save Changes" msgstr "" +#: templates/member/signup.html:5 templates/member/signup.html:8 #: templates/member/signup.html:14 -msgid "Sign Up" +msgid "Sign up" +msgstr "" + +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 +msgid "Select emitters" +msgstr "" + +#: templates/note/conso_form.html:45 +msgid "Select consumptions" +msgstr "" + +#: templates/note/conso_form.html:51 +msgid "Consume!" +msgstr "" + +#: templates/note/conso_form.html:64 +msgid "Most used buttons" +msgstr "" + +#: templates/note/conso_form.html:121 +msgid "Edit" +msgstr "" + +#: templates/note/conso_form.html:126 +msgid "Single consumptions" +msgstr "" + +#: templates/note/conso_form.html:130 +msgid "Double consumptions" +msgstr "" + +#: templates/note/conso_form.html:141 +msgid "Recent transactions history" +msgstr "" + +#: templates/note/transaction_form.html:55 +msgid "External payment" +msgstr "" + +#: templates/note/transaction_form.html:63 +msgid "Transfer type" +msgstr "" + +#: templates/note/transaction_form.html:73 +msgid "Name" +msgstr "" + +#: templates/note/transaction_form.html:79 +msgid "First name" +msgstr "" + +#: templates/note/transaction_form.html:85 +msgid "Bank" +msgstr "" + +#: templates/note/transaction_form.html:97 +#: templates/note/transaction_form.html:179 +#: templates/note/transaction_form.html:186 +msgid "Select receivers" +msgstr "" + +#: templates/note/transaction_form.html:114 +msgid "Amount" +msgstr "" + +#: templates/note/transaction_form.html:119 +msgid "Reason" +msgstr "" + +#: templates/note/transaction_form.html:193 +msgid "Credit note" +msgstr "" + +#: templates/note/transaction_form.html:200 +msgid "Debit note" +msgstr "" + +#: templates/note/transactiontemplate_form.html:6 +msgid "Buttons list" msgstr "" #: templates/registration/logged_out.html:8 diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 05836a54..5e6e9470 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-11 11:44+0100\n" +"POT-Creation-Date: 2020-03-16 11:53+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,9 +18,10 @@ msgid "activity" msgstr "activité" #: apps/activity/models.py:19 apps/activity/models.py:44 -#: apps/member/models.py:60 apps/member/models.py:111 +#: apps/member/models.py:61 apps/member/models.py:112 #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 -#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:15 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:202 +#: templates/member/profile_detail.html:15 msgid "name" msgstr "nom" @@ -45,7 +46,7 @@ msgid "description" msgstr "description" #: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:62 +#: apps/note/models/transactions.py:62 apps/note/models/transactions.py:115 msgid "type" msgstr "type" @@ -81,7 +82,7 @@ msgstr "invités" msgid "API" msgstr "" -#: apps/logs/apps.py:10 +#: apps/logs/apps.py:11 msgid "Logs" msgstr "" @@ -161,37 +162,37 @@ msgstr "payé" msgid "user profile" msgstr "profil utilisateur" -#: apps/member/models.py:65 +#: apps/member/models.py:66 msgid "email" msgstr "courriel" -#: apps/member/models.py:70 +#: apps/member/models.py:71 msgid "membership fee" msgstr "cotisation pour adhérer" -#: apps/member/models.py:74 +#: apps/member/models.py:75 msgid "membership duration" msgstr "durée de l'adhésion" -#: apps/member/models.py:75 +#: apps/member/models.py:76 msgid "The longest time a membership can last (NULL = infinite)." msgstr "La durée maximale d'une adhésion (NULL = infinie)." -#: apps/member/models.py:80 +#: apps/member/models.py:81 msgid "membership start" msgstr "début de l'adhésion" -#: apps/member/models.py:81 +#: apps/member/models.py:82 msgid "How long after January 1st the members can renew their membership." msgstr "" "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur " "adhésion." -#: apps/member/models.py:86 +#: apps/member/models.py:87 msgid "membership end" msgstr "fin de l'adhésion" -#: apps/member/models.py:87 +#: apps/member/models.py:88 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." @@ -199,45 +200,39 @@ msgstr "" "Combien de temps l'adhésion peut durer après le 1er Janvier de l'année " "suivante avant que les adhérents peuvent renouveler leur adhésion." -#: apps/member/models.py:93 apps/note/models/notes.py:139 +#: apps/member/models.py:94 apps/note/models/notes.py:139 msgid "club" msgstr "club" -msgid "New club" -msgstr "Nouveau club" - -msgid "Clubs list" -msgstr "Liste des clubs" - -#: apps/member/models.py:94 +#: apps/member/models.py:95 msgid "clubs" msgstr "clubs" -#: apps/member/models.py:117 +#: apps/member/models.py:118 msgid "role" msgstr "rôle" -#: apps/member/models.py:118 +#: apps/member/models.py:119 msgid "roles" msgstr "rôles" -#: apps/member/models.py:142 +#: apps/member/models.py:143 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:145 +#: apps/member/models.py:146 msgid "membership ends on" msgstr "l'adhésion finie le" -#: apps/member/models.py:149 +#: apps/member/models.py:150 msgid "fee" msgstr "cotisation" -#: apps/member/models.py:153 +#: apps/member/models.py:154 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:154 +#: apps/member/models.py:155 msgid "memberships" msgstr "adhésions" @@ -258,12 +253,12 @@ msgstr "Compte n°%(id)s : %(username)s" msgid "Alias successfully deleted" msgstr "L'alias a bien été supprimé" -#: apps/note/admin.py:120 apps/note/models/transactions.py:93 +#: apps/note/admin.py:120 apps/note/models/transactions.py:94 msgid "source" msgstr "source" #: apps/note/admin.py:128 apps/note/admin.py:156 -#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99 +#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:100 msgid "destination" msgstr "destination" @@ -283,10 +278,6 @@ msgstr "Choisissez une image" msgid "Maximal size: 2MB" msgstr "Taille maximale : 2 Mo" -#: apps/note/forms.py:70 -msgid "Source and destination must be different." -msgstr "La source et la destination doivent être différentes." - #: apps/note/models/notes.py:27 msgid "account balance" msgstr "solde du compte" @@ -318,7 +309,7 @@ msgstr "" msgid "display image" msgstr "image affichée" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:102 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:103 msgid "created at" msgstr "créée le" @@ -380,15 +371,15 @@ msgstr "alias" msgid "aliases" msgstr "alias" -#: apps/note/models/notes.py:229 +#: apps/note/models/notes.py:233 msgid "Alias is too long." msgstr "L'alias est trop long." -#: apps/note/models/notes.py:234 +#: apps/note/models/notes.py:238 msgid "An alias with a similar name already exists: {} " msgstr "Un alias avec un nom similaire existe déjà : {}" -#: apps/note/models/notes.py:243 +#: apps/note/models/notes.py:247 msgid "You can't delete your main alias." msgstr "Vous ne pouvez pas supprimer votre alias principal." @@ -404,7 +395,7 @@ msgstr "catégories de transaction" msgid "A template with this name already exist" msgstr "Un modèle de transaction avec un nom similaire existe déjà." -#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109 +#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:111 msgid "amount" msgstr "montant" @@ -412,47 +403,81 @@ msgstr "montant" msgid "in centimes" msgstr "en centimes" -#: apps/note/models/transactions.py:74 +#: apps/note/models/transactions.py:75 msgid "transaction template" msgstr "modèle de transaction" -#: apps/note/models/transactions.py:75 +#: apps/note/models/transactions.py:76 msgid "transaction templates" msgstr "modèles de transaction" -#: apps/note/models/transactions.py:106 +#: apps/note/models/transactions.py:107 msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:111 -msgid "reason" -msgstr "raison" +#: apps/note/models/transactions.py:117 templates/note/transaction_form.html:15 +msgid "Gift" +msgstr "Don" -#: apps/note/models/transactions.py:115 -msgid "valid" -msgstr "valide" +#: apps/note/models/transactions.py:118 templates/base.html:90 +#: templates/note/transaction_form.html:19 +#: templates/note/transaction_form.html:126 +msgid "Transfer" +msgstr "Virement" -#: apps/note/models/transactions.py:120 -msgid "transaction" -msgstr "transaction" +#: apps/note/models/transactions.py:119 +msgid "Template" +msgstr "Bouton" -#: apps/note/models/transactions.py:121 -msgid "transactions" -msgstr "transactions" +#: apps/note/models/transactions.py:120 templates/note/transaction_form.html:23 +msgid "Credit" +msgstr "Crédit" -#: apps/note/models/transactions.py:185 +#: apps/note/models/transactions.py:121 templates/note/transaction_form.html:27 +msgid "Debit" +msgstr "Retrait" + +#: apps/note/models/transactions.py:122 apps/note/models/transactions.py:230 msgid "membership transaction" msgstr "transaction d'adhésion" -#: apps/note/models/transactions.py:186 +#: apps/note/models/transactions.py:129 +msgid "reason" +msgstr "raison" + +#: apps/note/models/transactions.py:133 +msgid "valid" +msgstr "valide" + +#: apps/note/models/transactions.py:138 +msgid "transaction" +msgstr "transaction" + +#: apps/note/models/transactions.py:139 +msgid "transactions" +msgstr "transactions" + +#: apps/note/models/transactions.py:207 +msgid "first_name" +msgstr "Prénom" + +#: apps/note/models/transactions.py:212 +msgid "bank" +msgstr "Banque" + +#: apps/note/models/transactions.py:231 msgid "membership transactions" msgstr "transactions d'adhésion" -#: apps/note/views.py:29 -msgid "Transfer money from your account to one or others" -msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres" +#: apps/note/views.py:31 +msgid "Transfer money" +msgstr "Transferts d'argent" -#: note_kfet/settings/__init__.py:63 +#: apps/note/views.py:132 templates/base.html:78 +msgid "Consumptions" +msgstr "Consommations" + +#: note_kfet/settings/__init__.py:61 msgid "" "The Central Authentication Service grants you access to most of our websites " "by authenticating only once, so you don't need to type your credentials " @@ -475,29 +500,18 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." -#: templates/base.html:70 -msgid "Consumptions" -msgstr "Consommations" - -#: templates/base.html:73 +#: templates/base.html:81 msgid "Clubs" msgstr "Clubs" -#: templates/base.html:76 +#: templates/base.html:84 msgid "Activities" msgstr "Activités" -#: templates/base.html:79 +#: templates/base.html:87 msgid "Buttons" msgstr "Boutons" -msgid "Buttons list" -msgstr "Liste des boutons" - -#: templates/base.html:82 templates/note/transaction_form.html:35 -msgid "Transfer" -msgstr "Virement" - #: templates/cas_server/base.html:7 msgid "Central Authentication Service" msgstr "" @@ -556,8 +570,9 @@ msgid "Field filters" msgstr "" #: templates/django_filters/rest_framework/form.html:5 +#: templates/member/club_form.html:10 msgid "Submit" -msgstr "" +msgstr "Envoyer" #: templates/member/club_detail.html:10 msgid "Membership starts on" @@ -579,6 +594,14 @@ msgstr "solde du compte" msgid "Transaction history" msgstr "Historique des transactions" +#: templates/member/club_form.html:6 +msgid "Clubs list" +msgstr "Liste des clubs" + +#: templates/member/club_list.html:8 +msgid "New club" +msgstr "Nouveau club" + #: templates/member/manage_auth_tokens.html:16 msgid "Token" msgstr "Jeton" @@ -627,11 +650,89 @@ msgstr "Voir mes adhésions" msgid "Save Changes" msgstr "Sauvegarder les changements" -#: templates/member/signup.html:8 +#: templates/member/signup.html:5 templates/member/signup.html:8 #: templates/member/signup.html:14 msgid "Sign up" msgstr "Inscription" +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:38 +msgid "Select emitters" +msgstr "Sélection des émetteurs" + +#: templates/note/conso_form.html:45 +msgid "Select consumptions" +msgstr "Consommations" + +#: templates/note/conso_form.html:51 +msgid "Consume!" +msgstr "Consommer !" + +#: templates/note/conso_form.html:64 +msgid "Most used buttons" +msgstr "Boutons les plus utilisés" + +#: templates/note/conso_form.html:121 +msgid "Edit" +msgstr "Éditer" + +#: templates/note/conso_form.html:126 +msgid "Single consumptions" +msgstr "Consos simples" + +#: templates/note/conso_form.html:130 +msgid "Double consumptions" +msgstr "Consos doubles" + +#: templates/note/conso_form.html:141 +msgid "Recent transactions history" +msgstr "Historique des transactions récentes" + +#: templates/note/transaction_form.html:55 +msgid "External payment" +msgstr "Paiement extérieur" + +#: templates/note/transaction_form.html:63 +msgid "Transfer type" +msgstr "Type de transfert" + +#: templates/note/transaction_form.html:73 +msgid "Name" +msgstr "Nom" + +#: templates/note/transaction_form.html:79 +msgid "First name" +msgstr "Prénom" + +#: templates/note/transaction_form.html:85 +msgid "Bank" +msgstr "Banque" + +#: templates/note/transaction_form.html:97 +#: templates/note/transaction_form.html:179 +#: templates/note/transaction_form.html:186 +msgid "Select receivers" +msgstr "Sélection des destinataires" + +#: templates/note/transaction_form.html:114 +msgid "Amount" +msgstr "Montant" + +#: templates/note/transaction_form.html:119 +msgid "Reason" +msgstr "Raison" + +#: templates/note/transaction_form.html:193 +msgid "Credit note" +msgstr "Note à créditer" + +#: templates/note/transaction_form.html:200 +msgid "Debit note" +msgstr "Note à débiter" + +#: templates/note/transactiontemplate_form.html:6 +msgid "Buttons list" +msgstr "Liste des boutons" + #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "" diff --git a/static/js/base.js b/static/js/base.js new file mode 100644 index 00000000..2362375b --- /dev/null +++ b/static/js/base.js @@ -0,0 +1,281 @@ +// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +// SPDX-License-Identifier: GPL-3.0-or-later + + +/** + * Convert balance in cents to a human readable amount + * @param value the balance, in cents + * @returns {string} + */ +function pretty_money(value) { + if (value % 100 === 0) + return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + " €"; + else + return (value < 0 ? "- " : "") + Math.floor(Math.abs(value) / 100) + "." + + (Math.abs(value) % 100 < 10 ? "0" : "") + (Math.abs(value) % 100) + " €"; +} + +/** + * Add a message on the top of the page. + * @param msg The message to display + * @param alert_type The type of the alert. Choices: info, success, warning, danger + */ +function addMsg(msg, alert_type) { + let msgDiv = $("#messages"); + let html = msgDiv.html(); + html += "
" + + "" + + msg + "
\n"; + msgDiv.html(html); +} + +/** + * Reload the balance of the user on the right top corner + */ +function refreshBalance() { + $("#user_balance").load("/ #user_balance"); +} + +/** + * Query the 20 first matched notes with a given pattern + * @param pattern The pattern that is queried + * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. + */ +function getMatchedNotes(pattern, fun) { + $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun); +} + +/** + * Generate a
  • entry with a given id and text + */ +function li(id, text) { + return "
  • " + text + "
  • \n"; +} + +/** + * Render note name and picture + * @param note The note to render + * @param alias The alias to be displayed + * @param user_note_field + * @param profile_pic_field + */ +function displayNote(note, alias, user_note_field=null, profile_pic_field=null) { + let img = note == null ? null : note.display_image; + if (img == null) + img = '/media/pic/default.png'; + if (note !== null && alias !== note.name) + alias += " (aka. " + note.name + ")"; + if (note !== null && user_note_field !== null) + $("#" + user_note_field).text(alias + " : " + pretty_money(note.balance)); + if (profile_pic_field != null) + $("#" + profile_pic_field).attr('src', img); +} + +/** + * Remove a note from the emitters. + * @param d The note to remove + * @param note_prefix The prefix of the identifiers of the
  • blocks of the emitters + * @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity] + * @param note_list_id The div block identifier where the notes of the buyers are displayed + * @param user_note_field The identifier of the field that display the note of the hovered note (useful in + * consumptions, put null if not used) + * @param profile_pic_field The identifier of the field that display the profile picture of the hovered note + * (useful in consumptions, put null if not used) + * @returns an anonymous function to be compatible with jQuery events + */ +function removeNote(d, note_prefix="note", notes_display, note_list_id, user_note_field=null, profile_pic_field=null) { + return (function() { + let new_notes_display = []; + let html = ""; + notes_display.forEach(function (disp) { + if (disp.quantity > 1 || disp.id !== d.id) { + disp.quantity -= disp.id === d.id ? 1 : 0; + new_notes_display.push(disp); + html += li(note_prefix + "_" + disp.id, disp.name + + "" + disp.quantity + ""); + } + }); + + notes_display.length = 0; + new_notes_display.forEach(function(disp) { + notes_display.push(disp); + }); + + $("#" + note_list_id).html(html); + notes_display.forEach(function (disp) { + let obj = $("#" + note_prefix + "_" + disp.id); + obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field)); + obj.hover(function() { + if (disp.note) + displayNote(disp.note, disp.name, user_note_field, profile_pic_field); + }); + }); + }); +} + +/** + * Generate an auto-complete field to query a note with its alias + * @param field_id The identifier of the text field where the alias is typed + * @param alias_matched_id The div block identifier where the matched aliases are displayed + * @param note_list_id The div block identifier where the notes of the buyers are displayed + * @param notes An array containing the note objects of the buyers + * @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity] + * @param alias_prefix The prefix of the
  • blocks for the matched aliases + * @param note_prefix The prefix of the
  • blocks for the notes of the buyers + * @param user_note_field The identifier of the field that display the note of the hovered note (useful in + * consumptions, put null if not used) + * @param profile_pic_field The identifier of the field that display the profile picture of the hovered note + * (useful in consumptions, put null if not used) + * @param alias_click Function that is called when an alias is clicked. If this method exists and doesn't return true, + * the associated note is not displayed. + * Useful for a consumption if the item is selected before. + */ +function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes_display, alias_prefix="alias", + note_prefix="note", user_note_field=null, profile_pic_field=null, alias_click=null) { + let field = $("#" + field_id); + // When the user clicks on the search field, it is immediately cleared + field.click(function() { + field.val(""); + }); + + let old_pattern = null; + + // When the user type "Enter", the first alias is clicked + field.keypress(function(event) { + if (event.originalEvent.charCode === 13) + $("#" + alias_matched_id + " li").first().trigger("click"); + }); + + // When the user type something, the matched aliases are refreshed + field.keyup(function(e) { + if (e.originalEvent.charCode === 13) + return; + + let pattern = field.val(); + // If the pattern is not modified, we don't query the API + if (pattern === old_pattern || pattern === "") + return; + + old_pattern = pattern; + + // Clear old matched notes + notes.length = 0; + + let aliases_matched_obj = $("#" + alias_matched_id); + let aliases_matched_html = ""; + + // Get matched notes with the given pattern + getMatchedNotes(pattern, function(aliases) { + // The response arrived too late, we stop the request + if (pattern !== $("#" + field_id).val()) + return; + + aliases.results.forEach(function (alias) { + let note = alias.note; + aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name); + note.alias = alias; + notes.push(note); + }); + + // Display the list of matched aliases + aliases_matched_obj.html(aliases_matched_html); + + notes.forEach(function (note) { + let alias = note.alias; + let alias_obj = $("#" + alias_prefix + "_" + alias.id); + // When an alias is hovered, the profile picture and the balance are displayed at the right place + alias_obj.hover(function () { + displayNote(note, alias.name, user_note_field, profile_pic_field); + }); + + // When the user click on an alias, the associated note is added to the emitters + alias_obj.click(function () { + field.val(""); + // If the note is already an emitter, we increase the quantity + var disp = null; + notes_display.forEach(function (d) { + // We compare the note ids + if (d.id === note.id) { + d.quantity += 1; + disp = d; + } + }); + // In the other case, we add a new emitter + if (disp == null) { + disp = { + name: alias.name, + id: note.id, + note: note, + quantity: 1 + }; + notes_display.push(disp); + } + + // If the function alias_click exists, it is called. If it doesn't return true, then the notes are + // note displayed. Useful for a consumption when a button is already clicked + if (alias_click && !alias_click()) + return; + + let note_list = $("#" + note_list_id); + let html = ""; + notes_display.forEach(function (disp) { + html += li(note_prefix + "_" + disp.id, disp.name + + "" + disp.quantity + ""); + }); + + // Emitters are displayed + note_list.html(html); + + notes_display.forEach(function (disp) { + let line_obj = $("#" + note_prefix + "_" + disp.id); + // Hover an emitter display also the profile picture + line_obj.hover(function () { + displayNote(disp.note, disp.name, user_note_field, profile_pic_field); + }); + + // When an emitter is clicked, it is removed + line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, + profile_pic_field)); + }); + }); + }); + }); + }); +} + +// When a validate button is clicked, we switch the validation status +function de_validate(id, validated) { + $("#validate_" + id).html("⟳ ..."); + + // Perform a PATCH request to the API in order to update the transaction + // If the user has insuffisent rights, an error message will appear + $.ajax({ + "url": "/api/note/transaction/transaction/" + id + "/", + type: "PATCH", + dataType: "json", + headers: { + "X-CSRFTOKEN": CSRF_TOKEN + }, + data: { + "resourcetype": "TemplateTransaction", + valid: !validated + }, + success: function () { + // Refresh jQuery objects + $(".validate").click(de_validate); + + refreshBalance(); + // error if this method doesn't exist. Please define it. + refreshHistory(); + }, + error: function(err) { + addMsg("Une erreur est survenue lors de la validation/dévalidation " + + "de cette transaction : " + err.responseText, "danger"); + + refreshBalance(); + // error if this method doesn't exist. Please define it. + refreshHistory(); + } + }); +} diff --git a/static/js/consos.js b/static/js/consos.js new file mode 100644 index 00000000..5f7a314a --- /dev/null +++ b/static/js/consos.js @@ -0,0 +1,205 @@ +// Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +// SPDX-License-Identifier: GPL-3.0-or-later + +/** + * Refresh the history table on the consumptions page. + */ +function refreshHistory() { + $("#history").load("/note/consos/ #history"); + $("#most_used").load("/note/consos/ #most_used"); +} + +$(document).ready(function() { + // If hash of a category in the URL, then select this category + // else select the first one + if (location.hash) { + $("a[href='" + location.hash + "']").tab("show"); + } else { + $("a[data-toggle='tab']").first().tab("show"); + } + + // When selecting a category, change URL + $(document.body).on("click", "a[data-toggle='tab']", function() { + location.hash = this.getAttribute("href"); + }); + + // Switching in double consumptions mode should update the layout + let double_conso_obj = $("#double_conso"); + double_conso_obj.click(function() { + $("#consos_list_div").show(); + $("#infos_div").attr('class', 'col-sm-5 col-xl-6'); + $("#note_infos_div").attr('class', 'col-xl-3'); + $("#user_select_div").attr('class', 'col-xl-4'); + $("#buttons_div").attr('class', 'col-sm-7 col-xl-6'); + + let note_list_obj = $("#note_list"); + if (buttons.length > 0 && note_list_obj.text().length > 0) { + $("#consos_list").html(note_list_obj.html()); + note_list_obj.html(""); + + buttons.forEach(function(button) { + $("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, + "consos_list")); + }); + } + }); + + let single_conso_obj = $("#single_conso"); + single_conso_obj.click(function() { + $("#consos_list_div").hide(); + $("#infos_div").attr('class', 'col-sm-5 col-md-4'); + $("#note_infos_div").attr('class', 'col-xl-5'); + $("#user_select_div").attr('class', 'col-xl-7'); + $("#buttons_div").attr('class', 'col-sm-7 col-md-8'); + + let consos_list_obj = $("#consos_list"); + if (buttons.length > 0) { + if (notes_display.length === 0 && consos_list_obj.text().length > 0) { + $("#note_list").html(consos_list_obj.html()); + consos_list_obj.html(""); + buttons.forEach(function(button) { + $("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, + "note_list")); + }); + } + else { + buttons.length = 0; + consos_list_obj.html(""); + } + } + }); + + // Ensure we begin in single consumption. Removing these lines may cause problems when reloading. + single_conso_obj.prop('checked', 'true'); + double_conso_obj.removeAttr('checked'); + $("label[for='double_conso']").attr('class', 'btn btn-sm btn-outline-primary'); + + $("#consos_list_div").hide(); + + $("#consume_all").click(consumeAll); +}); + +notes = []; +notes_display = []; +buttons = []; + +// When the user searches an alias, we update the auto-completion +autoCompleteNote("note", "alias_matched", "note_list", notes, notes_display, + "alias", "note", "user_note", "profile_pic", function() { + if (buttons.length > 0 && $("#single_conso").is(":checked")) { + consumeAll(); + return false; + } + return true; + }); + +/** + * Add a transaction from a button. + * @param dest Where the money goes + * @param amount The price of the item + * @param type The type of the transaction (content type id for TemplateTransaction) + * @param category_id The category identifier + * @param category_name The category name + * @param template_id The identifier of the button + * @param template_name The name of the button + */ +function addConso(dest, amount, type, category_id, category_name, template_id, template_name) { + var button = null; + buttons.forEach(function(b) { + if (b.id === template_id) { + b.quantity += 1; + button = b; + } + }); + if (button == null) { + button = { + id: template_id, + name: template_name, + dest: dest, + quantity: 1, + amount: amount, + type: type, + category_id: category_id, + category_name: category_name + }; + buttons.push(button); + } + + let dc_obj = $("#double_conso"); + if (dc_obj.is(":checked") || notes_display.length === 0) { + let list = dc_obj.is(":checked") ? "consos_list" : "note_list"; + let html = ""; + buttons.forEach(function(button) { + html += li("conso_button_" + button.id, button.name + + "" + button.quantity + ""); + }); + + $("#" + list).html(html); + + buttons.forEach(function(button) { + $("#conso_button_" + button.id).click(removeNote(button, "conso_button", buttons, list)); + }); + } + else + consumeAll(); +} + +/** + * Reset the page as its initial state. + */ +function reset() { + notes_display.length = 0; + notes.length = 0; + buttons.length = 0; + $("#note_list").html(""); + $("#alias_matched").html(""); + $("#consos_list").html(""); + displayNote(null, ""); + refreshHistory(); + refreshBalance(); +} + + +/** + * Apply all transactions: all notes in `notes` buy each item in `buttons` + */ +function consumeAll() { + notes_display.forEach(function(note_display) { + buttons.forEach(function(button) { + consume(note_display.id, button.dest, button.quantity * note_display.quantity, button.amount, + button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id); + }); + }); +} + +/** + * Create a new transaction from a button through the API. + * @param source The note that paid the item (type: int) + * @param dest The note that sold the item (type: int) + * @param quantity The quantity sold (type: int) + * @param amount The price of one item, in cents (type: int) + * @param reason The transaction details (type: str) + * @param type The type of the transaction (content type id for TemplateTransaction) + * @param category The category id of the button (type: int) + * @param template The button id (type: int) + */ +function consume(source, dest, quantity, amount, reason, type, category, template) { + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": quantity, + "amount": amount, + "reason": reason, + "valid": true, + "polymorphic_ctype": type, + "resourcetype": "TemplateTransaction", + "source": source, + "destination": dest, + "category": category, + "template": template + }, reset).fail(function (e) { + reset(); + + addMsg("Une erreur est survenue lors de la transaction : " + e.responseText, "danger"); + }); +} diff --git a/static/js/transfer.js b/static/js/transfer.js new file mode 100644 index 00000000..a0c2d88a --- /dev/null +++ b/static/js/transfer.js @@ -0,0 +1,157 @@ +sources = []; +sources_notes_display = []; +dests = []; +dests_notes_display = []; + +function refreshHistory() { + $("#history").load("/note/transfer/ #history"); +} + +function reset() { + sources_notes_display.length = 0; + sources.length = 0; + dests_notes_display.length = 0; + dests.length = 0; + $("#source_note_list").html(""); + $("#dest_note_list").html(""); + $("#source_alias_matched").html(""); + $("#dest_alias_matched").html(""); + $("#amount").val(""); + $("#reason").val(""); + $("#last_name").val(""); + $("#first_name").val(""); + $("#bank").val(""); + refreshBalance(); + refreshHistory(); +} + +$(document).ready(function() { + autoCompleteNote("source_note", "source_alias_matched", "source_note_list", sources, sources_notes_display, + "source_alias", "source_note", "user_note", "profile_pic"); + autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display, + "dest_alias", "dest_note", "user_note", "profile_pic", function() { + let last = dests_notes_display[dests_notes_display.length - 1]; + dests_notes_display.length = 0; + dests_notes_display.push(last); + + last.quantity = 1; + + $.getJSON("/api/user/" + last.note.user + "/", function(user) { + $("#last_name").val(user.last_name); + $("#first_name").val(user.first_name); + }); + + return true; + }); + + + // Ensure we begin in gift mode. Removing these lines may cause problems when reloading. + $("#type_gift").prop('checked', 'true'); + $("#type_transfer").removeAttr('checked'); + $("#type_credit").removeAttr('checked'); + $("#type_debit").removeAttr('checked'); + $("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary'); + $("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary'); + $("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary'); +}); + +$("#transfer").click(function() { + if ($("#type_gift").is(':checked')) { + dests_notes_display.forEach(function (dest) { + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": dest.quantity, + "amount": 100 * $("#amount").val(), + "reason": $("#reason").val(), + "valid": true, + "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, + "resourcetype": "Transaction", + "source": user_id, + "destination": dest.id + }, function () { + addMsg("Le transfert de " + + pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note " + + " vers la note " + dest.name + " a été fait avec succès !", "success"); + + reset(); + }).fail(function (err) { + addMsg("Le transfert de " + + pretty_money(dest.quantity * 100 * $("#amount").val()) + " de votre note " + + " vers la note " + dest.name + " a échoué : " + err.responseText, "danger"); + + reset(); + }); + }); + } + else if ($("#type_transfer").is(':checked')) { + sources_notes_display.forEach(function (source) { + dests_notes_display.forEach(function (dest) { + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": source.quantity * dest.quantity, + "amount": 100 * $("#amount").val(), + "reason": $("#reason").val(), + "valid": true, + "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, + "resourcetype": "Transaction", + "source": source.id, + "destination": dest.id + }, function () { + addMsg("Le transfert de " + + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name + + " vers la note " + dest.name + " a été fait avec succès !", "success"); + + reset(); + }).fail(function (err) { + addMsg("Le transfert de " + + pretty_money(source.quantity * dest.quantity * 100 * $("#amount").val()) + " de la note " + source.name + + " vers la note " + dest.name + " a échoué : " + err.responseText, "danger"); + + reset(); + }); + }); + }); + } else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) { + let special_note = $("#credit_type").val(); + let user_note = dests_notes_display[0].id; + let given_reason = $("#reason").val(); + let source, dest, reason; + if ($("#type_credit").is(':checked')) { + source = special_note; + dest = user_note; + reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase(); + if (given_reason.length > 0) + reason += " (" + given_reason + ")"; + } + else { + source = user_note; + dest = special_note; + reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase(); + if (given_reason.length > 0) + reason += " (" + given_reason + ")"; + } + $.post("/api/note/transaction/transaction/", + { + "csrfmiddlewaretoken": CSRF_TOKEN, + "quantity": 1, + "amount": 100 * $("#amount").val(), + "reason": reason, + "valid": true, + "polymorphic_ctype": SPECIAL_TRANSFER_POLYMORPHIC_CTYPE, + "resourcetype": "SpecialTransaction", + "source": source, + "destination": dest, + "last_name": $("#last_name").val(), + "first_name": $("#first_name").val(), + "bank": $("#bank").val() + }, function () { + addMsg("Le crédit/retrait a bien été effectué !", "success"); + reset(); + }).fail(function (err) { + addMsg("Le crédit/transfert a échoué : " + err.responseText, "danger"); + reset(); + }); + } +}); \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index d57dab89..e6193702 100644 --- a/templates/base.html +++ b/templates/base.html @@ -46,12 +46,20 @@ SPDX-License-Identifier: GPL-3.0-or-later crossorigin="anonymous"> + {# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #} {% if form.media %} {{ form.media }} {% endif %} + + {% block extracss %}{% endblock %} @@ -86,7 +94,8 @@ SPDX-License-Identifier: GPL-3.0-or-later {% if user.is_authenticated %}