diff --git a/apps/activity/api/serializers.py b/apps/activity/api/serializers.py index 514515ef..2f257de0 100644 --- a/apps/activity/api/serializers.py +++ b/apps/activity/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers -from ..models import ActivityType, Activity, Guest +from ..models import ActivityType, Activity, Guest, Entry, GuestTransaction class ActivityTypeSerializer(serializers.ModelSerializer): @@ -37,3 +37,25 @@ class GuestSerializer(serializers.ModelSerializer): class Meta: model = Guest fields = '__all__' + + +class EntrySerializer(serializers.ModelSerializer): + """ + REST API Serializer for Entries. + The djangorestframework plugin will analyse the model `Entry` and parse all fields in the API. + """ + + class Meta: + model = Entry + fields = '__all__' + + +class GuestTransactionSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Special transactions. + The djangorestframework plugin will analyse the model `GuestTransaction` and parse all fields in the API. + """ + + class Meta: + model = GuestTransaction + fields = '__all__' diff --git a/apps/activity/api/urls.py b/apps/activity/api/urls.py index 79e0ba30..3a2495fb 100644 --- a/apps/activity/api/urls.py +++ b/apps/activity/api/urls.py @@ -1,7 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet +from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet, EntryViewSet def register_activity_urls(router, path): @@ -11,3 +11,4 @@ def register_activity_urls(router, path): router.register(path + '/activity', ActivityViewSet) router.register(path + '/type', ActivityTypeViewSet) router.register(path + '/guest', GuestViewSet) + router.register(path + '/entry', EntryViewSet) diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py index 76b2b333..764f2ac3 100644 --- a/apps/activity/api/views.py +++ b/apps/activity/api/views.py @@ -5,8 +5,8 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter from api.viewsets import ReadProtectedModelViewSet -from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer -from ..models import ActivityType, Activity, Guest +from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer, EntrySerializer +from ..models import ActivityType, Activity, Guest, Entry class ActivityTypeViewSet(ReadProtectedModelViewSet): @@ -42,4 +42,16 @@ class GuestViewSet(ReadProtectedModelViewSet): queryset = Guest.objects.all() serializer_class = GuestSerializer filter_backends = [SearchFilter] - search_fields = ['$name', ] + search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] + + +class EntryViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer, + then render it on /api/activity/entry/ + """ + queryset = Entry.objects.all() + serializer_class = EntrySerializer + filter_backends = [SearchFilter] + search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] diff --git a/apps/activity/forms.py b/apps/activity/forms.py new file mode 100644 index 00000000..dcbd3c9d --- /dev/null +++ b/apps/activity/forms.py @@ -0,0 +1,84 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later +from datetime import timedelta, datetime + +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext as _ +from member.models import Club +from note.models import NoteUser, Note +from note_kfet.inputs import DateTimePickerInput, Autocomplete + +from .models import Activity, Guest + + +class ActivityForm(forms.ModelForm): + class Meta: + model = Activity + exclude = ('creater', 'valid', 'open', ) + widgets = { + "organizer": Autocomplete( + model=Club, + attrs={"api_url": "/api/members/club/"}, + ), + "note": Autocomplete( + model=Note, + attrs={ + "api_url": "/api/note/note/", + 'placeholder': 'Note de l\'événement sur laquelle envoyer les crédits d\'invitation ...' + }, + ), + "attendees_club": Autocomplete( + model=Club, + attrs={"api_url": "/api/members/club/"}, + ), + "date_start": DateTimePickerInput(), + "date_end": DateTimePickerInput(), + } + + +class GuestForm(forms.ModelForm): + def clean(self): + cleaned_data = super().clean() + + if self.activity.date_start > datetime.now(): + self.add_error("inviter", _("You can't invite someone once the activity is started.")) + + if not self.activity.valid: + self.add_error("inviter", _("This activity is not validated yet.")) + + one_year = timedelta(days=365) + + qs = Guest.objects.filter( + first_name=cleaned_data["first_name"], + last_name=cleaned_data["last_name"], + activity__date_start__gte=self.activity.date_start - one_year, + ) + if len(qs) >= 5: + self.add_error("last_name", _("This person has been already invited 5 times this year.")) + + qs = qs.filter(activity=self.activity) + if qs.exists(): + self.add_error("last_name", _("This person is already invited.")) + + qs = Guest.objects.filter(inviter=cleaned_data["inviter"], activity=self.activity) + if len(qs) >= 3: + self.add_error("inviter", _("You can't invite more than 3 people to this activity.")) + + return cleaned_data + + class Meta: + model = Guest + fields = ('last_name', 'first_name', 'inviter', ) + widgets = { + "inviter": Autocomplete( + NoteUser, + attrs={ + '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(NoteUser).pk), + 'placeholder': 'Note ...', + }, + ), + } diff --git a/apps/activity/models.py b/apps/activity/models.py index 8f23060c..e3ff0c2f 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -1,9 +1,13 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from datetime import timedelta, datetime -from django.conf import settings +from django.contrib.auth.models import User from django.db import models +from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from rest_framework.exceptions import ValidationError +from note.models import NoteUser, Transaction class ActivityType(models.Model): @@ -44,39 +48,131 @@ class Activity(models.Model): verbose_name=_('name'), max_length=255, ) + description = models.TextField( verbose_name=_('description'), ) + activity_type = models.ForeignKey( ActivityType, on_delete=models.PROTECT, related_name='+', verbose_name=_('type'), ) + + creater = models.ForeignKey( + User, + on_delete=models.PROTECT, + verbose_name=_("user"), + ) + organizer = models.ForeignKey( 'member.Club', on_delete=models.PROTECT, related_name='+', verbose_name=_('organizer'), ) + + note = models.ForeignKey( + 'note.Note', + on_delete=models.PROTECT, + blank=True, + null=True, + related_name='+', + verbose_name=_('note'), + ) + attendees_club = models.ForeignKey( 'member.Club', on_delete=models.PROTECT, related_name='+', verbose_name=_('attendees club'), ) + date_start = models.DateTimeField( verbose_name=_('start date'), ) + date_end = models.DateTimeField( verbose_name=_('end date'), ) + valid = models.BooleanField( + default=False, + verbose_name=_('valid'), + ) + + open = models.BooleanField( + default=False, + verbose_name=_('open'), + ) + class Meta: verbose_name = _("activity") verbose_name_plural = _("activities") +class Entry(models.Model): + activity = models.ForeignKey( + Activity, + on_delete=models.PROTECT, + related_name="entries", + verbose_name=_("activity"), + ) + + time = models.DateTimeField( + auto_now_add=True, + verbose_name=_("entry time"), + ) + + note = models.ForeignKey( + NoteUser, + on_delete=models.PROTECT, + verbose_name=_("note"), + ) + + guest = models.OneToOneField( + 'activity.Guest', + on_delete=models.PROTECT, + null=True, + ) + + class Meta: + unique_together = (('activity', 'note', 'guest', ), ) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + + qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) + if qs.exists(): + raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, )) + + if self.guest: + self.note = self.guest.inviter + + insert = not self.pk + if insert: + if self.note.balance < 0: + raise ValidationError(_("The balance is negative.")) + + ret = super().save(force_insert, force_update, using, update_fields) + + if insert and self.guest: + GuestTransaction.objects.create( + source=self.note, + source_alias=self.note.user.username, + destination=self.note, + destination_alias=self.activity.organizer.name, + quantity=1, + amount=self.activity.activity_type.guest_entry_fee, + reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name, + valid=True, + guest=self.guest, + ).save() + + return ret + + class Guest(models.Model): """ People who are not current members of any clubs, and are invited by someone who is a current member. @@ -86,24 +182,73 @@ class Guest(models.Model): on_delete=models.PROTECT, related_name='+', ) - name = models.CharField( + + last_name = models.CharField( max_length=255, + verbose_name=_("last name"), ) + + first_name = models.CharField( + max_length=255, + verbose_name=_("first name"), + ) + inviter = models.ForeignKey( - settings.AUTH_USER_MODEL, + NoteUser, on_delete=models.PROTECT, - related_name='+', - ) - entry = models.DateTimeField( - null=True, - ) - entry_transaction = models.ForeignKey( - 'note.Transaction', - on_delete=models.PROTECT, - blank=True, - null=True, + related_name='guests', + verbose_name=_("inviter"), ) + @property + def has_entry(self): + try: + if self.entry: + return True + return False + except AttributeError: + return False + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + one_year = timedelta(days=365) + + if not force_insert: + if self.activity.date_start > datetime.now(): + raise ValidationError(_("You can't invite someone once the activity is started.")) + + if not self.activity.valid: + raise ValidationError(_("This activity is not validated yet.")) + + qs = Guest.objects.filter( + first_name=self.first_name, + last_name=self.last_name, + activity__date_start__gte=self.activity.date_start - one_year, + ) + if len(qs) >= 5: + raise ValidationError(_("This person has been already invited 5 times this year.")) + + qs = qs.filter(activity=self.activity) + if qs.exists(): + raise ValidationError(_("This person is already invited.")) + + qs = Guest.objects.filter(inviter=self.inviter, activity=self.activity) + if len(qs) >= 3: + raise ValidationError(_("You can't invite more than 3 people to this activity.")) + + return super().save(force_insert, force_update, using, update_fields) + class Meta: verbose_name = _("guest") verbose_name_plural = _("guests") + unique_together = ("activity", "last_name", "first_name", ) + + +class GuestTransaction(Transaction): + guest = models.OneToOneField( + Guest, + on_delete=models.PROTECT, + ) + + @property + def type(self): + return _('Invitation') diff --git a/apps/activity/tables.py b/apps/activity/tables.py new file mode 100644 index 00000000..d6e566d3 --- /dev/null +++ b/apps/activity/tables.py @@ -0,0 +1,108 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ +import django_tables2 as tables +from django_tables2 import A +from note.templatetags.pretty_money import pretty_money + +from .models import Activity, Guest, Entry + + +class ActivityTable(tables.Table): + name = tables.LinkColumn( + 'activity:activity_detail', + args=[A('pk'), ], + ) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Activity + template_name = 'django_tables2/bootstrap4.html' + fields = ('name', 'activity_type', 'organizer', 'attendees_club', 'date_start', 'date_end', ) + + +class GuestTable(tables.Table): + inviter = tables.LinkColumn( + 'member:user_detail', + args=[A('inviter.user.pk'), ], + ) + + entry = tables.Column( + empty_values=(), + attrs={ + "td": { + "class": lambda record: "" if record.has_entry else "validate btn btn-danger", + "onclick": lambda record: "" if record.has_entry else "remove_guest(" + str(record.pk) + ")" + } + } + ) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Guest + template_name = 'django_tables2/bootstrap4.html' + fields = ("last_name", "first_name", "inviter", ) + + def render_entry(self, record): + if record.has_entry: + return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, ))) + return _("remove").capitalize() + + +def get_row_class(record): + c = "table-row" + if isinstance(record, Guest): + if record.has_entry: + c += " table-success" + else: + c += " table-warning" + else: + qs = Entry.objects.filter(note=record.note, activity=record.activity, guest=None) + if qs.exists(): + c += " table-success" + elif record.note.balance < 0: + c += " table-danger" + return c + + +class EntryTable(tables.Table): + type = tables.Column(verbose_name=_("Type")) + + last_name = tables.Column(verbose_name=_("Last name")) + + first_name = tables.Column(verbose_name=_("First name")) + + note_name = tables.Column(verbose_name=_("Note")) + + balance = tables.Column(verbose_name=_("Balance")) + + def render_note_name(self, value, record): + if hasattr(record, 'username'): + username = record.username + if username != value: + return format_html(value + " aka. " + username) + return value + + def render_balance(self, value): + return pretty_money(value) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + template_name = 'django_tables2/bootstrap4.html' + row_attrs = { + 'class': lambda record: get_row_class(record), + 'id': lambda record: "row-" + ("guest-" if isinstance(record, Guest) else "membership-") + str(record.pk), + 'data-type': lambda record: "guest" if isinstance(record, Guest) else "membership", + 'data-id': lambda record: record.pk if isinstance(record, Guest) else record.note.pk, + 'data-inviter': lambda record: record.inviter.pk if isinstance(record, Guest) else "", + 'data-last-name': lambda record: record.last_name, + 'data-first-name': lambda record: record.first_name, + } diff --git a/apps/activity/urls.py b/apps/activity/urls.py new file mode 100644 index 00000000..f074e8f7 --- /dev/null +++ b/apps/activity/urls.py @@ -0,0 +1,17 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from . import views + +app_name = 'activity' + +urlpatterns = [ + path('', views.ActivityListView.as_view(), name='activity_list'), + path('/', views.ActivityDetailView.as_view(), name='activity_detail'), + path('/invite/', views.ActivityInviteView.as_view(), name='activity_invite'), + path('/entry/', views.ActivityEntryView.as_view(), name='activity_entry'), + path('/update/', views.ActivityUpdateView.as_view(), name='activity_update'), + path('new/', views.ActivityCreateView.as_view(), name='activity_create'), +] diff --git a/apps/activity/views.py b/apps/activity/views.py new file mode 100644 index 00000000..feb7591d --- /dev/null +++ b/apps/activity/views.py @@ -0,0 +1,153 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later +from datetime import datetime, timezone + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.contenttypes.models import ContentType +from django.db.models import F, Q +from django.urls import reverse_lazy +from django.views.generic import CreateView, DetailView, UpdateView, TemplateView +from django.utils.translation import gettext_lazy as _ +from django_tables2.views import SingleTableView +from note.models import NoteUser, Alias, NoteSpecial +from permission.backends import PermissionBackend + +from .forms import ActivityForm, GuestForm +from .models import Activity, Guest, Entry +from .tables import ActivityTable, GuestTable, EntryTable + + +class ActivityCreateView(LoginRequiredMixin, CreateView): + model = Activity + form_class = ActivityForm + + def form_valid(self, form): + form.instance.creater = self.request.user + return super().form_valid(form) + + def get_success_url(self, **kwargs): + self.object.refresh_from_db() + return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk}) + + +class ActivityListView(LoginRequiredMixin, SingleTableView): + model = Activity + table_class = ActivityTable + + def get_queryset(self): + return super().get_queryset()\ + .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).reverse() + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx['title'] = _("Activities") + + upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now()) + ctx['upcoming'] = ActivityTable(data=upcoming_activities + .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))) + + return ctx + + +class ActivityDetailView(LoginRequiredMixin, DetailView): + model = Activity + context_object_name = "activity" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data() + + table = GuestTable(data=Guest.objects.filter(activity=self.object) + .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))) + ctx["guests"] = table + + ctx["activity_started"] = datetime.now(timezone.utc) > self.object.date_start + + return ctx + + +class ActivityUpdateView(LoginRequiredMixin, UpdateView): + model = Activity + form_class = ActivityForm + + def get_success_url(self, **kwargs): + return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) + + +class ActivityInviteView(LoginRequiredMixin, CreateView): + model = Guest + form_class = GuestForm + template_name = "activity/activity_invite.html" + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.activity = Activity.objects.get(pk=self.kwargs["pk"]) + return form + + def form_valid(self, form): + form.instance.activity = Activity.objects.get(pk=self.kwargs["pk"]) + return super().form_valid(form) + + def get_success_url(self, **kwargs): + return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) + + +class ActivityEntryView(LoginRequiredMixin, TemplateView): + template_name = "activity/activity_entry.html" + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + activity = Activity.objects.get(pk=self.kwargs["pk"]) + ctx["activity"] = activity + + matched = [] + + pattern = "^$" + if "search" in self.request.GET: + pattern = self.request.GET["search"] + + if not pattern: + pattern = "^$" + + if pattern[0] != "^": + pattern = "^" + pattern + + guest_qs = Guest.objects\ + .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\ + .filter(Q(first_name__regex=pattern) | Q(last_name__regex=pattern) + | Q(inviter__alias__name__regex=pattern) + | Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))) \ + .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\ + .distinct()[:20] + for guest in guest_qs: + guest.type = "Invité" + matched.append(guest) + + note_qs = Alias.objects.annotate(last_name=F("note__noteuser__user__last_name"), + first_name=F("note__noteuser__user__first_name"), + username=F("note__noteuser__user__username"), + note_name=F("name"), + balance=F("note__balance"))\ + .filter(Q(note__polymorphic_ctype__model="noteuser") + & (Q(note__noteuser__user__first_name__regex=pattern) + | Q(note__noteuser__user__last_name__regex=pattern) + | Q(name__regex=pattern) + | Q(normalized_name__regex=Alias.normalize(pattern)))) \ + .filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))\ + .distinct("username")[:20] + for note in note_qs: + note.type = "Adhérent" + note.activity = activity + matched.append(note) + + table = EntryTable(data=matched) + ctx["table"] = table + + ctx["entries"] = Entry.objects.filter(activity=activity) + + ctx["title"] = _('Entry for activity "{}"').format(activity.name) + ctx["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk + ctx["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk + + return ctx diff --git a/apps/member/forms.py b/apps/member/forms.py index 5f2d5838..20f0acfe 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -4,10 +4,10 @@ from crispy_forms.bootstrap import Div from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout -from dal import autocomplete from django import forms from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.models import User +from note_kfet.inputs import Autocomplete from permission.models import PermissionMask from .models import Profile, Club, Membership @@ -63,11 +63,12 @@ class MembershipForm(forms.ModelForm): # et récupère les noms d'utilisateur valides widgets = { 'user': - autocomplete.ModelSelect2( - url='member:user_autocomplete', + Autocomplete( + User, attrs={ - 'data-placeholder': 'Nom ...', - 'data-minimum-input-length': 1, + 'api_url': '/api/user/', + 'name_field': 'username', + 'placeholder': 'Nom ...', }, ), } diff --git a/apps/member/urls.py b/apps/member/urls.py index 0b705bfd..085a3fec 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -21,6 +21,4 @@ urlpatterns = [ path('user//update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), path('user//aliases', views.ProfileAliasView.as_view(), name="user_alias"), path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), - # API for the user autocompleter - path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"), ] diff --git a/apps/member/views.py b/apps/member/views.py index 2992f76e..8145b5e9 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -4,24 +4,19 @@ import io from PIL import Image -from dal import autocomplete from django.conf import settings -from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.auth.views import LoginView -from django.core.exceptions import ValidationError from django.db.models import Q -from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, DetailView, UpdateView, TemplateView, DeleteView +from django.views.generic import CreateView, DetailView, UpdateView, TemplateView from django.views.generic.edit import FormMixin from django_tables2.views import SingleTableView from rest_framework.authtoken.models import Token from note.forms import ImageForm -#from note.forms import AliasForm, ImageForm from note.models import Alias, NoteUser from note.models.transactions import Transaction from note.tables import HistoryTable, AliasTable @@ -168,12 +163,12 @@ class UserListView(LoginRequiredMixin, SingleTableView): context["filter"] = self.filter return context - + class ProfileAliasView(LoginRequiredMixin, DetailView): model = User template_name = 'member/profile_alias.html' context_object_name = 'user_object' - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) note = context['object'].note @@ -257,28 +252,6 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): return context -class UserAutocomplete(autocomplete.Select2QuerySetView): - """ - Auto complete users by usernames - """ - - def get_queryset(self): - """ - Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion. - Cette fonction récupère la requête, et renvoie la liste filtrée des utilisateurs par pseudos. - """ - # Un utilisateur non connecté n'a accès à aucune information - if not self.request.user.is_authenticated: - return User.objects.none() - - qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all() - - if self.q: - qs = qs.filter(username__regex="^" + self.q) - - return qs - - # ******************************* # # CLUB # # ******************************* # @@ -294,7 +267,7 @@ class ClubCreateView(LoginRequiredMixin, CreateView): def form_valid(self, form): return super().form_valid(form) - + class ClubListView(LoginRequiredMixin, SingleTableView): """ @@ -326,11 +299,12 @@ class ClubDetailView(LoginRequiredMixin, DetailView): context['member_list'] = club_member return context + class ClubAliasView(LoginRequiredMixin, DetailView): model = Club template_name = 'member/club_alias.html' context_object_name = 'club' - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) note = context['object'].note @@ -364,6 +338,7 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView): return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view") | PermissionBackend.filter_queryset(self.request.user, Membership, "change")) + def get_context_data(self, **kwargs): club = Club.objects.get(pk=self.kwargs["pk"]) context = super().get_context_data(**kwargs) diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 05c35aa5..fbd12038 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -163,6 +163,7 @@ class SpecialTransactionSerializer(serializers.ModelSerializer): fields = '__all__' +# noinspection PyUnresolvedReferences class TransactionPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { Transaction: TransactionSerializer, @@ -171,5 +172,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 diff --git a/apps/note/api/views.py b/apps/note/api/views.py index e70eb49e..23bed1c9 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -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, TemplateCategorySerializer, \ @@ -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. diff --git a/apps/note/forms.py b/apps/note/forms.py index 60252ad5..50f226f2 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -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 ...', }, ), } diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 43faabfe..9282bde9 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -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."), diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index c6b8baa6..d1dcd788 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -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 _ diff --git a/apps/note/tables.py b/apps/note/tables.py index 201b6c43..0d83e3cc 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -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,8 @@ 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'}}) class ButtonTable(tables.Table): @@ -145,8 +143,8 @@ class ButtonTable(tables.Table): accessor='pk') 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'}}) def render_amount(self, value): return pretty_money(value) diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index ba527f9b..265870a8 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -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) diff --git a/apps/note/urls.py b/apps/note/urls.py index 59316069..9d6af317 100644 --- a/apps/note/urls.py +++ b/apps/note/urls.py @@ -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//', 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'), ] diff --git a/apps/note/views.py b/apps/note/views.py index ddf5ee6f..25279281 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -1,18 +1,17 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -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.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 .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 @@ -40,6 +39,7 @@ 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() @@ -47,62 +47,6 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView): return context -class NoteAutocomplete(autocomplete.Select2QuerySetView): - """ - 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 - (aka. ) - """ - # 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 diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 43d39a36..31b59069 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -55,6 +55,20 @@ "name": "Tr\u00e9sorier\u00b7\u00e8re de club" } }, + { + "model": "member.role", + "pk": 8, + "fields": { + "name": "Tr\u00e9sorier\u00b7\u00e8re de club" + } + }, + { + "model": "member.role", + "pk": 9, + "fields": { + "name": "Res[pot]" + } + }, { "model": "permission.permissionmask", "pk": 1, @@ -574,6 +588,201 @@ "description": "Create any transaction" } }, + { + "model": "permission.permission", + "pk": 34, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "[\"OR\", {\"valid\": true}, {\"creater\": [\"user\"]}]", + "type": "view", + "mask": 1, + "field": "", + "description": "View valid activites" + } + }, + { + "model": "permission.permission", + "pk": 35, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "[\"AND\", {\"valid\": false}, {\"creater\": [\"user\"]}]", + "type": "change", + "mask": 1, + "field": "", + "description": "Change our activities" + } + }, + { + "model": "permission.permission", + "pk": 36, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "{\"creater\": [\"user\"], \"valid\": false}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add activities" + } + }, + { + "model": "permission.permission", + "pk": 37, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "{}", + "type": "change", + "mask": 2, + "field": "valid", + "description": "Validate activities" + } + }, + { + "model": "permission.permission", + "pk": 38, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "{}", + "type": "change", + "mask": 2, + "field": "open", + "description": "Open activities" + } + }, + { + "model": "permission.permission", + "pk": 39, + "fields": { + "model": [ + "activity", + "guest" + ], + "query": "{\"inviter\": [\"user\", \"note\"], \"activity__activity_type__can_invite\": true}", + "type": "add", + "mask": 1, + "field": "", + "description": "Invite people to activities" + } + }, + { + "model": "permission.permission", + "pk": 40, + "fields": { + "model": [ + "activity", + "guest" + ], + "query": "{\"inviter\": [\"user\", \"note\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View invited people" + } + }, + { + "model": "permission.permission", + "pk": 41, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View all activities" + } + }, + { + "model": "permission.permission", + "pk": 42, + "fields": { + "model": [ + "activity", + "guest" + ], + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View all invited people" + } + }, + { + "model": "permission.permission", + "pk": 43, + "fields": { + "model": [ + "activity", + "entry" + ], + "query": "{}", + "type": "add", + "mask": 2, + "field": "", + "description": "Manage entries" + } + }, + { + "model": "permission.permission", + "pk": 44, + "fields": { + "model": [ + "activity", + "guesttransaction" + ], + "query": "{}", + "type": "add", + "mask": 2, + "field": "", + "description": "Add invitation transactions" + } + }, + { + "model": "permission.permission", + "pk": 45, + "fields": { + "model": [ + "activity", + "guesttransaction" + ], + "query": "{}", + "type": "view", + "mask": 1, + "field": "", + "description": "View invitation transactions" + } + }, + { + "model": "permission.permission", + "pk": 46, + "fields": { + "model": [ + "activity", + "guesttransaction" + ], + "query": "{}", + "type": "change", + "mask": 2, + "field": "valid", + "description": "Validate invitation transactions" + } + }, { "model": "permission.rolepermissions", "pk": 1, @@ -613,7 +822,12 @@ 15, 16, 17, - 18 + 18, + 34, + 35, + 36, + 39, + 40 ] } }, @@ -649,5 +863,22 @@ 33 ] } + }, + { + "model": "permission.rolepermissions", + "pk": 5, + "fields": { + "role": 9, + "permissions": [ + 37, + 38, + 41, + 42, + 43, + 44, + 45, + 46 + ] + } } ] diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py index 8bcd3597..aa2feeca 100644 --- a/apps/permission/templatetags/perms.py +++ b/apps/permission/templatetags/perms.py @@ -48,6 +48,11 @@ def not_empty_model_change_list(model_name): return session.get("not_empty_model_change_list_" + model_name) == 1 +def has_perm(perm, obj): + return PermissionBackend().has_perm(get_current_authenticated_user(), perm, obj) + + register = template.Library() register.filter('not_empty_model_list', not_empty_model_list) register.filter('not_empty_model_change_list', not_empty_model_change_list) +register.filter('has_perm', has_perm) diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py index caaa365f..7fe7de4c 100644 --- a/apps/treasury/forms.py +++ b/apps/treasury/forms.py @@ -7,6 +7,7 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit from django import forms from django.utils.translation import gettext_lazy as _ +from note_kfet.inputs import DatePickerInput, AmountInput from .models import Invoice, Product, Remittance, SpecialTransactionProxy @@ -19,7 +20,7 @@ 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=forms.TextInput(attrs={'type': 'date'}) + widget=DatePickerInput() ) def clean_date(self): @@ -30,12 +31,21 @@ class InvoiceForm(forms.ModelForm): exclude = ('bde', ) +class ProductForm(forms.ModelForm): + class Meta: + model = Product + fields = '__all__' + widgets = { + "amount": AmountInput() + } + + # Add a subform per product in the invoice form, and manage correctly the link between the invoice and # its products. The FormSet will search automatically the ForeignKey in the Product model. ProductFormSet = forms.inlineformset_factory( Invoice, Product, - fields='__all__', + form=ProductForm, extra=1, ) diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 90440566..c374ced1 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -50,18 +50,8 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView): def form_valid(self, form): ret = super().form_valid(form) - kwargs = {} - - # The user type amounts in cents. We convert it in euros. - for key in self.request.POST: - value = self.request.POST[key] - if key.endswith("amount") and value: - kwargs[key] = str(int(100 * float(value))) - elif value: - kwargs[key] = value - # For each product, we save it - formset = ProductFormSet(kwargs, instance=form.instance) + formset = ProductFormSet(self.request.POST, instance=form.instance) if formset.is_valid(): for f in formset: # We don't save the product if the designation is not entered, ie. if the line is empty @@ -112,16 +102,7 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView): def form_valid(self, form): ret = super().form_valid(form) - kwargs = {} - # The user type amounts in cents. We convert it in euros. - for key in self.request.POST: - value = self.request.POST[key] - if key.endswith("amount") and value: - kwargs[key] = str(int(100 * float(value))) - elif value: - kwargs[key] = value - - formset = ProductFormSet(kwargs, instance=form.instance) + formset = ProductFormSet(self.request.POST, instance=form.instance) saved = [] # For each product, we save it if formset.is_valid(): diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index c9eda5aa..e2c8075e 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-26 14:40+0100\n" +"POT-Creation-Date: 2020-03-30 17:31+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,72 +18,182 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: apps/activity/apps.py:10 apps/activity/models.py:76 +#: apps/activity/apps.py:10 apps/activity/models.py:111 +#: apps/activity/models.py:120 msgid "activity" msgstr "" -#: apps/activity/models.py:19 apps/activity/models.py:44 -#: apps/member/models.py:63 apps/member/models.py:114 -#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 -#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:232 -#: templates/member/profile_detail.html:15 +#: apps/activity/forms.py:45 apps/activity/models.py:217 +msgid "You can't invite someone once the activity is started." +msgstr "" + +#: apps/activity/forms.py:48 apps/activity/models.py:220 +msgid "This activity is not validated yet." +msgstr "" + +#: apps/activity/forms.py:58 apps/activity/models.py:228 +msgid "This person has been already invited 5 times this year." +msgstr "" + +#: apps/activity/forms.py:62 apps/activity/models.py:232 +msgid "This person is already invited." +msgstr "" + +#: apps/activity/forms.py:66 apps/activity/models.py:236 +msgid "You can't invite more than 3 people to this activity." +msgstr "" + +#: apps/activity/models.py:23 apps/activity/models.py:48 +#: apps/member/models.py:64 apps/member/models.py:122 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:231 +#: templates/member/club_info.html:13 templates/member/profile_info.html:14 msgid "name" msgstr "" -#: apps/activity/models.py:23 +#: apps/activity/models.py:27 templates/activity/activity_detail.html:39 msgid "can invite" msgstr "" -#: apps/activity/models.py:26 +#: apps/activity/models.py:30 templates/activity/activity_detail.html:43 msgid "guest entry fee" msgstr "" -#: apps/activity/models.py:30 +#: apps/activity/models.py:34 msgid "activity type" msgstr "" -#: apps/activity/models.py:31 +#: apps/activity/models.py:35 msgid "activity types" msgstr "" -#: apps/activity/models.py:48 apps/note/models/transactions.py:70 -#: apps/permission/models.py:91 +#: apps/activity/models.py:53 apps/note/models/transactions.py:69 +#: apps/permission/models.py:90 templates/activity/activity_detail.html:16 msgid "description" msgstr "" -#: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:63 +#: apps/activity/models.py:60 apps/note/models/notes.py:164 +#: apps/note/models/transactions.py:62 +#: templates/activity/activity_detail.html:19 msgid "type" msgstr "" -#: apps/activity/models.py:60 +#: apps/activity/models.py:66 apps/logs/models.py:21 +#: apps/note/models/notes.py:117 +msgid "user" +msgstr "" + +#: apps/activity/models.py:73 templates/activity/activity_detail.html:33 msgid "organizer" msgstr "" -#: apps/activity/models.py:66 +#: apps/activity/models.py:82 apps/activity/models.py:131 apps/note/apps.py:14 +#: apps/note/models/notes.py:58 +msgid "note" +msgstr "" + +#: apps/activity/models.py:89 templates/activity/activity_detail.html:36 msgid "attendees club" msgstr "" -#: apps/activity/models.py:69 +#: apps/activity/models.py:93 templates/activity/activity_detail.html:22 msgid "start date" msgstr "" -#: apps/activity/models.py:72 +#: apps/activity/models.py:97 templates/activity/activity_detail.html:25 msgid "end date" msgstr "" -#: apps/activity/models.py:77 +#: apps/activity/models.py:102 apps/note/models/transactions.py:134 +#: templates/activity/activity_detail.html:47 +msgid "valid" +msgstr "" + +#: apps/activity/models.py:107 templates/activity/activity_detail.html:61 +msgid "open" +msgstr "" + +#: apps/activity/models.py:112 msgid "activities" msgstr "" -#: apps/activity/models.py:108 +#: apps/activity/models.py:125 +msgid "entry time" +msgstr "" + +#: apps/activity/models.py:148 +msgid "Already entered on " +msgstr "" + +#: apps/activity/models.py:148 apps/activity/tables.py:54 +msgid "{:%Y-%m-%d %H:%M:%S}" +msgstr "" + +#: apps/activity/models.py:156 +msgid "The balance is negative." +msgstr "" + +#: apps/activity/models.py:188 +msgid "last name" +msgstr "" + +#: apps/activity/models.py:193 templates/member/profile_info.html:14 +msgid "first name" +msgstr "" + +#: apps/activity/models.py:200 +msgid "inviter" +msgstr "" + +#: apps/activity/models.py:241 msgid "guest" msgstr "" -#: apps/activity/models.py:109 +#: apps/activity/models.py:242 msgid "guests" msgstr "" +#: apps/activity/models.py:254 +msgid "Invitation" +msgstr "" + +#: apps/activity/tables.py:54 +msgid "Entered on " +msgstr "" + +#: apps/activity/tables.py:55 +msgid "remove" +msgstr "" + +#: apps/activity/tables.py:75 apps/treasury/models.py:126 +msgid "Type" +msgstr "" + +#: apps/activity/tables.py:77 apps/treasury/forms.py:120 +msgid "Last name" +msgstr "" + +#: apps/activity/tables.py:79 apps/treasury/forms.py:122 +#: templates/note/transaction_form.html:92 +msgid "First name" +msgstr "" + +#: apps/activity/tables.py:81 apps/note/models/notes.py:67 +msgid "Note" +msgstr "" + +#: apps/activity/tables.py:83 +msgid "Balance" +msgstr "" + +#: apps/activity/views.py:44 templates/base.html:94 +msgid "Activities" +msgstr "" + +#: apps/activity/views.py:149 +msgid "Entry for activity \"{}\"" +msgstr "" + #: apps/api/apps.py:10 msgid "API" msgstr "" @@ -92,10 +202,6 @@ msgstr "" msgid "Logs" msgstr "" -#: apps/logs/models.py:21 apps/note/models/notes.py:117 -msgid "user" -msgstr "" - #: apps/logs/models.py:27 msgid "IP Address" msgstr "" @@ -120,11 +226,12 @@ msgstr "" msgid "create" msgstr "" -#: apps/logs/models.py:61 apps/note/tables.py:147 +#: apps/logs/models.py:61 apps/note/tables.py:142 +#: templates/activity/activity_detail.html:67 msgid "edit" msgstr "" -#: apps/logs/models.py:62 apps/note/tables.py:151 +#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:146 msgid "delete" msgstr "" @@ -144,139 +251,130 @@ msgstr "" msgid "member" msgstr "" -#: apps/member/models.py:25 +#: apps/member/models.py:26 msgid "phone number" msgstr "" -#: apps/member/models.py:31 templates/member/profile_detail.html:28 +#: apps/member/models.py:32 templates/member/profile_info.html:27 msgid "section" msgstr "" -#: apps/member/models.py:32 +#: apps/member/models.py:33 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "" -#: apps/member/models.py:38 templates/member/profile_detail.html:31 +#: apps/member/models.py:39 templates/member/profile_info.html:30 msgid "address" msgstr "" -#: apps/member/models.py:44 +#: apps/member/models.py:45 msgid "paid" msgstr "" -#: apps/member/models.py:49 apps/member/models.py:50 +#: apps/member/models.py:50 apps/member/models.py:51 msgid "user profile" msgstr "" -#: apps/member/models.py:68 +#: apps/member/models.py:69 templates/member/club_info.html:36 msgid "email" msgstr "" -#: apps/member/models.py:73 +#: apps/member/models.py:76 +msgid "parent club" +msgstr "" + +#: apps/member/models.py:81 templates/member/club_info.html:30 msgid "membership fee" msgstr "" -#: apps/member/models.py:77 +#: apps/member/models.py:85 templates/member/club_info.html:27 msgid "membership duration" msgstr "" -#: apps/member/models.py:78 +#: apps/member/models.py:86 msgid "The longest time a membership can last (NULL = infinite)." msgstr "" -#: apps/member/models.py:83 +#: apps/member/models.py:91 templates/member/club_info.html:21 msgid "membership start" msgstr "" -#: apps/member/models.py:84 +#: apps/member/models.py:92 msgid "How long after January 1st the members can renew their membership." msgstr "" -#: apps/member/models.py:89 +#: apps/member/models.py:97 templates/member/club_info.html:24 msgid "membership end" msgstr "" -#: apps/member/models.py:90 +#: apps/member/models.py:98 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:96 apps/note/models/notes.py:139 +#: apps/member/models.py:104 apps/note/models/notes.py:139 msgid "club" msgstr "" -#: apps/member/models.py:97 +#: apps/member/models.py:105 msgid "clubs" msgstr "" -#: apps/member/models.py:120 apps/permission/models.py:276 +#: apps/member/models.py:128 apps/permission/models.py:275 msgid "role" msgstr "" -#: apps/member/models.py:121 +#: apps/member/models.py:129 msgid "roles" msgstr "" -#: apps/member/models.py:145 +#: apps/member/models.py:153 msgid "membership starts on" msgstr "" -#: apps/member/models.py:148 +#: apps/member/models.py:156 msgid "membership ends on" msgstr "" -#: apps/member/models.py:152 +#: apps/member/models.py:160 msgid "fee" msgstr "" -#: apps/member/models.py:162 +#: apps/member/models.py:172 +msgid "User is not a member of the parent club" +msgstr "" + +#: apps/member/models.py:176 msgid "membership" msgstr "" -#: apps/member/models.py:163 +#: apps/member/models.py:177 msgid "memberships" msgstr "" -#: apps/member/views.py:80 templates/member/profile_detail.html:46 +#: apps/member/views.py:76 templates/member/profile_info.html:45 msgid "Update Profile" msgstr "" -#: apps/member/views.py:93 +#: apps/member/views.py:89 msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/views.py:146 -#, python-format -msgid "Account #%(id)s: %(username)s" -msgstr "" - -#: apps/member/views.py:216 -msgid "Alias successfully deleted" -msgstr "" - -#: apps/note/admin.py:120 apps/note/models/transactions.py:95 +#: 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:54 apps/note/models/transactions.py:108 +#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107 msgid "destination" msgstr "" -#: apps/note/apps.py:14 apps/note/models/notes.py:58 -msgid "note" -msgstr "" - -#: apps/note/forms.py:20 -msgid "New Alias" -msgstr "" - -#: apps/note/forms.py:25 +#: apps/note/forms.py:14 msgid "select an image" msgstr "" -#: apps/note/forms.py:26 +#: apps/note/forms.py:15 msgid "Maximal size: 2MB" msgstr "" @@ -310,7 +408,7 @@ msgstr "" msgid "display image" msgstr "" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:118 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:117 msgid "created at" msgstr "" @@ -318,10 +416,6 @@ msgstr "" msgid "notes" msgstr "" -#: apps/note/models/notes.py:67 -msgid "Note" -msgstr "" - #: apps/note/models/notes.py:77 apps/note/models/notes.py:101 msgid "This alias is already taken." msgstr "" @@ -368,7 +462,8 @@ msgstr "" msgid "alias" msgstr "" -#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37 +#: apps/note/models/notes.py:211 templates/member/club_info.html:33 +#: templates/member/profile_info.html:36 msgid "aliases" msgstr "" @@ -380,106 +475,114 @@ msgstr "" msgid "An alias with a similar name already exists: {} " msgstr "" -#: apps/note/models/notes.py:247 +#: apps/note/models/notes.py:251 msgid "You can't delete your main alias." msgstr "" -#: apps/note/models/transactions.py:31 +#: apps/note/models/transactions.py:30 msgid "transaction category" msgstr "" -#: apps/note/models/transactions.py:32 +#: apps/note/models/transactions.py:31 msgid "transaction categories" msgstr "" -#: apps/note/models/transactions.py:48 +#: apps/note/models/transactions.py:47 msgid "A template with this name already exist" msgstr "" -#: apps/note/models/transactions.py:57 apps/note/models/transactions.py:126 +#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:125 msgid "amount" msgstr "" -#: apps/note/models/transactions.py:58 +#: apps/note/models/transactions.py:57 msgid "in centimes" msgstr "" -#: apps/note/models/transactions.py:76 +#: apps/note/models/transactions.py:75 msgid "transaction template" msgstr "" -#: apps/note/models/transactions.py:77 +#: apps/note/models/transactions.py:76 msgid "transaction templates" msgstr "" -#: apps/note/models/transactions.py:101 apps/note/models/transactions.py:114 +#: apps/note/models/transactions.py:100 apps/note/models/transactions.py:113 #: apps/note/tables.py:33 apps/note/tables.py:42 msgid "used alias" msgstr "" -#: apps/note/models/transactions.py:122 +#: apps/note/models/transactions.py:121 msgid "quantity" msgstr "" -#: apps/note/models/transactions.py:130 +#: apps/note/models/transactions.py:129 msgid "reason" msgstr "" -#: apps/note/models/transactions.py:135 -msgid "valid" -msgstr "" - -#: apps/note/models/transactions.py:140 apps/note/tables.py:95 +#: apps/note/models/transactions.py:139 apps/note/tables.py:95 msgid "invalidity reason" msgstr "" -#: apps/note/models/transactions.py:147 +#: apps/note/models/transactions.py:146 msgid "transaction" msgstr "" -#: apps/note/models/transactions.py:148 +#: apps/note/models/transactions.py:147 msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:202 templates/base.html:83 +#: apps/note/models/transactions.py:201 templates/base.html:84 #: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:145 +#: templates/note/transaction_form.html:140 msgid "Transfer" msgstr "" -#: apps/note/models/transactions.py:188 +#: apps/note/models/transactions.py:221 msgid "Template" msgstr "" -#: apps/note/models/transactions.py:203 +#: apps/note/models/transactions.py:236 msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:208 +#: apps/note/models/transactions.py:241 msgid "bank" msgstr "" -#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 +#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:24 msgid "Credit" msgstr "" -#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:28 msgid "Debit" msgstr "" -#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 +#: apps/note/models/transactions.py:263 apps/note/models/transactions.py:268 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:231 +#: apps/note/models/transactions.py:264 msgid "membership transactions" msgstr "" -#: apps/note/views.py:39 +#: apps/note/tables.py:57 +msgid "Click to invalidate" +msgstr "" + +#: apps/note/tables.py:57 +msgid "Click to validate" +msgstr "" + +#: apps/note/tables.py:93 +msgid "No reason specified" +msgstr "" + +#: apps/note/views.py:41 msgid "Transfer money" msgstr "" -#: apps/note/views.py:145 templates/base.html:79 +#: apps/note/views.py:102 templates/base.html:79 msgid "Consumptions" msgstr "" @@ -501,41 +604,35 @@ msgstr "" msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/treasury/apps.py:11 templates/base.html:102 +#: apps/treasury/apps.py:12 templates/base.html:99 msgid "Treasury" msgstr "" -#: apps/treasury/forms.py:56 apps/treasury/forms.py:95 +#: apps/treasury/forms.py:84 apps/treasury/forms.py:132 +#: templates/activity/activity_form.html:9 +#: templates/activity/activity_invite.html:8 #: templates/django_filters/rest_framework/form.html:5 -#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47 +#: templates/member/club_form.html:9 templates/treasury/invoice_form.html:46 msgid "Submit" msgstr "" -#: apps/treasury/forms.py:58 +#: apps/treasury/forms.py:86 msgid "Close" msgstr "" -#: apps/treasury/forms.py:65 +#: apps/treasury/forms.py:95 msgid "Remittance is already closed." msgstr "" -#: apps/treasury/forms.py:70 +#: apps/treasury/forms.py:100 msgid "You can't change the type of the remittance." msgstr "" -#: apps/treasury/forms.py:84 -msgid "Last name" -msgstr "" - -#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92 -msgid "First name" -msgstr "" - -#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 +#: apps/treasury/forms.py:124 templates/note/transaction_form.html:98 msgid "Bank" msgstr "" -#: apps/treasury/forms.py:90 apps/treasury/tables.py:40 +#: apps/treasury/forms.py:126 apps/treasury/tables.py:47 #: templates/note/transaction_form.html:128 #: templates/treasury/remittance_form.html:18 msgid "Amount" @@ -589,10 +686,6 @@ msgstr "" msgid "Date" msgstr "" -#: apps/treasury/models.py:126 -msgid "Type" -msgstr "" - #: apps/treasury/models.py:131 msgid "Comment" msgstr "" @@ -601,38 +694,38 @@ msgstr "" msgid "Closed" msgstr "" -#: apps/treasury/models.py:159 +#: apps/treasury/models.py:169 msgid "Remittance #{:d}: {}" msgstr "" -#: apps/treasury/models.py:178 apps/treasury/tables.py:64 -#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13 +#: apps/treasury/models.py:188 apps/treasury/tables.py:76 +#: apps/treasury/tables.py:84 templates/treasury/invoice_list.html:13 #: templates/treasury/remittance_list.html:13 msgid "Remittance" msgstr "" -#: apps/treasury/tables.py:16 +#: apps/treasury/tables.py:19 msgid "Invoice #{:d}" msgstr "" -#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10 +#: apps/treasury/tables.py:22 templates/treasury/invoice_list.html:10 #: templates/treasury/remittance_list.html:10 msgid "Invoice" msgstr "" -#: apps/treasury/tables.py:38 +#: apps/treasury/tables.py:45 msgid "Transaction count" msgstr "" -#: apps/treasury/tables.py:43 apps/treasury/tables.py:45 +#: apps/treasury/tables.py:50 apps/treasury/tables.py:52 msgid "View" msgstr "" -#: apps/treasury/tables.py:66 +#: apps/treasury/tables.py:78 msgid "Add" msgstr "" -#: apps/treasury/tables.py:74 +#: apps/treasury/tables.py:86 msgid "Remove" msgstr "" @@ -643,30 +736,86 @@ msgid "" "again unless your session expires or you logout." msgstr "" -#: note_kfet/settings/base.py:151 +#: note_kfet/settings/base.py:152 msgid "German" msgstr "" -#: note_kfet/settings/base.py:152 +#: note_kfet/settings/base.py:153 msgid "English" msgstr "" -#: note_kfet/settings/base.py:153 +#: note_kfet/settings/base.py:154 msgid "French" msgstr "" +#: templates/activity/activity_detail.html:29 +msgid "creater" +msgstr "" + +#: templates/activity/activity_detail.html:50 +msgid "opened" +msgstr "" + +#: templates/activity/activity_detail.html:57 +msgid "Entry page" +msgstr "" + +#: templates/activity/activity_detail.html:61 +msgid "close" +msgstr "" + +#: templates/activity/activity_detail.html:64 +msgid "invalidate" +msgstr "" + +#: templates/activity/activity_detail.html:64 +msgid "validate" +msgstr "" + +#: templates/activity/activity_detail.html:70 +msgid "Invite" +msgstr "" + +#: templates/activity/activity_detail.html:77 +msgid "Guests list" +msgstr "" + +#: templates/activity/activity_entry.html:10 +msgid "Return to activity page" +msgstr "" + +#: templates/activity/activity_entry.html:18 +msgid "entries" +msgstr "" + +#: templates/activity/activity_entry.html:18 +msgid "entry" +msgstr "" + +#: templates/activity/activity_list.html:5 +msgid "Upcoming activities" +msgstr "" + +#: templates/activity/activity_list.html:10 +msgid "There is no planned activity." +msgstr "" + +#: templates/activity/activity_list.html:14 +msgid "New activity" +msgstr "" + +#: templates/activity/activity_list.html:18 +msgid "All activities" +msgstr "" + #: templates/base.html:13 msgid "The ENS Paris-Saclay BDE note." msgstr "" -#: templates/base.html:87 +#: templates/base.html:89 msgid "Clubs" msgstr "" -#: templates/base.html:92 -msgid "Activities" -msgstr "" - #: templates/cas_server/base.html:7 msgid "Central Authentication Service" msgstr "" @@ -722,32 +871,48 @@ msgstr "" msgid "Field filters" msgstr "" -#: templates/member/club_detail.html:10 -msgid "Membership starts on" +#: templates/member/alias_update.html:5 +msgid "Add alias" msgstr "" -#: templates/member/club_detail.html:12 -msgid "Membership ends on" +#: templates/member/club_info.html:17 +msgid "Club Parent" msgstr "" -#: templates/member/club_detail.html:14 -msgid "Membership duration" +#: templates/member/club_info.html:41 +msgid "Add member" msgstr "" -#: templates/member/club_detail.html:18 templates/member/profile_detail.html:34 -msgid "balance" +#: templates/member/club_info.html:42 templates/note/conso_form.html:121 +msgid "Edit" msgstr "" -#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75 -msgid "Transaction history" +#: templates/member/club_info.html:43 +msgid "Add roles" msgstr "" -#: templates/member/club_form.html:6 -msgid "Clubs list" +#: templates/member/club_info.html:46 templates/member/profile_info.html:48 +msgid "View Profile" msgstr "" #: templates/member/club_list.html:8 -msgid "New club" +msgid "search clubs" +msgstr "" + +#: templates/member/club_list.html:12 +msgid "Créer un club" +msgstr "" + +#: templates/member/club_list.html:19 +msgid "club listing " +msgstr "" + +#: templates/member/club_tables.html:9 +msgid "Member of the Club" +msgstr "" + +#: templates/member/club_tables.html:22 templates/member/profile_tables.html:22 +msgid "Transaction history" msgstr "" #: templates/member/manage_auth_tokens.html:16 @@ -762,35 +927,31 @@ msgstr "" msgid "Regenerate token" msgstr "" -#: templates/member/profile_alias.html:10 -msgid "Add alias" +#: templates/member/profile_info.html:5 +msgid "Account #" msgstr "" -#: templates/member/profile_detail.html:15 -msgid "first name" -msgstr "" - -#: templates/member/profile_detail.html:18 +#: templates/member/profile_info.html:17 msgid "username" msgstr "" -#: templates/member/profile_detail.html:21 +#: templates/member/profile_info.html:20 msgid "password" msgstr "" -#: templates/member/profile_detail.html:24 +#: templates/member/profile_info.html:23 msgid "Change password" msgstr "" -#: templates/member/profile_detail.html:42 +#: templates/member/profile_info.html:33 +msgid "balance" +msgstr "" + +#: templates/member/profile_info.html:41 msgid "Manage auth token" msgstr "" -#: templates/member/profile_detail.html:49 -msgid "View Profile" -msgstr "" - -#: templates/member/profile_detail.html:62 +#: templates/member/profile_tables.html:9 msgid "View my memberships" msgstr "" @@ -819,10 +980,6 @@ msgstr "" msgid "Most used buttons" msgstr "" -#: templates/note/conso_form.html:121 -msgid "Edit" -msgstr "" - #: templates/note/conso_form.html:126 msgid "Single consumptions" msgstr "" @@ -831,7 +988,7 @@ msgstr "" msgid "Double consumptions" msgstr "" -#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152 +#: templates/note/conso_form.html:141 templates/note/transaction_form.html:147 msgid "Recent transactions history" msgstr "" @@ -847,37 +1004,21 @@ msgstr "" msgid "Transfer type" msgstr "" -#: templates/note/transaction_form.html:86 -msgid "Name" -msgstr "" - -#: templates/note/transaction_form.html:92 -msgid "First name" -msgstr "" - -#: templates/note/transaction_form.html:98 -msgid "Bank" -msgstr "" - #: templates/note/transaction_form.html:111 -#: templates/note/transaction_form.html:169 -#: templates/note/transaction_form.html:176 +#: templates/note/transaction_form.html:164 +#: templates/note/transaction_form.html:171 msgid "Select receivers" msgstr "" -#: templates/note/transaction_form.html:128 -msgid "Amount" -msgstr "" - -#: templates/note/transaction_form.html:138 +#: templates/note/transaction_form.html:133 msgid "Reason" msgstr "" -#: templates/note/transaction_form.html:183 +#: templates/note/transaction_form.html:178 msgid "Credit note" msgstr "" -#: templates/note/transaction_form.html:190 +#: templates/note/transaction_form.html:185 msgid "Debit note" msgstr "" @@ -889,6 +1030,10 @@ msgstr "" msgid "search button" msgstr "" +#: templates/note/transactiontemplate_list.html:13 +msgid "New button" +msgstr "" + #: templates/note/transactiontemplate_list.html:20 msgid "buttons listing " msgstr "" @@ -991,11 +1136,11 @@ msgstr "" msgid "Invoices list" msgstr "" -#: templates/treasury/invoice_form.html:42 +#: templates/treasury/invoice_form.html:41 msgid "Add product" msgstr "" -#: templates/treasury/invoice_form.html:43 +#: templates/treasury/invoice_form.html:42 msgid "Remove product" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index ca43d5a4..08c1b174 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-26 14:40+0100\n" +"POT-Creation-Date: 2020-03-30 17:31+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -13,83 +13,189 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: apps/activity/apps.py:10 apps/activity/models.py:76 +#: apps/activity/apps.py:10 apps/activity/models.py:111 +#: apps/activity/models.py:120 msgid "activity" msgstr "activité" -#: apps/activity/models.py:19 apps/activity/models.py:44 -#: apps/member/models.py:63 apps/member/models.py:114 -#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 -#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:232 -#: templates/member/profile_detail.html:15 +#: apps/activity/forms.py:45 apps/activity/models.py:217 +msgid "You can't invite someone once the activity is started." +msgstr "Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré." + +#: apps/activity/forms.py:48 apps/activity/models.py:220 +msgid "This activity is not validated yet." +msgstr "Cette activité n'est pas encore validée." + +#: apps/activity/forms.py:58 apps/activity/models.py:228 +msgid "This person has been already invited 5 times this year." +msgstr "Cette personne a déjà été invitée 5 fois cette année." + +#: apps/activity/forms.py:62 apps/activity/models.py:232 +msgid "This person is already invited." +msgstr "Cette personne est déjà invitée." + +#: apps/activity/forms.py:66 apps/activity/models.py:236 +msgid "You can't invite more than 3 people to this activity." +msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." + +#: apps/activity/models.py:23 apps/activity/models.py:48 +#: apps/member/models.py:64 apps/member/models.py:122 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:231 +#: templates/member/club_info.html:13 templates/member/profile_info.html:14 msgid "name" msgstr "nom" -#: apps/activity/models.py:23 +#: apps/activity/models.py:27 templates/activity/activity_detail.html:39 msgid "can invite" msgstr "peut inviter" -#: apps/activity/models.py:26 +#: apps/activity/models.py:30 templates/activity/activity_detail.html:43 msgid "guest entry fee" msgstr "cotisation de l'entrée invité" -#: apps/activity/models.py:30 +#: apps/activity/models.py:34 msgid "activity type" msgstr "type d'activité" -#: apps/activity/models.py:31 +#: apps/activity/models.py:35 msgid "activity types" msgstr "types d'activité" -#: apps/activity/models.py:48 apps/note/models/transactions.py:70 -#: apps/permission/models.py:91 +#: apps/activity/models.py:53 apps/note/models/transactions.py:69 +#: apps/permission/models.py:90 templates/activity/activity_detail.html:16 msgid "description" msgstr "description" -#: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:63 +#: apps/activity/models.py:60 apps/note/models/notes.py:164 +#: apps/note/models/transactions.py:62 +#: templates/activity/activity_detail.html:19 msgid "type" msgstr "type" -#: apps/activity/models.py:60 +#: apps/activity/models.py:66 apps/logs/models.py:21 +#: apps/note/models/notes.py:117 +msgid "user" +msgstr "utilisateur" + +#: apps/activity/models.py:73 templates/activity/activity_detail.html:33 msgid "organizer" msgstr "organisateur" -#: apps/activity/models.py:66 -msgid "attendees club" -msgstr "" +#: apps/activity/models.py:82 apps/activity/models.py:131 apps/note/apps.py:14 +#: apps/note/models/notes.py:58 +msgid "note" +msgstr "note" -#: apps/activity/models.py:69 +#: apps/activity/models.py:89 templates/activity/activity_detail.html:36 +msgid "attendees club" +msgstr "club attendu" + +#: apps/activity/models.py:93 templates/activity/activity_detail.html:22 msgid "start date" msgstr "date de début" -#: apps/activity/models.py:72 +#: apps/activity/models.py:97 templates/activity/activity_detail.html:25 msgid "end date" msgstr "date de fin" -#: apps/activity/models.py:77 +#: apps/activity/models.py:102 apps/note/models/transactions.py:134 +#: templates/activity/activity_detail.html:47 +msgid "valid" +msgstr "valide" + +#: apps/activity/models.py:107 templates/activity/activity_detail.html:61 +msgid "open" +msgstr "ouvrir" + +#: apps/activity/models.py:112 msgid "activities" msgstr "activités" -#: apps/activity/models.py:108 +#: apps/activity/models.py:125 +msgid "entry time" +msgstr "heure d'entrée" + +#: apps/activity/models.py:148 +msgid "Already entered on " +msgstr "Déjà rentré le " + +#: apps/activity/models.py:148 apps/activity/tables.py:54 +msgid "{:%Y-%m-%d %H:%M:%S}" +msgstr "{:%d/%m/%Y %H:%M:%S}" + +#: apps/activity/models.py:156 +msgid "The balance is negative." +msgstr "La note est en négatif." + +#: apps/activity/models.py:188 +msgid "last name" +msgstr "nom de famille" + +#: apps/activity/models.py:193 templates/member/profile_info.html:14 +msgid "first name" +msgstr "prénom" + +#: apps/activity/models.py:200 +msgid "inviter" +msgstr "hôte" + +#: apps/activity/models.py:241 msgid "guest" msgstr "invité" -#: apps/activity/models.py:109 +#: apps/activity/models.py:242 msgid "guests" msgstr "invités" +#: apps/activity/models.py:254 +msgid "Invitation" +msgstr "Invitation" + +#: apps/activity/tables.py:54 +msgid "Entered on " +msgstr "Entré le " + +#: apps/activity/tables.py:55 +msgid "remove" +msgstr "supprimer" + +#: apps/activity/tables.py:75 apps/treasury/models.py:126 +msgid "Type" +msgstr "Type" + +#: apps/activity/tables.py:77 apps/treasury/forms.py:120 +msgid "Last name" +msgstr "Nom de famille" + +#: apps/activity/tables.py:79 apps/treasury/forms.py:122 +#: templates/note/transaction_form.html:92 +msgid "First name" +msgstr "Prénom" + +#: apps/activity/tables.py:81 apps/note/models/notes.py:67 +msgid "Note" +msgstr "Note" + +#: apps/activity/tables.py:83 +msgid "Balance" +msgstr "Solde du compte" + +#: apps/activity/views.py:44 templates/base.html:94 +msgid "Activities" +msgstr "Activités" + +#: apps/activity/views.py:149 +msgid "Entry for activity \"{}\"" +msgstr "Entrées pour l'activité « {} »" + #: apps/api/apps.py:10 msgid "API" -msgstr "" +msgstr "API" #: apps/logs/apps.py:11 msgid "Logs" -msgstr "" - -#: apps/logs/models.py:21 apps/note/models/notes.py:117 -msgid "user" -msgstr "utilisateur" +msgstr "Logs" #: apps/logs/models.py:27 msgid "IP Address" @@ -115,11 +221,12 @@ msgstr "Nouvelles données" msgid "create" msgstr "Créer" -#: apps/logs/models.py:61 apps/note/tables.py:147 +#: apps/logs/models.py:61 apps/note/tables.py:142 +#: templates/activity/activity_detail.html:67 msgid "edit" msgstr "Modifier" -#: apps/logs/models.py:62 apps/note/tables.py:151 +#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:146 msgid "delete" msgstr "Supprimer" @@ -139,61 +246,65 @@ msgstr "Les logs ne peuvent pas être détruits." msgid "member" msgstr "adhérent" -#: apps/member/models.py:25 +#: apps/member/models.py:26 msgid "phone number" msgstr "numéro de téléphone" -#: apps/member/models.py:31 templates/member/profile_detail.html:28 +#: apps/member/models.py:32 templates/member/profile_info.html:27 msgid "section" msgstr "section" -#: apps/member/models.py:32 +#: apps/member/models.py:33 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" -#: apps/member/models.py:38 templates/member/profile_detail.html:31 +#: apps/member/models.py:39 templates/member/profile_info.html:30 msgid "address" msgstr "adresse" -#: apps/member/models.py:44 +#: apps/member/models.py:45 msgid "paid" msgstr "payé" -#: apps/member/models.py:49 apps/member/models.py:50 +#: apps/member/models.py:50 apps/member/models.py:51 msgid "user profile" msgstr "profil utilisateur" -#: apps/member/models.py:68 +#: apps/member/models.py:69 templates/member/club_info.html:36 msgid "email" msgstr "courriel" -#: apps/member/models.py:73 +#: apps/member/models.py:76 +msgid "parent club" +msgstr "club parent" + +#: apps/member/models.py:81 templates/member/club_info.html:30 msgid "membership fee" msgstr "cotisation pour adhérer" -#: apps/member/models.py:77 +#: apps/member/models.py:85 templates/member/club_info.html:27 msgid "membership duration" msgstr "durée de l'adhésion" -#: apps/member/models.py:78 +#: apps/member/models.py:86 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:83 +#: apps/member/models.py:91 templates/member/club_info.html:21 msgid "membership start" msgstr "début de l'adhésion" -#: apps/member/models.py:84 +#: apps/member/models.py:92 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:89 +#: apps/member/models.py:97 templates/member/club_info.html:24 msgid "membership end" msgstr "fin de l'adhésion" -#: apps/member/models.py:90 +#: apps/member/models.py:98 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." @@ -201,81 +312,68 @@ 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:96 apps/note/models/notes.py:139 +#: apps/member/models.py:104 apps/note/models/notes.py:139 msgid "club" msgstr "club" -#: apps/member/models.py:97 +#: apps/member/models.py:105 msgid "clubs" msgstr "clubs" -#: apps/member/models.py:120 apps/permission/models.py:276 +#: apps/member/models.py:128 apps/permission/models.py:275 msgid "role" msgstr "rôle" -#: apps/member/models.py:121 +#: apps/member/models.py:129 msgid "roles" msgstr "rôles" -#: apps/member/models.py:145 +#: apps/member/models.py:153 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:148 +#: apps/member/models.py:156 msgid "membership ends on" msgstr "l'adhésion finie le" -#: apps/member/models.py:152 +#: apps/member/models.py:160 msgid "fee" msgstr "cotisation" -#: apps/member/models.py:162 +#: apps/member/models.py:172 +msgid "User is not a member of the parent club" +msgstr "L'utilisateur n'est pas membre du club parent" + +#: apps/member/models.py:176 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:163 +#: apps/member/models.py:177 msgid "memberships" msgstr "adhésions" -#: apps/member/views.py:80 templates/member/profile_detail.html:46 +#: apps/member/views.py:76 templates/member/profile_info.html:45 msgid "Update Profile" msgstr "Modifier le profil" -#: apps/member/views.py:93 +#: apps/member/views.py:89 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà." -#: apps/member/views.py:146 -#, python-format -msgid "Account #%(id)s: %(username)s" -msgstr "Compte n°%(id)s : %(username)s" - -#: apps/member/views.py:216 -msgid "Alias successfully deleted" -msgstr "L'alias a bien été supprimé" - -#: apps/note/admin.py:120 apps/note/models/transactions.py:95 +#: 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:54 apps/note/models/transactions.py:108 +#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107 msgid "destination" msgstr "destination" -#: apps/note/apps.py:14 apps/note/models/notes.py:58 -msgid "note" -msgstr "note" - -#: apps/note/forms.py:20 -msgid "New Alias" -msgstr "Nouvel alias" - -#: apps/note/forms.py:25 +#: apps/note/forms.py:14 msgid "select an image" msgstr "Choisissez une image" -#: apps/note/forms.py:26 +#: apps/note/forms.py:15 msgid "Maximal size: 2MB" msgstr "Taille maximale : 2 Mo" @@ -310,7 +408,7 @@ msgstr "" msgid "display image" msgstr "image affichée" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:118 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:117 msgid "created at" msgstr "créée le" @@ -318,10 +416,6 @@ msgstr "créée le" msgid "notes" msgstr "notes" -#: apps/note/models/notes.py:67 -msgid "Note" -msgstr "Note" - #: apps/note/models/notes.py:77 apps/note/models/notes.py:101 msgid "This alias is already taken." msgstr "Cet alias est déjà pris." @@ -368,7 +462,8 @@ msgstr "Alias invalide" msgid "alias" msgstr "alias" -#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37 +#: apps/note/models/notes.py:211 templates/member/club_info.html:33 +#: templates/member/profile_info.html:36 msgid "aliases" msgstr "alias" @@ -380,102 +475,114 @@ msgstr "L'alias est trop long." msgid "An alias with a similar name already exists: {} " msgstr "Un alias avec un nom similaire existe déjà : {}" -#: apps/note/models/notes.py:247 +#: apps/note/models/notes.py:251 msgid "You can't delete your main alias." msgstr "Vous ne pouvez pas supprimer votre alias principal." -#: apps/note/models/transactions.py:31 +#: apps/note/models/transactions.py:30 msgid "transaction category" msgstr "catégorie de transaction" -#: apps/note/models/transactions.py:32 +#: apps/note/models/transactions.py:31 msgid "transaction categories" msgstr "catégories de transaction" -#: apps/note/models/transactions.py:48 +#: apps/note/models/transactions.py:47 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:57 apps/note/models/transactions.py:126 +#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:125 msgid "amount" msgstr "montant" -#: apps/note/models/transactions.py:58 +#: apps/note/models/transactions.py:57 msgid "in centimes" msgstr "en centimes" -#: apps/note/models/transactions.py:76 +#: apps/note/models/transactions.py:75 msgid "transaction template" msgstr "modèle de transaction" -#: apps/note/models/transactions.py:77 +#: apps/note/models/transactions.py:76 msgid "transaction templates" msgstr "modèles de transaction" -#: apps/note/models/transactions.py:101 apps/note/models/transactions.py:114 +#: apps/note/models/transactions.py:100 apps/note/models/transactions.py:113 #: apps/note/tables.py:33 apps/note/tables.py:42 msgid "used alias" msgstr "alias utilisé" -#: apps/note/models/transactions.py:122 +#: apps/note/models/transactions.py:121 msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:115 +#: apps/note/models/transactions.py:129 msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:119 -msgid "valid" -msgstr "valide" +#: apps/note/models/transactions.py:139 apps/note/tables.py:95 +msgid "invalidity reason" +msgstr "Motif d'invalidité" -#: apps/note/models/transactions.py:124 +#: apps/note/models/transactions.py:146 msgid "transaction" msgstr "transaction" -#: apps/note/models/transactions.py:125 +#: apps/note/models/transactions.py:147 msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:168 templates/base.html:98 +#: apps/note/models/transactions.py:201 templates/base.html:84 #: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:145 +#: templates/note/transaction_form.html:140 msgid "Transfer" msgstr "Virement" -#: apps/note/models/transactions.py:188 +#: apps/note/models/transactions.py:221 msgid "Template" msgstr "Bouton" -#: apps/note/models/transactions.py:203 +#: apps/note/models/transactions.py:236 msgid "first_name" msgstr "prénom" -#: apps/note/models/transactions.py:208 +#: apps/note/models/transactions.py:241 msgid "bank" msgstr "banque" -#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 +#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:24 msgid "Credit" msgstr "Crédit" -#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:28 msgid "Debit" msgstr "Débit" -#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 +#: apps/note/models/transactions.py:263 apps/note/models/transactions.py:268 msgid "membership transaction" msgstr "transaction d'adhésion" -#: apps/note/models/transactions.py:231 +#: apps/note/models/transactions.py:264 msgid "membership transactions" msgstr "transactions d'adhésion" -#: apps/note/views.py:39 +#: apps/note/tables.py:57 +msgid "Click to invalidate" +msgstr "Cliquez pour dévalider" + +#: apps/note/tables.py:57 +msgid "Click to validate" +msgstr "Cliquez pour valider" + +#: apps/note/tables.py:93 +msgid "No reason specified" +msgstr "Pas de motif spécifié" + +#: apps/note/views.py:41 msgid "Transfer money" msgstr "Transférer de l'argent" -#: apps/note/views.py:145 templates/base.html:79 +#: apps/note/views.py:102 templates/base.html:79 msgid "Consumptions" msgstr "Consommations" @@ -497,41 +604,35 @@ msgstr "Rang" msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/treasury/apps.py:11 templates/base.html:102 +#: apps/treasury/apps.py:12 templates/base.html:99 msgid "Treasury" msgstr "Trésorerie" -#: apps/treasury/forms.py:56 apps/treasury/forms.py:95 +#: apps/treasury/forms.py:84 apps/treasury/forms.py:132 +#: templates/activity/activity_form.html:9 +#: templates/activity/activity_invite.html:8 #: templates/django_filters/rest_framework/form.html:5 -#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47 +#: templates/member/club_form.html:9 templates/treasury/invoice_form.html:46 msgid "Submit" msgstr "Envoyer" -#: apps/treasury/forms.py:58 +#: apps/treasury/forms.py:86 msgid "Close" msgstr "Fermer" -#: apps/treasury/forms.py:65 +#: apps/treasury/forms.py:95 msgid "Remittance is already closed." msgstr "La remise est déjà fermée." -#: apps/treasury/forms.py:70 +#: apps/treasury/forms.py:100 msgid "You can't change the type of the remittance." msgstr "Vous ne pouvez pas changer le type de la remise." -#: apps/treasury/forms.py:84 -msgid "Last name" -msgstr "Nom de famille" - -#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92 -msgid "First name" -msgstr "Prénom" - -#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 +#: apps/treasury/forms.py:124 templates/note/transaction_form.html:98 msgid "Bank" msgstr "Banque" -#: apps/treasury/forms.py:90 apps/treasury/tables.py:40 +#: apps/treasury/forms.py:126 apps/treasury/tables.py:47 #: templates/note/transaction_form.html:128 #: templates/treasury/remittance_form.html:18 msgid "Amount" @@ -585,10 +686,6 @@ msgstr "Prix unitaire" msgid "Date" msgstr "Date" -#: apps/treasury/models.py:126 -msgid "Type" -msgstr "Type" - #: apps/treasury/models.py:131 msgid "Comment" msgstr "Commentaire" @@ -597,38 +694,38 @@ msgstr "Commentaire" msgid "Closed" msgstr "Fermée" -#: apps/treasury/models.py:159 +#: apps/treasury/models.py:169 msgid "Remittance #{:d}: {}" msgstr "Remise n°{:d} : {}" -#: apps/treasury/models.py:178 apps/treasury/tables.py:64 -#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13 +#: apps/treasury/models.py:188 apps/treasury/tables.py:76 +#: apps/treasury/tables.py:84 templates/treasury/invoice_list.html:13 #: templates/treasury/remittance_list.html:13 msgid "Remittance" msgstr "Remise" -#: apps/treasury/tables.py:16 +#: apps/treasury/tables.py:19 msgid "Invoice #{:d}" msgstr "Facture n°{:d}" -#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10 +#: apps/treasury/tables.py:22 templates/treasury/invoice_list.html:10 #: templates/treasury/remittance_list.html:10 msgid "Invoice" msgstr "Facture" -#: apps/treasury/tables.py:38 +#: apps/treasury/tables.py:45 msgid "Transaction count" msgstr "Nombre de transactions" -#: apps/treasury/tables.py:43 apps/treasury/tables.py:45 +#: apps/treasury/tables.py:50 apps/treasury/tables.py:52 msgid "View" msgstr "Voir" -#: apps/treasury/tables.py:66 +#: apps/treasury/tables.py:78 msgid "Add" msgstr "Ajouter" -#: apps/treasury/tables.py:74 +#: apps/treasury/tables.py:86 msgid "Remove" msgstr "supprimer" @@ -639,30 +736,86 @@ msgid "" "again unless your session expires or you logout." msgstr "" -#: note_kfet/settings/base.py:151 +#: note_kfet/settings/base.py:152 msgid "German" msgstr "" -#: note_kfet/settings/base.py:152 +#: note_kfet/settings/base.py:153 msgid "English" msgstr "" -#: note_kfet/settings/base.py:153 +#: note_kfet/settings/base.py:154 msgid "French" msgstr "" +#: templates/activity/activity_detail.html:29 +msgid "creater" +msgstr "Créateur" + +#: templates/activity/activity_detail.html:50 +msgid "opened" +msgstr "ouvert" + +#: templates/activity/activity_detail.html:57 +msgid "Entry page" +msgstr "Page des entrées" + +#: templates/activity/activity_detail.html:61 +msgid "close" +msgstr "fermer" + +#: templates/activity/activity_detail.html:64 +msgid "invalidate" +msgstr "dévalider" + +#: templates/activity/activity_detail.html:64 +msgid "validate" +msgstr "valider" + +#: templates/activity/activity_detail.html:70 +msgid "Invite" +msgstr "Inviter" + +#: templates/activity/activity_detail.html:77 +msgid "Guests list" +msgstr "Liste des invités" + +#: templates/activity/activity_entry.html:10 +msgid "Return to activity page" +msgstr "Retour à la page de l'activité" + +#: templates/activity/activity_entry.html:18 +msgid "entries" +msgstr "entrées" + +#: templates/activity/activity_entry.html:18 +msgid "entry" +msgstr "entrée" + +#: templates/activity/activity_list.html:5 +msgid "Upcoming activities" +msgstr "Activités à venir" + +#: templates/activity/activity_list.html:10 +msgid "There is no planned activity." +msgstr "Il n'y a pas d'activité prévue." + +#: templates/activity/activity_list.html:14 +msgid "New activity" +msgstr "Nouvelle activité" + +#: templates/activity/activity_list.html:18 +msgid "All activities" +msgstr "Toutes les activités" + #: templates/base.html:13 msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." -#: templates/base.html:87 +#: templates/base.html:89 msgid "Clubs" msgstr "Clubs" -#: templates/base.html:92 -msgid "Activities" -msgstr "Activités" - #: templates/cas_server/base.html:7 msgid "Central Authentication Service" msgstr "" @@ -720,33 +873,49 @@ msgstr "" msgid "Field filters" msgstr "" -#: templates/member/club_detail.html:10 -msgid "Membership starts on" -msgstr "L'adhésion commence le" +#: templates/member/alias_update.html:5 +msgid "Add alias" +msgstr "Ajouter un alias" -#: templates/member/club_detail.html:12 -msgid "Membership ends on" -msgstr "L'adhésion finie le" +#: templates/member/club_info.html:17 +msgid "Club Parent" +msgstr "Club parent" -#: templates/member/club_detail.html:14 -msgid "Membership duration" -msgstr "Durée de l'adhésion" +#: templates/member/club_info.html:41 +msgid "Add member" +msgstr "Ajouter un membre" -#: templates/member/club_detail.html:18 templates/member/profile_detail.html:34 -msgid "balance" -msgstr "solde du compte" +#: templates/member/club_info.html:42 templates/note/conso_form.html:121 +msgid "Edit" +msgstr "Éditer" -#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75 -msgid "Transaction history" -msgstr "Historique des transactions" +#: templates/member/club_info.html:43 +msgid "Add roles" +msgstr "Ajouter des rôles" -#: templates/member/club_form.html:6 -msgid "Clubs list" -msgstr "Liste des clubs" +#: templates/member/club_info.html:46 templates/member/profile_info.html:48 +msgid "View Profile" +msgstr "Voir le profil" #: templates/member/club_list.html:8 -msgid "New club" -msgstr "Nouveau club" +msgid "search clubs" +msgstr "Chercher un club" + +#: templates/member/club_list.html:12 +msgid "Créer un club" +msgstr "" + +#: templates/member/club_list.html:19 +msgid "club listing " +msgstr "Liste des clubs" + +#: templates/member/club_tables.html:9 +msgid "Member of the Club" +msgstr "Membre du club" + +#: templates/member/club_tables.html:22 templates/member/profile_tables.html:22 +msgid "Transaction history" +msgstr "Historique des transactions" #: templates/member/manage_auth_tokens.html:16 msgid "Token" @@ -760,35 +929,31 @@ msgstr "Créé le" msgid "Regenerate token" msgstr "Regénérer le jeton" -#: templates/member/profile_alias.html:10 -msgid "Add alias" -msgstr "Ajouter un alias" +#: templates/member/profile_info.html:5 +msgid "Account #" +msgstr "Compte n°" -#: templates/member/profile_detail.html:15 -msgid "first name" -msgstr "prénom" - -#: templates/member/profile_detail.html:18 +#: templates/member/profile_info.html:17 msgid "username" msgstr "pseudo" -#: templates/member/profile_detail.html:21 +#: templates/member/profile_info.html:20 msgid "password" msgstr "mot de passe" -#: templates/member/profile_detail.html:24 +#: templates/member/profile_info.html:23 msgid "Change password" msgstr "Changer le mot de passe" -#: templates/member/profile_detail.html:42 +#: templates/member/profile_info.html:33 +msgid "balance" +msgstr "solde du compte" + +#: templates/member/profile_info.html:41 msgid "Manage auth token" msgstr "Gérer les jetons d'authentification" -#: templates/member/profile_detail.html:49 -msgid "View Profile" -msgstr "Voir le profil" - -#: templates/member/profile_detail.html:62 +#: templates/member/profile_tables.html:9 msgid "View my memberships" msgstr "Voir mes adhésions" @@ -817,10 +982,6 @@ msgstr "Consommer !" 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 "Consommations simples" @@ -829,7 +990,7 @@ msgstr "Consommations simples" msgid "Double consumptions" msgstr "Consommations doubles" -#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152 +#: templates/note/conso_form.html:141 templates/note/transaction_form.html:147 msgid "Recent transactions history" msgstr "Historique des transactions récentes" @@ -845,37 +1006,21 @@ msgstr "Paiement externe" msgid "Transfer type" msgstr "Type de transfert" -#: templates/note/transaction_form.html:86 -msgid "Name" -msgstr "Nom" - -#: templates/note/transaction_form.html:92 -msgid "First name" -msgstr "Prénom" - -#: templates/note/transaction_form.html:98 -msgid "Bank" -msgstr "Banque" - #: templates/note/transaction_form.html:111 -#: templates/note/transaction_form.html:169 -#: templates/note/transaction_form.html:176 +#: templates/note/transaction_form.html:164 +#: templates/note/transaction_form.html:171 msgid "Select receivers" msgstr "Sélection des destinataires" -#: templates/note/transaction_form.html:128 -msgid "Amount" -msgstr "Montant" - -#: templates/note/transaction_form.html:138 +#: templates/note/transaction_form.html:133 msgid "Reason" msgstr "Raison" -#: templates/note/transaction_form.html:183 +#: templates/note/transaction_form.html:178 msgid "Credit note" msgstr "Note à recharger" -#: templates/note/transaction_form.html:190 +#: templates/note/transaction_form.html:185 msgid "Debit note" msgstr "Note à débiter" @@ -887,6 +1032,10 @@ msgstr "Liste des boutons" msgid "search button" msgstr "Chercher un bouton" +#: templates/note/transactiontemplate_list.html:13 +msgid "New button" +msgstr "Nouveau bouton" + #: templates/note/transactiontemplate_list.html:20 msgid "buttons listing " msgstr "Liste des boutons" @@ -989,11 +1138,11 @@ msgstr "" msgid "Invoices list" msgstr "Liste des factures" -#: templates/treasury/invoice_form.html:42 +#: templates/treasury/invoice_form.html:41 msgid "Add product" msgstr "Ajouter produit" -#: templates/treasury/invoice_form.html:43 +#: templates/treasury/invoice_form.html:42 msgid "Remove product" msgstr "Retirer produit" diff --git a/note_kfet/inputs.py b/note_kfet/inputs.py new file mode 100644 index 00000000..a3170007 --- /dev/null +++ b/note_kfet/inputs.py @@ -0,0 +1,302 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from json import dumps as json_dumps + +from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput + + +class AmountInput(NumberInput): + """ + This input type lets the user type amounts in euros, but forms receive data in cents + """ + template_name = "note/amount_input.html" + + def format_value(self, value): + return None if value is None or value == "" else "{:.02f}".format(value / 100, ) + + def value_from_datadict(self, data, files, name): + val = super().value_from_datadict(data, files, name) + return str(int(100 * float(val))) if val else val + + +class Autocomplete(TextInput): + template_name = "member/autocomplete_model.html" + + def __init__(self, model, attrs=None): + super().__init__(attrs) + + self.model = model + self.model_pk = None + + class Media: + """JS/CSS resources needed to render the date-picker calendar.""" + + js = ('js/autocomplete_model.js', ) + + def format_value(self, value): + if value: + self.attrs["model_pk"] = int(value) + return str(self.model.objects.get(pk=int(value))) + return "" + + +""" +The remaining of this file comes from the project `django-bootstrap-datepicker-plus` available on Github: +https://github.com/monim67/django-bootstrap-datepicker-plus +This is distributed under Apache License 2.0. + +This adds datetime pickers with bootstrap. +""" + +"""Contains Base Date-Picker input class for widgets of this package.""" + + +class DatePickerDictionary: + """Keeps track of all date-picker input classes.""" + + _i = 0 + items = dict() + + @classmethod + def generate_id(cls): + """Return a unique ID for each date-picker input class.""" + cls._i += 1 + return 'dp_%s' % cls._i + + +class BasePickerInput(DateTimeBaseInput): + """Base Date-Picker input class for widgets of this package.""" + + template_name = 'bootstrap_datepicker_plus/date_picker.html' + picker_type = 'DATE' + format = '%Y-%m-%d' + config = {} + _default_config = { + 'id': None, + 'picker_type': None, + 'linked_to': None, + 'options': {} # final merged options + } + options = {} # options extended by user + options_param = {} # options passed as parameter + _default_options = { + 'showClose': True, + 'showClear': True, + 'showTodayButton': True, + "locale": "fr", + } + + # source: https://github.com/tutorcruncher/django-bootstrap3-datetimepicker + # file: /blob/31fbb09/bootstrap3_datetime/widgets.py#L33 + format_map = ( + ('DDD', r'%j'), + ('DD', r'%d'), + ('MMMM', r'%B'), + ('MMM', r'%b'), + ('MM', r'%m'), + ('YYYY', r'%Y'), + ('YY', r'%y'), + ('HH', r'%H'), + ('hh', r'%I'), + ('mm', r'%M'), + ('ss', r'%S'), + ('a', r'%p'), + ('ZZ', r'%z'), + ) + + class Media: + """JS/CSS resources needed to render the date-picker calendar.""" + + js = ( + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/' + 'moment-with-locales.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/' + '4.17.47/js/bootstrap-datetimepicker.min.js', + 'bootstrap_datepicker_plus/js/datepicker-widget.js' + ) + css = {'all': ( + 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/' + '4.17.47/css/bootstrap-datetimepicker.css', + 'bootstrap_datepicker_plus/css/datepicker-widget.css' + ), } + + @classmethod + def format_py2js(cls, datetime_format): + """Convert python datetime format to moment datetime format.""" + for js_format, py_format in cls.format_map: + datetime_format = datetime_format.replace(py_format, js_format) + return datetime_format + + @classmethod + def format_js2py(cls, datetime_format): + """Convert moment datetime format to python datetime format.""" + for js_format, py_format in cls.format_map: + datetime_format = datetime_format.replace(js_format, py_format) + return datetime_format + + def __init__(self, attrs=None, format=None, options=None): + """Initialize the Date-picker widget.""" + self.format_param = format + self.options_param = options if options else {} + self.config = self._default_config.copy() + self.config['id'] = DatePickerDictionary.generate_id() + self.config['picker_type'] = self.picker_type + self.config['options'] = self._calculate_options() + attrs = attrs if attrs else {} + if 'class' not in attrs: + attrs['class'] = 'form-control' + super().__init__(attrs, self._calculate_format()) + + def _calculate_options(self): + """Calculate and Return the options.""" + _options = self._default_options.copy() + _options.update(self.options) + if self.options_param: + _options.update(self.options_param) + return _options + + def _calculate_format(self): + """Calculate and Return the datetime format.""" + _format = self.format_param if self.format_param else self.format + if self.config['options'].get('format'): + _format = self.format_js2py(self.config['options'].get('format')) + else: + self.config['options']['format'] = self.format_py2js(_format) + return _format + + def get_context(self, name, value, attrs): + """Return widget context dictionary.""" + context = super().get_context( + name, value, attrs) + context['widget']['attrs']['dp_config'] = json_dumps(self.config) + return context + + def start_of(self, event_id): + """ + Set Date-Picker as the start-date of a date-range. + + Args: + - event_id (string): User-defined unique id for linking two fields + """ + DatePickerDictionary.items[str(event_id)] = self + return self + + def end_of(self, event_id, import_options=True): + """ + Set Date-Picker as the end-date of a date-range. + + Args: + - event_id (string): User-defined unique id for linking two fields + - import_options (bool): inherit options from start-date input, + default: TRUE + """ + event_id = str(event_id) + if event_id in DatePickerDictionary.items: + linked_picker = DatePickerDictionary.items[event_id] + self.config['linked_to'] = linked_picker.config['id'] + if import_options: + backup_moment_format = self.config['options']['format'] + self.config['options'].update(linked_picker.config['options']) + self.config['options'].update(self.options_param) + if self.format_param or 'format' in self.options_param: + self.config['options']['format'] = backup_moment_format + else: + self.format = linked_picker.format + # Setting useCurrent is necessary, see following issue + # https://github.com/Eonasdan/bootstrap-datetimepicker/issues/1075 + self.config['options']['useCurrent'] = False + self._link_to(linked_picker) + else: + raise KeyError( + 'start-date not specified for event_id "%s"' % event_id) + return self + + def _link_to(self, linked_picker): + """ + Executed when two date-inputs are linked together. + + This method for sub-classes to override to customize the linking. + """ + pass + + +class DatePickerInput(BasePickerInput): + """ + Widget to display a Date-Picker Calendar on a DateField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'DATE' + format = '%Y-%m-%d' + format_key = 'DATE_INPUT_FORMATS' + + +class TimePickerInput(BasePickerInput): + """ + Widget to display a Time-Picker Calendar on a TimeField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'TIME' + format = '%H:%M' + format_key = 'TIME_INPUT_FORMATS' + template_name = 'bootstrap_datepicker_plus/time_picker.html' + + +class DateTimePickerInput(BasePickerInput): + """ + Widget to display a DateTime-Picker Calendar on a DateTimeField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'DATETIME' + format = '%Y-%m-%d %H:%M' + format_key = 'DATETIME_INPUT_FORMATS' + + +class MonthPickerInput(BasePickerInput): + """ + Widget to display a Month-Picker Calendar on a DateField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'MONTH' + format = '01/%m/%Y' + format_key = 'DATE_INPUT_FORMATS' + + +class YearPickerInput(BasePickerInput): + """ + Widget to display a Year-Picker Calendar on a DateField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'YEAR' + format = '01/01/%Y' + format_key = 'DATE_INPUT_FORMATS' + + def _link_to(self, linked_picker): + """Customize the options when linked with other date-time input""" + yformat = self.config['options']['format'].replace('-01-01', '-12-31') + self.config['options']['format'] = yformat \ No newline at end of file diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index d49b2542..61e5ea51 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -48,12 +48,10 @@ INSTALLED_APPS = [ 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.forms', # API 'rest_framework', 'rest_framework.authtoken', - # Autocomplete - 'dal', - 'dal_select2', # Note apps 'activity', @@ -100,6 +98,8 @@ TEMPLATES = [ }, ] +FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' + WSGI_APPLICATION = 'note_kfet.wsgi.application' # Password validation diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 40a9a614..a7afab29 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -15,13 +15,14 @@ urlpatterns = [ # Include project routers path('note/', include('note.urls')), + path('accounts/', include('member.urls')), + path('activity/', include('activity.urls')), path('treasury/', include('treasury.urls')), # Include Django Contrib and Core routers path('i18n/', include('django.conf.urls.i18n')), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), - path('accounts/', include('member.urls')), path('accounts/login/', CustomLoginView.as_view()), path('accounts/', include('django.contrib.auth.urls')), path('api/', include('api.urls')), @@ -42,7 +43,7 @@ if "cas" in settings.INSTALLED_APPS: # Include CAS Client routers path('accounts/login/cas/', cas_views.login, name='cas_login'), path('accounts/logout/cas/', cas_views.logout, name='cas_logout'), - + ] if "debug_toolbar" in settings.INSTALLED_APPS: import debug_toolbar diff --git a/requirements/base.txt b/requirements/base.txt index 6c5fbc4c..9c978ed0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -3,7 +3,6 @@ chardet==3.0.4 defusedxml==0.6.0 Django~=2.2 django-allauth==0.39.1 -django-autocomplete-light==3.5.1 django-crispy-forms==1.7.2 django-extensions==2.1.9 django-filter==2.2.0 diff --git a/static/bootstrap_datepicker_plus/css/datepicker-widget.css b/static/bootstrap_datepicker_plus/css/datepicker-widget.css new file mode 100644 index 00000000..baeec507 --- /dev/null +++ b/static/bootstrap_datepicker_plus/css/datepicker-widget.css @@ -0,0 +1,121 @@ +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot'); + src: url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), + url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'), + url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'), + url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'), + url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} + +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.glyphicon-time:before { + content: "\e023"; +} + +.glyphicon-chevron-left:before { + content: "\e079"; +} + +.glyphicon-chevron-right:before { + content: "\e080"; +} + +.glyphicon-chevron-up:before { + content: "\e113"; +} + +.glyphicon-chevron-down:before { + content: "\e114"; +} + +.glyphicon-calendar:before { + content: "\e109"; +} + +.glyphicon-screenshot:before { + content: "\e087"; +} + +.glyphicon-trash:before { + content: "\e020"; +} + +.glyphicon-remove:before { + content: "\e014"; +} + +.bootstrap-datetimepicker-widget .btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} + +.bootstrap-datetimepicker-widget.dropdown-menu { + position: absolute; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); +} + +.bootstrap-datetimepicker-widget .list-unstyled { + padding-left: 0; + list-style: none; +} + +.bootstrap-datetimepicker-widget .collapse { + display: none; +} + +.bootstrap-datetimepicker-widget .collapse.in { + display: block; +} + +/* fix for bootstrap4 */ +.bootstrap-datetimepicker-widget .table-condensed > thead > tr > th, +.bootstrap-datetimepicker-widget .table-condensed > tbody > tr > td, +.bootstrap-datetimepicker-widget .table-condensed > tfoot > tr > td { + padding: 5px; +} diff --git a/static/bootstrap_datepicker_plus/js/datepicker-widget.js b/static/bootstrap_datepicker_plus/js/datepicker-widget.js new file mode 100644 index 00000000..2288b46b --- /dev/null +++ b/static/bootstrap_datepicker_plus/js/datepicker-widget.js @@ -0,0 +1,55 @@ +jQuery(function ($) { + var datepickerDict = {}; + var isBootstrap4 = $.fn.collapse.Constructor.VERSION.split('.').shift() == "4"; + function fixMonthEndDate(e, picker) { + e.date && picker.val().length && picker.val(e.date.endOf('month').format('YYYY-MM-DD')); + } + $("[dp_config]:not([disabled])").each(function (i, element) { + var $element = $(element), data = {}; + try { + data = JSON.parse($element.attr('dp_config')); + } + catch (x) { } + if (data.id && data.options) { + data.$element = $element.datetimepicker(data.options); + data.datepickerdata = $element.data("DateTimePicker"); + datepickerDict[data.id] = data; + data.$element.next('.input-group-addon').on('click', function(){ + data.datepickerdata.show(); + }); + if(isBootstrap4){ + data.$element.on("dp.show", function (e) { + $('.collapse.in').addClass('show'); + }); + } + } + }); + $.each(datepickerDict, function (id, to_picker) { + if (to_picker.linked_to) { + var from_picker = datepickerDict[to_picker.linked_to]; + from_picker.datepickerdata.maxDate(to_picker.datepickerdata.date() || false); + to_picker.datepickerdata.minDate(from_picker.datepickerdata.date() || false); + from_picker.$element.on("dp.change", function (e) { + to_picker.datepickerdata.minDate(e.date || false); + }); + to_picker.$element.on("dp.change", function (e) { + if (to_picker.picker_type == 'MONTH') fixMonthEndDate(e, to_picker.$element); + from_picker.datepickerdata.maxDate(e.date || false); + }); + if (to_picker.picker_type == 'MONTH') { + to_picker.$element.on("dp.hide", function (e) { + fixMonthEndDate(e, to_picker.$element); + }); + fixMonthEndDate({ date: to_picker.datepickerdata.date() }, to_picker.$element); + } + } + }); + if(isBootstrap4) { + $('body').on('show.bs.collapse','.bootstrap-datetimepicker-widget .collapse',function(e){ + $(e.target).addClass('in'); + }); + $('body').on('hidden.bs.collapse','.bootstrap-datetimepicker-widget .collapse',function(e){ + $(e.target).removeClass('in'); + }); + } +}); diff --git a/static/js/autocomplete_model.js b/static/js/autocomplete_model.js new file mode 100644 index 00000000..e2a3f0cb --- /dev/null +++ b/static/js/autocomplete_model.js @@ -0,0 +1,34 @@ +$(document).ready(function () { + $(".autocomplete").keyup(function(e) { + let target = $("#" + e.target.id); + let prefix = target.attr("id"); + let api_url = target.attr("api_url"); + let api_url_suffix = target.attr("api_url_suffix"); + if (!api_url_suffix) + api_url_suffix = ""; + let name_field = target.attr("name_field"); + if (!name_field) + name_field = "name"; + let input = target.val(); + + $.getJSON(api_url + "?format=json&search=^" + input + api_url_suffix, function(objects) { + let html = ""; + + objects.results.forEach(function (obj) { + html += li(prefix + "_" + obj.id, obj[name_field]); + }); + + $("#" + prefix + "_list").html(html); + + objects.results.forEach(function (obj) { + $("#" + prefix + "_" + obj.id).click(function() { + target.val(obj[name_field]); + $("#" + prefix + "_pk").val(obj.id); + }); + + if (input === obj[name_field]) + $("#" + prefix + "_pk").val(obj.id); + }); + }); + }); +}); \ No newline at end of file diff --git a/static/js/base.js b/static/js/base.js index d21bd433..22d1366a 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -28,15 +28,35 @@ function addMsg(msg, alert_type) { + msg + "\n"; msgDiv.html(html); } + /** * add Muliple error message from err_obj - * @param err_obj {error_code:erro_message} + * @param errs_obj [{error_code:erro_message}] */ function errMsg(errs_obj){ for (const err_msg of Object.values(errs_obj)) { addMsg(err_msg,'danger'); } } + +var reloadWithTurbolinks = (function () { + var scrollPosition; + + function reload () { + scrollPosition = [window.scrollX, window.scrollY]; + Turbolinks.visit(window.location.toString(), { action: 'replace' }) + } + + document.addEventListener('turbolinks:load', function () { + if (scrollPosition) { + window.scrollTo.apply(window, scrollPosition); + scrollPosition = null + } + }); + + return reload; +})(); + /** * Reload the balance of the user on the right top corner */ diff --git a/templates/activity/activity_detail.html b/templates/activity/activity_detail.html new file mode 100644 index 00000000..0ed3c719 --- /dev/null +++ b/templates/activity/activity_detail.html @@ -0,0 +1,139 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load render_table from django_tables2 %} +{% load pretty_money %} +{% load perms %} + +{% block content %} + +
+
+

{{ activity.name }}

+
+
+
+
{% trans 'description'|capfirst %}
+
{{ activity.description }}
+ +
{% trans 'type'|capfirst %}
+
{{ activity.activity_type }}
+ +
{% trans 'start date'|capfirst %}
+
{{ activity.date_start }}
+ +
{% trans 'end date'|capfirst %}
+
{{ activity.date_end }}
+ + {% if "view_"|has_perm:activity.creater %} +
{% trans 'creater'|capfirst %}
+
{{ activity.creater }}
+ {% endif %} + +
{% trans 'organizer'|capfirst %}
+
{{ activity.organizer }}
+ +
{% trans 'attendees club'|capfirst %}
+
{{ activity.attendees_club }}
+ +
{% trans 'can invite'|capfirst %}
+
{{ activity.activity_type.can_invite|yesno }}
+ + {% if activity.activity_type.can_invite %} +
{% trans 'guest entry fee'|capfirst %}
+
{{ activity.activity_type.guest_entry_fee|pretty_money }}
+ {% endif %} + +
{% trans 'valid'|capfirst %}
+
{{ activity.valid|yesno }}
+ +
{% trans 'opened'|capfirst %}
+
{{ activity.open|yesno }}
+
+
+ + +
+ + {% if guests.data %} +
+

{% trans "Guests list" %}

+
+ {% render_table guests %} +
+ {% endif %} + +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/activity/activity_entry.html b/templates/activity/activity_entry.html new file mode 100644 index 00000000..c06a5188 --- /dev/null +++ b/templates/activity/activity_entry.html @@ -0,0 +1,126 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load render_table from django_tables2 %} +{% load pretty_money %} +{% load perms %} + +{% block content %} + + + + + + +
+ +
+

{{ entries.count }} {% if entries.count >= 2 %}{% trans "entries" %}{% else %}{% trans "entry" %}{% endif %}

+ {% render_table table %} +
+{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/activity/activity_form.html b/templates/activity/activity_form.html new file mode 100644 index 00000000..99c254e3 --- /dev/null +++ b/templates/activity/activity_form.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load crispy_forms_tags %} +{% block content %} +
+{% csrf_token %} +{{form|crispy}} + +
+{% endblock %} diff --git a/templates/activity/activity_invite.html b/templates/activity/activity_invite.html new file mode 100644 index 00000000..8bdb1965 --- /dev/null +++ b/templates/activity/activity_invite.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load i18n crispy_forms_tags %} +{% block content %} +
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/activity/activity_list.html b/templates/activity/activity_list.html new file mode 100644 index 00000000..f2871fd6 --- /dev/null +++ b/templates/activity/activity_list.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load i18n crispy_forms_tags%} +{% block content %} +

{% trans "Upcoming activities" %}

+ {% if upcoming.data %} + {% render_table upcoming %} + {% else %} +
+ {% trans "There is no planned activity." %} +
+ {% endif %} + + {% trans 'New activity' %} + +
+ +

{% trans "All activities" %}

+ + {% render_table table %} +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 6a688fc9..26903c2f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -91,7 +91,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} {% if "activity.activity"|not_empty_model_list %} {% endif %} {% if "treasury.invoice"|not_empty_model_change_list %} diff --git a/templates/bootstrap_datepicker_plus/date_picker.html b/templates/bootstrap_datepicker_plus/date_picker.html new file mode 100644 index 00000000..67a11df1 --- /dev/null +++ b/templates/bootstrap_datepicker_plus/date_picker.html @@ -0,0 +1,6 @@ +
+ {% include "bootstrap_datepicker_plus/input.html" %} +
+
+
+
diff --git a/templates/bootstrap_datepicker_plus/input.html b/templates/bootstrap_datepicker_plus/input.html new file mode 100644 index 00000000..b2f8c403 --- /dev/null +++ b/templates/bootstrap_datepicker_plus/input.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/templates/bootstrap_datepicker_plus/time_picker.html b/templates/bootstrap_datepicker_plus/time_picker.html new file mode 100644 index 00000000..2bd509a3 --- /dev/null +++ b/templates/bootstrap_datepicker_plus/time_picker.html @@ -0,0 +1,6 @@ +
+ {% include "bootstrap_datepicker_plus/input.html" %} +
+
+
+
diff --git a/templates/member/autocomplete_model.html b/templates/member/autocomplete_model.html new file mode 100644 index 00000000..2236c6ef --- /dev/null +++ b/templates/member/autocomplete_model.html @@ -0,0 +1,9 @@ + + +
    +
diff --git a/templates/member/club_info.html b/templates/member/club_info.html index 539d9867..1c8e8661 100644 --- a/templates/member/club_info.html +++ b/templates/member/club_info.html @@ -13,8 +13,10 @@
{% trans 'name'|capfirst %}
{{ club.name}}
-
{% trans 'Club Parent'|capfirst %}
-
{{ club.parent_club.name}}
+ {% if club.parent_club %} +
{% trans 'Club Parent'|capfirst %}
+
{{ club.parent_club.name}}
+ {% endif %}
{% trans 'membership start'|capfirst %}
{{ club.membership_start }}
diff --git a/templates/note/amount_input.html b/templates/note/amount_input.html new file mode 100644 index 00000000..6ef4a53a --- /dev/null +++ b/templates/note/amount_input.html @@ -0,0 +1,11 @@ +
+ +
+ +
+
\ No newline at end of file diff --git a/templates/note/transaction_form.html b/templates/note/transaction_form.html index d2cd85e9..65aaa635 100644 --- a/templates/note/transaction_form.html +++ b/templates/note/transaction_form.html @@ -126,12 +126,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
-
- -
- -
-
+ {% include "note/amount_input.html" with widget=amount_widget %}
diff --git a/templates/treasury/invoice_form.html b/templates/treasury/invoice_form.html index 0edcbdcd..2875d410 100644 --- a/templates/treasury/invoice_form.html +++ b/templates/treasury/invoice_form.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load static %} {% load i18n %} -{% load crispy_forms_tags pretty_money %} +{% load crispy_forms_tags %} {% block content %}

{% trans "Invoices list" %}

@@ -26,18 +26,8 @@ {% endif %} {{ form.designation }} - {{ form.quantity }} - - {# Use custom input for amount, with the € symbol #} -
- -
- -
-
- + {{ form.quantity }} + {{ form.amount }} {# These fields are hidden but handled by the formset to link the id and the invoice id #} {{ form.invoice }} {{ form.id }} @@ -64,15 +54,7 @@ {{ formset.empty_form.designation }} {{ formset.empty_form.quantity }} - -
- -
- -
-
- + {{ formset.empty_form.amount }} {{ formset.empty_form.invoice }} {{ formset.empty_form.id }}