Merge branch 'activity' into 'master'

Activity management

See merge request bde/nk20!32
This commit is contained in:
ynerant 2020-03-30 22:43:12 +02:00
commit c8854cf45d
48 changed files with 2477 additions and 626 deletions

View File

@ -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__'

View File

@ -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)

View File

@ -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', ]

84
apps/activity/forms.py Normal file
View File

@ -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 ...',
},
),
}

View File

@ -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')

108
apps/activity/tables.py Normal file
View File

@ -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 + " <em>aka.</em> " + 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,
}

17
apps/activity/urls.py Normal file
View File

@ -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('<int:pk>/', views.ActivityDetailView.as_view(), name='activity_detail'),
path('<int:pk>/invite/', views.ActivityInviteView.as_view(), name='activity_invite'),
path('<int:pk>/entry/', views.ActivityEntryView.as_view(), name='activity_entry'),
path('<int:pk>/update/', views.ActivityUpdateView.as_view(), name='activity_update'),
path('new/', views.ActivityCreateView.as_view(), name='activity_create'),
]

153
apps/activity/views.py Normal file
View File

@ -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

View File

@ -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 ...',
},
),
}

View File

@ -21,6 +21,4 @@ urlpatterns = [
path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
path('user/<int:pk>/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"),
]

View File

@ -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)

View File

@ -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

View File

@ -8,7 +8,6 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
from .serializers import NotePolymorphicSerializer, AliasSerializer, 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.

View File

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

View File

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

View File

@ -2,7 +2,6 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models
from django.db.models import F
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import gettext_lazy as _

View File

@ -106,9 +106,8 @@ DELETE_TEMPLATE = """
class AliasTable(tables.Table):
class Meta:
attrs = {
'class':
'table table condensed table-striped table-hover',
'id':"alias_table"
'class': 'table table condensed table-striped table-hover',
'id': "alias_table"
}
model = Alias
fields = ('name',)
@ -118,9 +117,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)

View File

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

View File

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

View File

@ -1,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
<Alias> (aka. <Username> )
"""
# Gère l'affichage de l'alias dans la recherche
res = result.name
note_name = str(result.note)
if res != note_name:
res += " (aka. " + note_name + ")"
return res
def get_result_value(self, result):
"""
The value used for the transactions will be the id of the Note.
"""
return str(result.note.pk)
class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
"""
Create TransactionTemplate

View File

@ -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
]
}
}
]

View File

@ -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)

View File

@ -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,
)

View File

@ -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():

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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 ""

View File

@ -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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\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"

302
note_kfet/inputs.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}

View File

@ -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');
});
}
});

View File

@ -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);
});
});
});
});

View File

@ -28,15 +28,35 @@ function addMsg(msg, alert_type) {
+ msg + "</div>\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
*/

View File

@ -0,0 +1,139 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% load pretty_money %}
{% load perms %}
{% block content %}
<div id="activity_info" class="card bg-light shadow">
<div class="card-header text-center">
<h4>{{ activity.name }}</h4>
</div>
<div class="card-body" id="profile_infos">
<dl class="row">
<dt class="col-xl-6">{% trans 'description'|capfirst %}</dt>
<dd class="col-xl-6"> {{ activity.description }}</dd>
<dt class="col-xl-6">{% trans 'type'|capfirst %}</dt>
<dd class="col-xl-6"> {{ activity.activity_type }}</dd>
<dt class="col-xl-6">{% trans 'start date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_start }}</dd>
<dt class="col-xl-6">{% trans 'end date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_end }}</dd>
{% if "view_"|has_perm:activity.creater %}
<dt class="col-xl-6">{% trans 'creater'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:user_detail" pk=activity.creater.pk %}">{{ activity.creater }}</a></dd>
{% endif %}
<dt class="col-xl-6">{% trans 'organizer'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:club_detail" pk=activity.organizer.pk %}">{{ activity.organizer }}</a></dd>
<dt class="col-xl-6">{% trans 'attendees club'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:club_detail" pk=activity.attendees_club.pk %}">{{ activity.attendees_club }}</a></dd>
<dt class="col-xl-6">{% trans 'can invite'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.activity_type.can_invite|yesno }}</dd>
{% if activity.activity_type.can_invite %}
<dt class="col-xl-6">{% trans 'guest entry fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.activity_type.guest_entry_fee|pretty_money }}</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'valid'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.valid|yesno }}</dd>
<dt class="col-xl-6">{% trans 'opened'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.open|yesno }}</dd>
</dl>
</div>
<div class="card-footer text-center">
{% if activity.open and "change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a>
{% endif %}
{% if activity.valid and "change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a>
{% endif %}
{% if not activity.open and "change__valid"|has_perm:activity %}
<a class="btn btn-success btn-sm my-1" id="validate_activity"> {% if activity.valid %}{% trans "invalidate"|capfirst %}{% else %}{% trans "validate"|capfirst %}{% endif %}</a>
{% endif %}
{% if "view_"|has_perm:activity %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}"> {% trans "edit"|capfirst %}</a>
{% endif %}
{% if activity.activity_type.can_invite and not activity_started %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_invite' pk=activity.pk %}"> {% trans "Invite" %}</a>
{% endif %}
</div>
</div>
{% if guests.data %}
<hr>
<h2>{% trans "Guests list" %}</h2>
<div id="guests_table">
{% render_table guests %}
</div>
{% endif %}
{% endblock %}
{% block extrajavascript %}
<script>
function remove_guest(guest_id) {
$.ajax({
url:"/api/activity/guest/" + guest_id + "/",
method:"DELETE",
headers: {"X-CSRFTOKEN": CSRF_TOKEN}
})
.done(function() {
addMsg('Invité supprimé','success');
$("#guests_table").load(location.href + " #guests_table");
})
.fail(function(xhr, textStatus, error) {
errMsg(xhr.responseJSON);
});
}
$("#open_activity").click(function() {
$.ajax({
url: "/api/activity/activity/{{ activity.pk }}/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
open: {{ activity.open|yesno:'false,true' }}
}
}).done(function () {
reloadWithTurbolinks();
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
});
$("#validate_activity").click(function () {
console.log(42);
$.ajax({
url: "/api/activity/activity/{{ activity.pk }}/",
type: "PATCH",
dataType: "json",
headers: {
"X-CSRFTOKEN": CSRF_TOKEN
},
data: {
valid: {{ activity.valid|yesno:'false,true' }}
}
}).done(function () {
reloadWithTurbolinks();
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,126 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% load pretty_money %}
{% load perms %}
{% block content %}
<a href="{% url "activity:activity_detail" pk=activity.pk %}">
<button class="btn btn-light">{% trans "Return to activity page" %}</button>
</a>
<input id="alias" type="text" class="form-control" placeholder="Nom/note ...">
<hr>
<div id="entry_table">
<h2 class="text-center">{{ entries.count }} {% if entries.count >= 2 %}{% trans "entries" %}{% else %}{% trans "entry" %}{% endif %}</h2>
{% render_table table %}
</div>
{% endblock %}
{% block extrajavascript %}
<script>
old_pattern = null;
alias_obj = $("#alias");
function reloadTable(force=false) {
let pattern = alias_obj.val();
if ((pattern === old_pattern || pattern === "") && !force)
return;
$("#entry_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #entry_table", init);
refreshBalance();
}
alias_obj.keyup(reloadTable);
$(document).ready(init);
function init() {
$(".table-row").click(function(e) {
let target = e.target.parentElement;
target = $("#" + target.id);
let type = target.attr("data-type");
let id = target.attr("data-id");
let last_name = target.attr("data-last-name");
let first_name = target.attr("data-first-name");
if (type === "membership") {
$.post("/api/activity/entry/?format=json", {
csrfmiddlewaretoken: CSRF_TOKEN,
activity: {{ activity.id }},
note: id,
guest: null
}).done(function () {
addMsg("Entrée effectuée !", "success");
reloadTable(true);
}).fail(function(xhr) {
errMsg(xhr.responseJSON);
});
}
else {
let line_obj = $("#buttons_guest_" + id);
if (line_obj.length || target.attr('class').includes("table-success")) {
line_obj.remove();
return;
}
let tr = "<tr class='text-center'>" +
"<td id='buttons_guest_" + id + "' style='table-danger center' colspan='5'>" +
"<button id='transaction_guest_" + id + "' class='btn btn-secondary'>Payer avec la note de l'hôte</button> " +
"<button id='transaction_guest_" + id + "_especes' class='btn btn-secondary'>Payer en espèces</button> " +
"<button id='transaction_guest_" + id + "_cb' class='btn btn-secondary'>Payer en CB</button></td>" +
"<tr>";
$(tr).insertAfter(target);
let makeTransaction = function() {
$.post("/api/activity/entry/?format=json", {
csrfmiddlewaretoken: CSRF_TOKEN,
activity: {{ activity.id }},
note: target.attr("data-inviter"),
guest: id
}).done(function () {
addMsg("Entrée effectuée !", "success");
reloadTable(true);
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
};
let credit = function(credit_id, credit_name) {
return function() {
$.post("/api/note/transaction/transaction/",
{
"csrfmiddlewaretoken": CSRF_TOKEN,
"quantity": 1,
"amount": {{ activity.activity_type.guest_entry_fee }},
"reason": "Crédit " + credit_name + " (invitation {{ activity.name }})",
"valid": true,
"polymorphic_ctype": {{ notespecial_ctype }},
"resourcetype": "SpecialTransaction",
"source": credit_id,
"destination": target.attr('data-inviter'),
"last_name": last_name,
"first_name": first_name,
"bank": ""
}).done(function () {
makeTransaction();
reset();
}).fail(function (xhr) {
errMsg(xhr.responseJSON);
});
};
};
$("#transaction_guest_" + id).click(makeTransaction);
$("#transaction_guest_" + id + "_especes").click(credit(1, "espèces"));
$("#transaction_guest_" + id + "_cb").click(credit(2, "carte bancaire"));
}
});
}
</script>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
<form method="post">
{% csrf_token %}
{{form|crispy}}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load render_table from django_tables2 %}
{% load i18n crispy_forms_tags %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
</script>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% load render_table from django_tables2 %}
{% load i18n crispy_forms_tags%}
{% block content %}
<h2>{% trans "Upcoming activities" %}</h2>
{% if upcoming.data %}
{% render_table upcoming %}
{% else %}
<div class="alert alert-warning">
{% trans "There is no planned activity." %}
</div>
{% endif %}
<a class="btn btn-primary" href="{% url 'activity:activity_create' %}">{% trans 'New activity' %}</a>
<hr>
<h2>{% trans "All activities" %}</h2>
{% render_table table %}
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(document).ready(function($) {
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
});
</script>
{% endblock %}

View File

@ -91,7 +91,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
{% endif %}
{% if "activity.activity"|not_empty_model_list %}
<li class="nav-item active">
<a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
<a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a>
</li>
{% endif %}
{% if "treasury.invoice"|not_empty_model_change_list %}

View File

@ -0,0 +1,6 @@
<div class="input-group date">
{% include "bootstrap_datepicker_plus/input.html" %}
<div class="input-group-addon input-group-append" data-target="#datetimepicker1" data-toggle="datetimepickerv">
<div class="input-group-text"><i class="glyphicon glyphicon-calendar"></i></div>
</div>
</div>

View File

@ -0,0 +1,4 @@
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None and widget.value != "" %}
value="{{ widget.value }}"{% endif %}{% for name, value in widget.attrs.items %}{% ifnotequal value False %}
{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}
{% endifnotequal %}{% endfor %}/>

View File

@ -0,0 +1,6 @@
<div class="input-group date">
{% include "bootstrap_datepicker_plus/input.html" %}
<div class="input-group-addon input-group-append" data-target="#datetimepicker1" data-toggle="datetimepickerv">
<div class="input-group-text"><i class="glyphicon glyphicon-time"></i></div>
</div>
</div>

View File

@ -0,0 +1,9 @@
<input type="hidden" name="{{ widget.name }}" {% if widget.attrs.model_pk %}value="{{ widget.attrs.model_pk }}"{% endif %} id="{{ widget.attrs.id }}_pk">
<input type="text"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
name="{{ widget.name }}_name" autocomplete="off"
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
<ul class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
</ul>

View File

@ -13,8 +13,10 @@
<dt class="col-xl-6">{% trans 'name'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.name}}</dd>
<dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt>
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
{% if club.parent_club %}
<dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt>
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_start }}</dd>

View File

@ -0,0 +1,11 @@
<div class="input-group">
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
name="{{ widget.name }}"
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>

View File

@ -126,12 +126,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<div class="form-row">
<div class="form-group col-md-6">
<label for="amount">{% trans "Amount" %} :</label>
<div class="input-group">
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01" id="amount" />
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>
{% include "note/amount_input.html" with widget=amount_widget %}
</div>
<div class="form-group col-md-6">

View File

@ -1,7 +1,7 @@
{% extends "base.html" %}
{% load static %}
{% load i18n %}
{% load crispy_forms_tags pretty_money %}
{% load crispy_forms_tags %}
{% block content %}
<p><a class="btn btn-default" href="{% url 'treasury:invoice_list' %}">{% trans "Invoices list" %}</a></p>
<form method="post" action="">
@ -26,18 +26,8 @@
{% endif %}
<tr class="row-formset">
<td>{{ form.designation }}</td>
<td>{{ form.quantity }} </td>
<td>
{# Use custom input for amount, with the € symbol #}
<div class="input-group">
<input type="number" name="product_set-{{ forloop.counter0 }}-amount" step="0.01"
id="id_product_set-{{ forloop.counter0 }}-amount"
value="{{ form.instance.amount|cents_to_euros }}">
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>
</td>
<td>{{ form.quantity }}</td>
<td>{{ form.amount }}</td>
{# 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 @@
<tr class="row-formset">
<td>{{ formset.empty_form.designation }}</td>
<td>{{ formset.empty_form.quantity }} </td>
<td>
<div class="input-group">
<input type="number" name="product_set-__prefix__-amount" step="0.01"
id="id_product_set-__prefix__-amount">
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>
</td>
<td>{{ formset.empty_form.amount }}</td>
{{ formset.empty_form.invoice }}
{{ formset.empty_form.id }}
</tr>