Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • bde/nk20
  • mcngnt/nk20
2 results
Show changes
Showing
with 1790 additions and 192 deletions
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
class NoteSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Notes.
The djangorestframework plugin will analyse the model `Note` and parse all fields in the API.
"""
class Meta:
model = Note
fields = '__all__'
extra_kwargs = {
'url': {
'view_name': 'project-detail',
'lookup_field': 'pk'
},
}
class NoteClubSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Club's notes.
The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API.
"""
class Meta:
model = NoteClub
fields = '__all__'
class NoteSpecialSerializer(serializers.ModelSerializer):
"""
REST API Serializer for special notes.
The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API.
"""
class Meta:
model = NoteSpecial
fields = '__all__'
class NoteUserSerializer(serializers.ModelSerializer):
"""
REST API Serializer for User's notes.
The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API.
"""
class Meta:
model = NoteUser
fields = '__all__'
class AliasSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Aliases.
The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API.
"""
class Meta:
model = Alias
fields = '__all__'
class NotePolymorphicSerializer(PolymorphicSerializer):
model_serializer_mapping = {
Note: NoteSerializer,
NoteUser: NoteUserSerializer,
NoteClub: NoteClubSerializer,
NoteSpecial: NoteSpecialSerializer
}
class TransactionTemplateSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Transaction templates.
The djangorestframework plugin will analyse the model `TransactionTemplate` and parse all fields in the API.
"""
class Meta:
model = TransactionTemplate
fields = '__all__'
class TransactionSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Transactions.
The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API.
"""
class Meta:
model = Transaction
fields = '__all__'
class MembershipTransactionSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Membership transactions.
The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API.
"""
class Meta:
model = MembershipTransaction
fields = '__all__'
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import NotePolymorphicViewSet, AliasViewSet, \
TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet
def register_note_urls(router, path):
"""
Configure router for Note REST API.
"""
router.register(path + '/note', NotePolymorphicViewSet)
router.register(path + '/alias', AliasViewSet)
router.register(path + '/transaction/transaction', TransactionViewSet)
router.register(path + '/transaction/template', TransactionTemplateViewSet)
router.register(path + '/transaction/membership', MembershipTransactionViewSet)
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db.models import Q
from rest_framework import viewsets
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
NoteUserSerializer, AliasSerializer, \
TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer
class NoteViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer,
then render it on /api/note/note/
"""
queryset = Note.objects.all()
serializer_class = NoteSerializer
class NoteClubViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer,
then render it on /api/note/club/
"""
queryset = NoteClub.objects.all()
serializer_class = NoteClubSerializer
class NoteSpecialViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer,
then render it on /api/note/special/
"""
queryset = NoteSpecial.objects.all()
serializer_class = NoteSpecialSerializer
class NoteUserViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer,
then render it on /api/note/user/
"""
queryset = NoteUser.objects.all()
serializer_class = NoteUserSerializer
class NotePolymorphicViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
then render it on /api/note/note/
"""
queryset = Note.objects.all()
serializer_class = NotePolymorphicSerializer
def get_queryset(self):
"""
Parse query and apply filters.
:return: The filtered set of requested notes
"""
queryset = Note.objects.all()
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
Q(alias__name__regex=alias)
| Q(alias__normalized_name__regex=alias.lower()))
note_type = self.request.query_params.get("type", None)
if note_type:
types = str(note_type).lower()
if "user" in types:
queryset = queryset.filter(polymorphic_ctype__model="noteuser")
elif "club" in types:
queryset = queryset.filter(polymorphic_ctype__model="noteclub")
elif "special" in types:
queryset = queryset.filter(
polymorphic_ctype__model="notespecial")
else:
queryset = queryset.none()
return queryset
class AliasViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
then render it on /api/aliases/
"""
queryset = Alias.objects.all()
serializer_class = AliasSerializer
def get_queryset(self):
"""
Parse query and apply filters.
:return: The filtered set of requested aliases
"""
queryset = Alias.objects.all()
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(
Q(name__regex=alias) | Q(normalized_name__regex=alias.lower()))
note_id = self.request.query_params.get("note", None)
if note_id:
queryset = queryset.filter(id=note_id)
note_type = self.request.query_params.get("type", None)
if note_type:
types = str(note_type).lower()
if "user" in types:
queryset = queryset.filter(
note__polymorphic_ctype__model="noteuser")
elif "club" in types:
queryset = queryset.filter(
note__polymorphic_ctype__model="noteclub")
elif "special" in types:
queryset = queryset.filter(
note__polymorphic_ctype__model="notespecial")
else:
queryset = queryset.none()
return queryset
class TransactionTemplateViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/template/
"""
queryset = TransactionTemplate.objects.all()
serializer_class = TransactionTemplateSerializer
class TransactionViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/transaction/
"""
queryset = Transaction.objects.all()
serializer_class = TransactionSerializer
class MembershipTransactionViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `MembershipTransaction` objects, serialize it to JSON with the given serializer,
then render it on /api/note/transaction/membership/
"""
queryset = MembershipTransaction.objects.all()
serializer_class = MembershipTransactionSerializer
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig
......@@ -20,9 +19,9 @@ class NoteConfig(AppConfig):
"""
post_save.connect(
signals.save_user_note,
sender=settings.AUTH_USER_MODEL
sender=settings.AUTH_USER_MODEL,
)
post_save.connect(
signals.save_club_note,
sender='member.Club'
sender='member.Club',
)
[
{
"model": "note.note",
"pk": 1,
"fields": {
"polymorphic_ctype": 22,
"balance": 0,
"is_active": true,
"display_image": "",
"created_at": "2020-02-20T20:02:48.778Z"
}
},
{
"model": "note.note",
"pk": 2,
"fields": {
"polymorphic_ctype": 22,
"balance": 0,
"is_active": true,
"display_image": "",
"created_at": "2020-02-20T20:06:39.546Z"
}
},
{
"model": "note.note",
"pk": 3,
"fields": {
"polymorphic_ctype": 22,
"balance": 0,
"is_active": true,
"display_image": "",
"created_at": "2020-02-20T20:06:43.049Z"
}
},
{
"model": "note.note",
"pk": 4,
"fields": {
"polymorphic_ctype": 22,
"balance": 0,
"is_active": true,
"display_image": "",
"created_at": "2020-02-20T20:06:50.996Z"
}
},
{
"model": "note.note",
"pk": 5,
"fields": {
"polymorphic_ctype": 21,
"balance": 0,
"is_active": true,
"display_image": "",
"created_at": "2020-02-20T20:09:38.615Z"
}
},
{
"model": "note.note",
"pk": 6,
"fields": {
"polymorphic_ctype": 21,
"balance": 0,
"is_active": true,
"display_image": "",
"created_at": "2020-02-20T20:16:14.753Z"
}
},
{
"model": "note.notespecial",
"pk": 1,
"fields": {
"special_type": "Esp\u00e8ces"
}
},
{
"model": "note.notespecial",
"pk": 2,
"fields": {
"special_type": "Carte bancaire"
}
},
{
"model": "note.notespecial",
"pk": 3,
"fields": {
"special_type": "Ch\u00e8que"
}
},
{
"model": "note.notespecial",
"pk": 4,
"fields": {
"special_type": "Virement bancaire"
}
},
{
"model": "note.noteclub",
"pk": 5,
"fields": {
"club": 1
}
},
{
"model": "note.noteclub",
"pk": 6,
"fields": {
"club": 2
}
},
{
"model": "note.alias",
"pk": 1,
"fields": {
"name": "Esp\u00e8ces",
"normalized_name": "especes",
"note": 1
}
},
{
"model": "note.alias",
"pk": 2,
"fields": {
"name": "Carte bancaire",
"normalized_name": "cartebancaire",
"note": 2
}
},
{
"model": "note.alias",
"pk": 3,
"fields": {
"name": "Ch\u00e8que",
"normalized_name": "cheque",
"note": 3
}
},
{
"model": "note.alias",
"pk": 4,
"fields": {
"name": "Virement bancaire",
"normalized_name": "virementbancaire",
"note": 4
}
},
{
"model": "note.alias",
"pk": 5,
"fields": {
"name": "BDE",
"normalized_name": "bde",
"note": 5
}
},
{
"model": "note.alias",
"pk": 6,
"fields": {
"name": "Kfet",
"normalized_name": "kfet",
"note": 6
}
},
{
"model": "note.templatecategory",
"pk": 1,
"fields": {
"name": "Soft"
}
},
{
"model": "note.templatecategory",
"pk": 2,
"fields": {
"name": "Pulls"
}
},
{
"model": "note.templatecategory",
"pk": 3,
"fields": {
"name": "Gala"
}
},
{
"model": "note.templatecategory",
"pk": 4,
"fields": {
"name": "Clubs"
}
},
{
"model": "note.templatecategory",
"pk": 5,
"fields": {
"name": "Bouffe"
}
},
{
"model": "note.templatecategory",
"pk": 6,
"fields": {
"name": "BDA"
}
},
{
"model": "note.templatecategory",
"pk": 7,
"fields": {
"name": "Autre"
}
},
{
"model": "note.templatecategory",
"pk": 8,
"fields": {
"name": "Alcool"
}
}
]
#!/usr/bin/env python
# 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 .models import TransactionTemplate
from django.conf import settings
from django.utils.translation import gettext_lazy as _
import os
from crispy_forms.helper import FormHelper
from crispy_forms.bootstrap import Div
from crispy_forms.layout import Layout, HTML
from .models import Transaction, TransactionTemplate, TemplateTransaction
from .models import Note, Alias
class AliasForm(forms.ModelForm):
class Meta:
model = Alias
fields = ("name",)
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
self.fields["name"].label = False
self.fields["name"].widget.attrs={"placeholder":_('New Alias')}
class ImageForm(forms.Form):
image = forms.ImageField(required = False,
label=_('select an image'),
help_text=_('Maximal size: 2MB'))
x = forms.FloatField(widget=forms.HiddenInput())
y = forms.FloatField(widget=forms.HiddenInput())
width = forms.FloatField(widget=forms.HiddenInput())
height = forms.FloatField(widget=forms.HiddenInput())
class TransactionTemplateForm(forms.ModelForm):
class Meta:
model = TransactionTemplate
fields ='__all__'
fields = '__all__'
# Le champ de destination est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les aliases valides
# Pour force le type d'une note, il faut rajouter le paramètre :
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
widgets = {
'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
}
class TransactionForm(forms.ModelForm):
def save(self, commit=True):
super().save(commit)
def clean(self):
"""
If the user has no right to transfer funds, then it will be the source of the transfer by default.
Transactions between a note and the same note are not authorized.
"""
cleaned_data = super().clean()
if not "source" in cleaned_data: # TODO Replace it with "if %user has no right to transfer funds"
cleaned_data["source"] = self.user.note
if cleaned_data["source"].pk == cleaned_data["destination"].pk:
self.add_error("destination", _("Source and destination must be different."))
return cleaned_data
class Meta:
model = Transaction
fields = (
'source',
'destination',
'reason',
'amount',
)
# Voir ci-dessus
widgets = {
'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
}
class ConsoForm(forms.ModelForm):
def save(self, commit=True):
button: TransactionTemplate = TransactionTemplate.objects.filter(
name=self.data['button']).get()
self.instance.destination = button.destination
self.instance.amount = button.amount
self.instance.reason = '{} ({})'.format(button.name, button.category)
self.instance.name = button.name
self.instance.category = button.category
super().save(commit)
class Meta:
model = TemplateTransaction
fields = ('source', )
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les aliases de note valides
widgets = {
'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},
),
}
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .transactions import MembershipTransaction, Transaction, \
TransactionTemplate
TemplateCategory, TransactionTemplate, TemplateTransaction
__all__ = [
# Notes
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions
'MembershipTransaction', 'Transaction', 'TransactionTemplate',
'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate',
'TemplateTransaction',
]
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import unicodedata
......@@ -10,7 +9,6 @@ from django.core.validators import RegexValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel
"""
Defines each note types
"""
......@@ -18,25 +16,37 @@ Defines each note types
class Note(PolymorphicModel):
"""
An model, use to add transactions capabilities
Gives transactions capabilities. Note is a Polymorphic Model, use as based
for the models :model:`note.NoteUser` and :model:`note.NoteClub`.
A Note principaly store the actual balance of someone/some club.
A Note can be searched find throught an :model:`note.Alias`
"""
balance = models.IntegerField(
verbose_name=_('account balance'),
help_text=_('in centimes, money credited for this instance'),
default=0,
)
last_negative= models.DateTimeField(
verbose_name=_('last negative date'),
help_text=_('last time the balance was negative'),
null=True,
blank=True,
)
is_active = models.BooleanField(
_('active'),
default=True,
help_text=_(
'Designates whether this note should be treated as active. '
'Unselect this instead of deleting notes.'
),
'Unselect this instead of deleting notes.'),
)
display_image = models.ImageField(
verbose_name=_('display image'),
max_length=255,
blank=True,
blank=False,
null=False,
upload_to='pic/',
default='pic/default.png'
)
created_at = models.DateTimeField(
verbose_name=_('created at'),
......@@ -63,7 +73,8 @@ class Note(PolymorphicModel):
if aliases.exists():
# Alias exists, so check if it is linked to this note
if aliases.first().note != self:
raise ValidationError(_('This alias is already taken.'))
raise ValidationError(_('This alias is already taken.'),
code="same_alias")
# Save note
super().save(*args, **kwargs)
......@@ -81,11 +92,13 @@ class Note(PolymorphicModel):
"""
Verify alias (simulate save)
"""
aliases = Alias.objects.filter(name=str(self))
aliases = Alias.objects.filter(
normalized_name=Alias.normalize(str(self)))
if aliases.exists():
# Alias exists, so check if it is linked to this note
if aliases.first().note != self:
raise ValidationError(_('This alias is already taken.'))
raise ValidationError(_('This alias is already taken.'),
code="same_alias",)
else:
# Alias does not exist yet, so check if it can exist
a = Alias(name=str(self))
......@@ -94,7 +107,7 @@ class Note(PolymorphicModel):
class NoteUser(Note):
"""
A Note associated to an User
A :model:`note.Note` associated to an unique :model:`auth.User`.
"""
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
......@@ -116,7 +129,7 @@ class NoteUser(Note):
class NoteClub(Note):
"""
A Note associated to a Club
A :model:`note.Note` associated to an unique :model:`member.Club`
"""
club = models.OneToOneField(
'member.Club',
......@@ -133,17 +146,18 @@ class NoteClub(Note):
return str(self.club)
def pretty(self):
return _("Note for %(club)s club") % {'club': str(self.club)}
return _("Note of %(club)s club") % {'club': str(self.club)}
class NoteSpecial(Note):
"""
A Note for special account, where real money enter or leave the system
A :model:`note.Note` for special accounts, where real money enter or leave the system
- bank check
- credit card
- bank transfer
- cash
- refund
This Type of Note is not associated to a :model:`auth.User` or :model:`member.Club` .
"""
special_type = models.CharField(
verbose_name=_('type'),
......@@ -161,7 +175,13 @@ class NoteSpecial(Note):
class Alias(models.Model):
"""
An alias labels a Note instance, only for user and clubs
points toward a :model:`note.NoteUser` or :model;`note.NoteClub` instance.
Alias are unique, but a :model:`note.NoteUser` or :model:`note.NoteClub` can
have multiples aliases.
Aliases name are also normalized, two differents :model:`note.Note` can not
have the same normalized alias, to avoid confusion when referring orally to
it.
"""
name = models.CharField(
verbose_name=_('name'),
......@@ -170,15 +190,15 @@ class Alias(models.Model):
validators=[
RegexValidator(
regex=settings.ALIAS_VALIDATOR_REGEX,
message=_('Invalid alias')
message=_('Invalid alias'),
)
] if settings.ALIAS_VALIDATOR_REGEX else []
] if settings.ALIAS_VALIDATOR_REGEX else [],
)
normalized_name = models.CharField(
max_length=255,
unique=True,
default='',
editable=False
editable=False,
)
note = models.ForeignKey(
Note,
......@@ -198,27 +218,27 @@ class Alias(models.Model):
Normalizes a string: removes most diacritics and does casefolding
"""
return ''.join(
char
for char in unicodedata.normalize('NFKD', string.casefold())
char for char in unicodedata.normalize('NFKD', string.casefold())
if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'})
).casefold()
def save(self, *args, **kwargs):
"""
Handle normalized_name
"""
self.normalized_name = Alias.normalize(self.name)
if len(self.normalized_name) < 256:
super().save(*args, **kwargs)
for cat in {'M', 'P', 'Z', 'C'})).casefold()
def clean(self):
normalized_name = Alias.normalize(self.name)
if len(normalized_name) >= 255:
raise ValidationError(_('Alias too long.'))
raise ValidationError(_('Alias is too long.'),
code='alias_too_long')
try:
if self != Alias.objects.get(normalized_name=normalized_name):
raise ValidationError(_('An alias with a similar name '
'already exists.'))
sim_alias = Alias.objects.get(normalized_name=normalized_name)
if self != sim_alias:
raise ValidationError(_('An alias with a similar name already exists: {} '.format(sim_alias)),
code="same_alias"
)
except Alias.DoesNotExist:
pass
self.normalized_name = normalized_name
def delete(self, using=None, keep_parents=False):
if self.name == str(self.note):
raise ValidationError(_("You can't delete your main alias."),
code="cant_delete_main_alias")
return super().delete(using, keep_parents)
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from polymorphic.models import PolymorphicModel
from .notes import Note,NoteClub
from .notes import Note, NoteClub
"""
Defines transactions
"""
class TemplateCategory(models.Model):
"""
Defined a recurrent transaction category
Example: food, softs, ...
"""
name = models.CharField(
verbose_name=_("name"),
max_length=31,
unique=True,
)
class Meta:
verbose_name = _("transaction category")
verbose_name_plural = _("transaction categories")
def __str__(self):
return str(self.name)
class TransactionTemplate(models.Model):
"""
Defined a recurrent transaction
associated to selling something (a burger, a beer, ...)
"""
name = models.CharField(
verbose_name=_('name'),
max_length=255,
unique=True,
error_messages={'unique':_("A template with this name already exist")},
)
destination = models.ForeignKey(
NoteClub,
......@@ -30,9 +56,18 @@ class TransactionTemplate(models.Model):
verbose_name=_('amount'),
help_text=_('in centimes'),
)
template_type = models.CharField(
category = models.ForeignKey(
TemplateCategory,
on_delete=models.PROTECT,
verbose_name=_('type'),
max_length=31
max_length=31,
)
display = models.BooleanField(
default = True,
)
description = models.CharField(
verbose_name=_('description'),
max_length=255,
)
class Meta:
......@@ -40,10 +75,17 @@ class TransactionTemplate(models.Model):
verbose_name_plural = _("transaction templates")
def get_absolute_url(self):
return reverse('note:template_update',args=(self.pk,))
return reverse('note:template_update', args=(self.pk, ))
class Transaction(PolymorphicModel):
"""
General transaction between two :model:`note.Note`
amount is store in centimes of currency, making it a positive integer
value. (from someone to someone else)
"""
class Transaction(models.Model):
source = models.ForeignKey(
Note,
on_delete=models.PROTECT,
......@@ -64,13 +106,7 @@ class Transaction(models.Model):
verbose_name=_('quantity'),
default=1,
)
amount = models.PositiveIntegerField(
verbose_name=_('amount'),
)
transaction_type = models.CharField(
verbose_name=_('type'),
max_length=31,
)
amount = models.PositiveIntegerField(verbose_name=_('amount'), )
reason = models.CharField(
verbose_name=_('reason'),
max_length=255,
......@@ -88,6 +124,11 @@ class Transaction(models.Model):
"""
When saving, also transfer money between two notes
"""
if self.source.pk == self.destination.pk:
# When source == destination, no money is transfered
return
created = self.pk is None
to_transfer = self.amount * self.quantity
if not created:
......@@ -108,10 +149,31 @@ class Transaction(models.Model):
@property
def total(self):
return self.amount*self.quantity
return self.amount * self.quantity
class TemplateTransaction(Transaction):
"""
Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`.
"""
template = models.ForeignKey(
TransactionTemplate,
null=True,
on_delete=models.SET_NULL,
)
category = models.ForeignKey(
TemplateCategory,
on_delete=models.PROTECT,
)
class MembershipTransaction(Transaction):
"""
Special type of :model:`note.Transaction` associated to a :model:`member.Membership`.
"""
membership = models.OneToOneField(
'member.Membership',
on_delete=models.PROTECT,
......
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
def save_user_note(instance, created, **_kwargs):
def save_user_note(instance, created, raw, **_kwargs):
"""
Hook to create and save a note when an user is updated
"""
if raw:
# When provisionning data, do not try to autocreate
return
if created:
from .models import NoteUser
NoteUser.objects.create(user=instance)
instance.note.save()
def save_club_note(instance, created, **_kwargs):
def save_club_note(instance, created, raw, **_kwargs):
"""
Hook to create and save a note when a club is updated
"""
if raw:
# When provisionning data, do not try to autocreate
return
if created:
from .models import NoteClub
NoteClub.objects.create(club=instance)
......
#!/usr/bin/env python
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from django.db.models import F
from django_tables2.utils import A
from .models.transactions import Transaction
from .models.notes import Alias
class HistoryTable(tables.Table):
class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'}
attrs = {
'class':
'table table-condensed table-striped table-hover'
}
model = Transaction
template_name = 'django_tables2/bootstrap.html'
sequence = ('...','total','valid')
template_name = 'django_tables2/bootstrap4.html'
sequence = ('...', 'total', 'valid')
total = tables.Column() #will use Transaction.total() !!
total = tables.Column() # will use Transaction.total() !!
def order_total(self, QuerySet, is_descending):
def order_total(self, queryset, is_descending):
# needed for rendering
QuerySet = QuerySet.annotate(
total=F('amount') * F('quantity')
).order_by(('-' if is_descending else '') + 'total')
return (QuerySet, True)
queryset = queryset.annotate(total=F('amount') * F('quantity')) \
.order_by(('-' if is_descending else '') + 'total')
return (queryset, True)
class AliasTable(tables.Table):
class Meta:
attrs = {
'class':
'table table condensed table-striped table-hover'
}
model = Alias
fields =('name',)
template_name = 'django_tables2/bootstrap4.html'
show_header = False
name = tables.Column(attrs={'td':{'class':'text-center'}})
delete = tables.LinkColumn('member:user_alias_delete',
args=[A('pk')],
attrs={
'td': {'class':'col-sm-2'},
'a': {'class': 'btn btn-danger'} },
text='delete',accessor='pk')
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import template
def pretty_money(value):
if value%100 == 0:
return str(value//100) + ''
if value % 100 == 0:
return "{:s}{:d} €".format(
"- " if value < 0 else "",
abs(value) // 100,
)
else:
return str(value//100) + '' + str(value%100)
return "{:s}{:d} € {:02d}".format(
"- " if value < 0 else "",
abs(value) // 100,
abs(value) % 100,
)
register = template.Library()
......
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# 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
from .models import Note
app_name = 'note'
urlpatterns = [
path('transfer/', views.TransactionCreate.as_view(), name='transfer'),
path('buttons/create/',views.TransactionTemplateCreateView.as_view(),name='template_create'),
path('buttons/update/<int:pk>/',views.TransactionTemplateUpdateView.as_view(),name='template_update'),
path('buttons/',views.TransactionTemplateListView.as_view(),name='template_list')
path('buttons/create/', views.TransactionTemplateCreateView.as_view(), name='template_create'),
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'),
]
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# 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.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, DetailView, UpdateView
from django.views.generic import CreateView, ListView, UpdateView
from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction
from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
from .models import Transaction,TransactionTemplate
from .forms import TransactionTemplateForm
class TransactionCreate(LoginRequiredMixin, CreateView):
"""
......@@ -16,7 +19,7 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
TODO: If user have sufficient rights, they can transfer from an other note
"""
model = Transaction
fields = ('destination', 'amount', 'reason')
form_class = TransactionForm
def get_context_data(self, **kwargs):
"""
......@@ -25,24 +28,123 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
context = super().get_context_data(**kwargs)
context['title'] = _('Transfer money from your account '
'to one or others')
context['no_cache'] = True
return context
class TransactionTemplateCreateView(LoginRequiredMixin,CreateView):
def get_form(self, form_class=None):
"""
If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
"""
form = super().get_form(form_class)
if False: # TODO: fix it with "if %user has no right to transfer funds"
del form.fields['source']
form.user = self.request.user
return form
def get_success_url(self):
return reverse('note:transfer')
class NoteAutocomplete(autocomplete.Select2QuerySetView):
"""
Auto complete note by aliases
"""
def get_queryset(self):
"""
Quand une personne cherche un alias, 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 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):
# 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):
# Le résultat renvoyé doit être l'identifiant de la note, et non de l'alias
return str(result.note.pk)
class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
"""
Create TransactionTemplate
"""
model = TransactionTemplate
form_class = TransactionTemplateForm
class TransactionTemplateListView(LoginRequiredMixin,ListView):
class TransactionTemplateListView(LoginRequiredMixin, ListView):
"""
List TransactionsTemplates
"""
model = TransactionTemplate
form_class = TransactionTemplateForm
class TransactionTemplateUpdateView(LoginRequiredMixin,UpdateView):
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
"""
"""
model = TransactionTemplate
form_class=TransactionTemplateForm
form_class = TransactionTemplateForm
class ConsoView(LoginRequiredMixin, CreateView):
"""
Consume
"""
model = TemplateTransaction
template_name = "note/conso_form.html"
form_class = ConsoForm
def get_context_data(self, **kwargs):
"""
Add some context variables in template such as page title
"""
context = super().get_context_data(**kwargs)
context['transaction_templates'] = TransactionTemplate.objects.filter(display=True) \
.order_by('category')
context['title'] = _("Consommations")
# select2 compatibility
context['no_cache'] = True
return context
def get_success_url(self):
"""
When clicking a button, reload the same page
"""
return reverse('note:consos')
#!/bin/bash
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
python manage.py compilemessages
python manage.py makemigrations
# Wait for database
sleep 5
python manage.py migrate
# TODO: use uwsgi in production
python manage.py runserver 0.0.0.0:8000
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-02-27 17:39+0100\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"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/activity/apps.py:10 apps/activity/models.py:76
msgid "activity"
msgstr ""
#: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:60 apps/member/models.py:111
#: apps/note/models/notes.py:184 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:11
msgid "name"
msgstr ""
#: apps/activity/models.py:23
msgid "can invite"
msgstr ""
#: apps/activity/models.py:26
msgid "guest entry fee"
msgstr ""
#: apps/activity/models.py:30
msgid "activity type"
msgstr ""
#: apps/activity/models.py:31
msgid "activity types"
msgstr ""
#: apps/activity/models.py:48 apps/note/models/transactions.py:69
msgid "description"
msgstr ""
#: apps/activity/models.py:54 apps/note/models/notes.py:160
#: apps/note/models/transactions.py:62
msgid "type"
msgstr ""
#: apps/activity/models.py:60
msgid "organizer"
msgstr ""
#: apps/activity/models.py:66
msgid "attendees club"
msgstr ""
#: apps/activity/models.py:69
msgid "start date"
msgstr ""
#: apps/activity/models.py:72
msgid "end date"
msgstr ""
#: apps/activity/models.py:77
msgid "activities"
msgstr ""
#: apps/activity/models.py:108
msgid "guest"
msgstr ""
#: apps/activity/models.py:109
msgid "guests"
msgstr ""
#: apps/api/apps.py:10
msgid "API"
msgstr ""
#: apps/logs/apps.py:10
msgid "Logs"
msgstr ""
#: apps/logs/models.py:20 apps/note/models/notes.py:105
msgid "user"
msgstr ""
#: apps/logs/models.py:27
msgid "model"
msgstr ""
#: apps/logs/models.py:34
msgid "identifier"
msgstr ""
#: apps/logs/models.py:39
msgid "previous data"
msgstr ""
#: apps/logs/models.py:44
msgid "new data"
msgstr ""
#: apps/logs/models.py:51
msgid "action"
msgstr ""
#: apps/logs/models.py:59
msgid "timestamp"
msgstr ""
#: apps/logs/models.py:63
msgid "Logs cannot be destroyed."
msgstr ""
#: apps/member/apps.py:10
msgid "member"
msgstr ""
#: apps/member/models.py:23
msgid "phone number"
msgstr ""
#: apps/member/models.py:29 templates/member/profile_detail.html:24
msgid "section"
msgstr ""
#: apps/member/models.py:30
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr ""
#: apps/member/models.py:36 templates/member/profile_detail.html:27
msgid "address"
msgstr ""
#: apps/member/models.py:42
msgid "paid"
msgstr ""
#: apps/member/models.py:47 apps/member/models.py:48
msgid "user profile"
msgstr ""
#: apps/member/models.py:65
msgid "email"
msgstr ""
#: apps/member/models.py:70
msgid "membership fee"
msgstr ""
#: apps/member/models.py:74
msgid "membership duration"
msgstr ""
#: apps/member/models.py:75
msgid "The longest time a membership can last (NULL = infinite)."
msgstr ""
#: apps/member/models.py:80
msgid "membership start"
msgstr ""
#: apps/member/models.py:81
msgid "How long after January 1st the members can renew their membership."
msgstr ""
#: apps/member/models.py:86
msgid "membership end"
msgstr ""
#: apps/member/models.py:87
msgid ""
"How long the membership can last after January 1st of the next year after "
"members can renew their membership."
msgstr ""
#: apps/member/models.py:93 apps/note/models/notes.py:135
msgid "club"
msgstr ""
#: apps/member/models.py:94
msgid "clubs"
msgstr ""
#: apps/member/models.py:117
msgid "role"
msgstr ""
#: apps/member/models.py:118
msgid "roles"
msgstr ""
#: apps/member/models.py:142
msgid "membership starts on"
msgstr ""
#: apps/member/models.py:145
msgid "membership ends on"
msgstr ""
#: apps/member/models.py:149
msgid "fee"
msgstr ""
#: apps/member/models.py:153
msgid "membership"
msgstr ""
#: apps/member/models.py:154
msgid "memberships"
msgstr ""
#: apps/member/views.py:63 templates/member/profile_detail.html:42
msgid "Update Profile"
msgstr ""
#: apps/member/views.py:79
msgid "An alias with a similar name already exists."
msgstr ""
#: apps/member/views.py:130
#, python-format
msgid "Account #%(id)s: %(username)s"
msgstr ""
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
msgid "source"
msgstr ""
#: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99
msgid "destination"
msgstr ""
#: apps/note/apps.py:14 apps/note/models/notes.py:54
msgid "note"
msgstr ""
#: apps/note/forms.py:49
msgid "Source and destination must be different."
msgstr ""
#: apps/note/models/notes.py:26
msgid "account balance"
msgstr ""
#: apps/note/models/notes.py:27
msgid "in centimes, money credited for this instance"
msgstr ""
#: apps/note/models/notes.py:31
msgid "last negative date"
msgstr ""
#: apps/note/models/notes.py:32
msgid "last time the balance was negative"
msgstr ""
#: apps/note/models/notes.py:37
msgid "active"
msgstr ""
#: apps/note/models/notes.py:40
msgid ""
"Designates whether this note should be treated as active. Unselect this "
"instead of deleting notes."
msgstr ""
#: apps/note/models/notes.py:44
msgid "display image"
msgstr ""
#: apps/note/models/notes.py:49 apps/note/models/transactions.py:102
msgid "created at"
msgstr ""
#: apps/note/models/notes.py:55
msgid "notes"
msgstr ""
#: apps/note/models/notes.py:63
msgid "Note"
msgstr ""
#: apps/note/models/notes.py:73 apps/note/models/notes.py:97
msgid "This alias is already taken."
msgstr ""
#: apps/note/models/notes.py:113
msgid "user"
msgstr ""
#: apps/note/models/notes.py:117
msgid "one's note"
msgstr ""
#: apps/note/models/notes.py:118
msgid "users note"
msgstr ""
#: apps/note/models/notes.py:124
#, python-format
msgid "%(user)s's note"
msgstr ""
#: apps/note/models/notes.py:139
msgid "club note"
msgstr ""
#: apps/note/models/notes.py:140
msgid "clubs notes"
msgstr ""
#: apps/note/models/notes.py:146
#, python-format
msgid "Note of %(club)s club"
msgstr ""
#: apps/note/models/notes.py:166
msgid "special note"
msgstr ""
#: apps/note/models/notes.py:167
msgid "special notes"
msgstr ""
#: apps/note/models/notes.py:190
msgid "Invalid alias"
msgstr ""
#: apps/note/models/notes.py:206
msgid "alias"
msgstr ""
#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33
msgid "aliases"
msgstr ""
#: apps/note/models/notes.py:233
msgid "Alias is too long."
msgstr ""
#: apps/note/models/notes.py:238
msgid "An alias with a similar name already exists:"
msgstr ""
#: apps/note/models/notes.py:246
msgid "You can't delete your main alias."
msgstr ""
#: apps/note/models/transactions.py:30
msgid "transaction category"
msgstr ""
#: apps/note/models/transactions.py:31
msgid "transaction categories"
msgstr ""
#: apps/note/models/transactions.py:47
msgid "A template with this name already exist"
msgstr ""
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
msgid "amount"
msgstr ""
#: apps/note/models/transactions.py:57
msgid "in centimes"
msgstr ""
#: apps/note/models/transactions.py:74
msgid "transaction template"
msgstr ""
#: apps/note/models/transactions.py:75
msgid "transaction templates"
msgstr ""
#: apps/note/models/transactions.py:106
msgid "quantity"
msgstr ""
#: apps/note/models/transactions.py:111
msgid "reason"
msgstr ""
#: apps/note/models/transactions.py:115
msgid "valid"
msgstr ""
#: apps/note/models/transactions.py:120
msgid "transaction"
msgstr ""
#: apps/note/models/transactions.py:121
msgid "transactions"
msgstr ""
#: apps/note/models/transactions.py:184
msgid "membership transaction"
msgstr ""
#: apps/note/models/transactions.py:185
msgid "membership transactions"
msgstr ""
#: apps/note/views.py:29
msgid "Transfer money from your account to one or others"
msgstr ""
#: apps/note/views.py:138
msgid "Consommations"
msgstr ""
#: note_kfet/settings/base.py:155
msgid "German"
msgstr ""
#: note_kfet/settings/base.py:156
msgid "English"
msgstr ""
#: note_kfet/settings/base.py:157
msgid "French"
msgstr ""
#: templates/base.html:13
msgid "The ENS Paris-Saclay BDE note."
msgstr ""
#: templates/member/club_detail.html:10
msgid "Membership starts on"
msgstr ""
#: templates/member/club_detail.html:12
msgid "Membership ends on"
msgstr ""
#: templates/member/club_detail.html:14
msgid "Membership duration"
msgstr ""
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30
msgid "balance"
msgstr ""
#: templates/member/manage_auth_tokens.html:16
msgid "Token"
msgstr ""
#: templates/member/manage_auth_tokens.html:23
msgid "Created"
msgstr ""
#: templates/member/manage_auth_tokens.html:31
msgid "Regenerate token"
msgstr ""
#: templates/member/profile_detail.html:11
msgid "first name"
msgstr ""
#: templates/member/profile_detail.html:14
msgid "username"
msgstr ""
#: templates/member/profile_detail.html:17
msgid "password"
msgstr ""
#: templates/member/profile_detail.html:20
msgid "Change password"
msgstr ""
#: templates/member/profile_detail.html:38
msgid "Manage auth token"
msgstr ""
#: templates/member/profile_detail.html:54
msgid "View my memberships"
msgstr ""
#: templates/member/profile_update.html:13
msgid "Save Changes"
msgstr ""
#: templates/member/signup.html:14
msgid "Sign Up"
msgstr ""
#: templates/note/transaction_form.html:35
msgid "Transfer"
msgstr ""
#: templates/registration/logged_out.html:8
msgid "Thanks for spending some quality time with the Web site today."
msgstr ""
#: templates/registration/logged_out.html:9
msgid "Log in again"
msgstr ""
#: templates/registration/login.html:7 templates/registration/login.html:8
#: templates/registration/login.html:22
#: templates/registration/password_reset_complete.html:10
msgid "Log in"
msgstr ""
#: templates/registration/login.html:13
#, python-format
msgid ""
"You are authenticated as %(username)s, but are not authorized to access this "
"page. Would you like to login to a different account?"
msgstr ""
#: templates/registration/login.html:23
msgid "Forgotten your password or username?"
msgstr ""
#: templates/registration/password_change_done.html:8
msgid "Your password was changed."
msgstr ""
#: templates/registration/password_change_form.html:9
msgid ""
"Please enter your old password, for security's sake, and then enter your new "
"password twice so we can verify you typed it in correctly."
msgstr ""
#: templates/registration/password_change_form.html:11
#: templates/registration/password_reset_confirm.html:12
msgid "Change my password"
msgstr ""
#: templates/registration/password_reset_complete.html:8
msgid "Your password has been set. You may go ahead and log in now."
msgstr ""
#: templates/registration/password_reset_confirm.html:9
msgid ""
"Please enter your new password twice so we can verify you typed it in "
"correctly."
msgstr ""
#: templates/registration/password_reset_confirm.html:15
msgid ""
"The password reset link was invalid, possibly because it has already been "
"used. Please request a new password reset."
msgstr ""
#: templates/registration/password_reset_done.html:8
msgid ""
"We've emailed you instructions for setting your password, if an account "
"exists with the email you entered. You should receive them shortly."
msgstr ""
#: templates/registration/password_reset_done.html:9
msgid ""
"If you don't receive an email, please make sure you've entered the address "
"you registered with, and check your spam folder."
msgstr ""
#: templates/registration/password_reset_form.html:8
msgid ""
"Forgotten your password? Enter your email address below, and we'll email "
"instructions for setting a new one."
msgstr ""
#: templates/registration/password_reset_form.html:11
msgid "Reset my password"
msgstr ""
......@@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-08-14 15:14+0200\n"
"POT-Creation-Date: 2020-02-27 17:39+0100\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,356 +13,500 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: apps/activity/apps.py:11 apps/activity/models.py:70
#: apps/activity/apps.py:10 apps/activity/models.py:76
msgid "activity"
msgstr "activité"
#: apps/activity/models.py:15 apps/activity/models.py:38
#: apps/member/models.py:59 apps/member/models.py:107
#: apps/note/models/notes.py:167 apps/note/models/transactions.py:19
#: templates/member/profile_detail.html:10
#: apps/activity/models.py:19 apps/activity/models.py:44
#: apps/member/models.py:60 apps/member/models.py:111
#: apps/note/models/notes.py:184 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 templates/member/profile_detail.html:11
msgid "name"
msgstr "nom"
#: apps/activity/models.py:19
#: apps/activity/models.py:23
msgid "can invite"
msgstr "peut inviter"
#: apps/activity/models.py:22
#: apps/activity/models.py:26
msgid "guest entry fee"
msgstr "cotisation de l'entrée invité"
#: apps/activity/models.py:26
#: apps/activity/models.py:30
msgid "activity type"
msgstr "type d'activité"
#: apps/activity/models.py:27
#: apps/activity/models.py:31
msgid "activity types"
msgstr "types d'activité"
#: apps/activity/models.py:42
#: apps/activity/models.py:48 apps/note/models/transactions.py:69
msgid "description"
msgstr "description"
#: apps/activity/models.py:48 apps/note/models/notes.py:149
#: apps/note/models/transactions.py:34 apps/note/models/transactions.py:71
#: apps/activity/models.py:54 apps/note/models/notes.py:160
#: apps/note/models/transactions.py:62
msgid "type"
msgstr "type"
#: apps/activity/models.py:54
#: apps/activity/models.py:60
msgid "organizer"
msgstr "organisateur"
#: apps/activity/models.py:60
#: apps/activity/models.py:66
msgid "attendees club"
msgstr ""
#: apps/activity/models.py:63
#: apps/activity/models.py:69
msgid "start date"
msgstr "date de début"
#: apps/activity/models.py:66
#: apps/activity/models.py:72
msgid "end date"
msgstr "date de fin"
#: apps/activity/models.py:71
#: apps/activity/models.py:77
msgid "activities"
msgstr "activités"
#: apps/activity/models.py:100
#: apps/activity/models.py:108
msgid "guest"
msgstr "invité"
#: apps/activity/models.py:101
#: apps/activity/models.py:109
msgid "guests"
msgstr "invités"
#: apps/member/apps.py:11
#: apps/api/apps.py:10
msgid "API"
msgstr ""
#: apps/logs/apps.py:10
msgid "Logs"
msgstr ""
#: apps/logs/models.py:20 apps/note/models/notes.py:105
msgid "user"
msgstr "utilisateur"
#: apps/logs/models.py:27
msgid "model"
msgstr "Modèle"
#: apps/logs/models.py:34
msgid "identifier"
msgstr "Identifiant"
#: apps/logs/models.py:39
msgid "previous data"
msgstr "Données précédentes"
#: apps/logs/models.py:44
#, fuzzy
#| msgid "end date"
msgid "new data"
msgstr "Nouvelles données"
#: apps/logs/models.py:51
#, fuzzy
#| msgid "section"
msgid "action"
msgstr "Action"
#: apps/logs/models.py:59
msgid "timestamp"
msgstr "Date"
#: apps/logs/models.py:63
msgid "Logs cannot be destroyed."
msgstr "Les logs ne peuvent pas être détruits."
#: apps/member/apps.py:10
msgid "member"
msgstr "adhérent"
#: apps/member/models.py:24
#: apps/member/models.py:23
msgid "phone number"
msgstr "numéro de téléphone"
#: apps/member/models.py:30 templates/member/profile_detail.html:18
#: apps/member/models.py:29 templates/member/profile_detail.html:24
msgid "section"
msgstr "section"
#: apps/member/models.py:31
#: apps/member/models.py:30
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
#: apps/member/models.py:37 templates/member/profile_detail.html:20
#: apps/member/models.py:36 templates/member/profile_detail.html:27
msgid "address"
msgstr "adresse"
#: apps/member/models.py:43
#: apps/member/models.py:42
msgid "paid"
msgstr "payé"
#: apps/member/models.py:48 apps/member/models.py:49
#: apps/member/models.py:47 apps/member/models.py:48
msgid "user profile"
msgstr "profil utilisateur"
#: apps/member/models.py:64
#: apps/member/models.py:65
msgid "email"
msgstr "courriel"
#: apps/member/models.py:69
#: apps/member/models.py:70
msgid "membership fee"
msgstr "cotisation pour adhérer"
#: apps/member/models.py:73
#: apps/member/models.py:74
msgid "membership duration"
msgstr "durée de l'adhésion"
#: apps/member/models.py:74
#: apps/member/models.py:75
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:79
#: apps/member/models.py:80
msgid "membership start"
msgstr "début de l'adhésion"
#: apps/member/models.py:80
#: apps/member/models.py:81
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:85
#: apps/member/models.py:86
msgid "membership end"
msgstr "fin de l'adhésion"
#: apps/member/models.py:86
#: apps/member/models.py:87
msgid ""
"How long the membership can last after January 1st of the next year after "
"members can renew their membership."
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:92 apps/note/models/notes.py:125
#: apps/member/models.py:93 apps/note/models/notes.py:135
msgid "club"
msgstr "club"
#: apps/member/models.py:93
#: apps/member/models.py:94
msgid "clubs"
msgstr "clubs"
#: apps/member/models.py:113
#: apps/member/models.py:117
msgid "role"
msgstr "rôle"
#: apps/member/models.py:114
#: apps/member/models.py:118
msgid "roles"
msgstr "rôles"
#: apps/member/models.py:134
#: apps/member/models.py:142
msgid "membership starts on"
msgstr "l'adhésion commence le"
#: apps/member/models.py:137
#: apps/member/models.py:145
msgid "membership ends on"
msgstr "l'adhésion finie le"
#: apps/member/models.py:141
#: apps/member/models.py:149
msgid "fee"
msgstr "cotisation"
#: apps/member/models.py:145
#: apps/member/models.py:153
msgid "membership"
msgstr "adhésion"
#: apps/member/models.py:146
#: apps/member/models.py:154
msgid "memberships"
msgstr "adhésions"
#: apps/note/admin.py:112 apps/note/models/transactions.py:51
#: apps/member/views.py:63 templates/member/profile_detail.html:42
msgid "Update Profile"
msgstr "Modifier le profil"
#: apps/member/views.py:79
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
#: apps/member/views.py:130
#, python-format
msgid "Account #%(id)s: %(username)s"
msgstr "Compte n°%(id)s : %(username)s"
#: apps/note/admin.py:120 apps/note/models/transactions.py:93
msgid "source"
msgstr "source"
#: apps/note/admin.py:120 apps/note/admin.py:148
#: apps/note/models/transactions.py:27 apps/note/models/transactions.py:57
#: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:99
msgid "destination"
msgstr "destination"
#: apps/note/apps.py:15 apps/note/models/notes.py:47
#: apps/note/apps.py:14 apps/note/models/notes.py:54
msgid "note"
msgstr "note"
#: apps/note/models/notes.py:24
#: apps/note/forms.py:49
msgid "Source and destination must be different."
msgstr "La source et la destination doivent être différentes."
#: apps/note/models/notes.py:26
msgid "account balance"
msgstr "solde du compte"
#: apps/note/models/notes.py:25
#: apps/note/models/notes.py:27
msgid "in centimes, money credited for this instance"
msgstr "en centimes, argent crédité pour cette instance"
#: apps/note/models/notes.py:29
#: apps/note/models/notes.py:31
msgid "last negative date"
msgstr "dernier date de négatif"
#: apps/note/models/notes.py:32
msgid "last time the balance was negative"
msgstr "dernier instant où la note était en négatif"
#: apps/note/models/notes.py:37
msgid "active"
msgstr "actif"
#: apps/note/models/notes.py:32
#: apps/note/models/notes.py:40
msgid ""
"Designates whether this note should be treated as active. Unselect this "
"instead of deleting notes."
msgstr ""
"Indique si la note est active. Désactiver cela plutôt que supprimer la note."
#: apps/note/models/notes.py:37
#: apps/note/models/notes.py:44
msgid "display image"
msgstr "image affichée"
#: apps/note/models/notes.py:42 apps/note/models/transactions.py:60
#: apps/note/models/notes.py:49 apps/note/models/transactions.py:102
msgid "created at"
msgstr "créée le"
#: apps/note/models/notes.py:48
#: apps/note/models/notes.py:55
msgid "notes"
msgstr "notes"
#: apps/note/models/notes.py:56
#: apps/note/models/notes.py:63
msgid "Note"
msgstr "Note"
#: apps/note/models/notes.py:66 apps/note/models/notes.py:88
#: apps/note/models/notes.py:73 apps/note/models/notes.py:97
msgid "This alias is already taken."
msgstr "Cet alias est déjà pris."
#: apps/note/models/notes.py:103
#: apps/note/models/notes.py:113
msgid "user"
msgstr "utilisateur"
#: apps/note/models/notes.py:107
#: apps/note/models/notes.py:117
msgid "one's note"
msgstr "note d'un utilisateur"
#: apps/note/models/notes.py:108
#: apps/note/models/notes.py:118
msgid "users note"
msgstr "notes des utilisateurs"
#: apps/note/models/notes.py:114
#: apps/note/models/notes.py:124
#, python-format
msgid "%(user)s's note"
msgstr "Note de %(user)s"
#: apps/note/models/notes.py:129
#: apps/note/models/notes.py:139
msgid "club note"
msgstr "note d'un club"
#: apps/note/models/notes.py:130
#: apps/note/models/notes.py:140
msgid "clubs notes"
msgstr "notes des clubs"
#: apps/note/models/notes.py:136
#: apps/note/models/notes.py:146
#, python-format
msgid "Note for %(club)s club"
msgid "Note of %(club)s club"
msgstr "Note du club %(club)s"
#: apps/note/models/notes.py:155
#: apps/note/models/notes.py:166
msgid "special note"
msgstr "note spéciale"
#: apps/note/models/notes.py:156
#: apps/note/models/notes.py:167
msgid "special notes"
msgstr "notes spéciales"
#: apps/note/models/notes.py:173
#: apps/note/models/notes.py:190
msgid "Invalid alias"
msgstr "Alias invalide"
#: apps/note/models/notes.py:189
#: apps/note/models/notes.py:206
msgid "alias"
msgstr "alias"
#: apps/note/models/notes.py:190
#: apps/note/models/notes.py:207 templates/member/profile_detail.html:33
msgid "aliases"
msgstr "alias"
#: apps/note/models/notes.py:218
msgid "Alias too long."
#: apps/note/models/notes.py:233
msgid "Alias is too long."
msgstr "L'alias est trop long."
#: apps/note/models/notes.py:221
msgid "An alias with a similar name already exists."
#: apps/note/models/notes.py:238
msgid "An alias with a similar name already exists:"
msgstr "Un alias avec un nom similaire existe déjà."
#: apps/note/models/transactions.py:30 apps/note/models/transactions.py:68
#: apps/note/models/notes.py:246
msgid "You can't delete your main alias."
msgstr "Vous ne pouvez pas supprimer votre alias principal."
#: apps/note/models/transactions.py:30
msgid "transaction category"
msgstr "catégorie de transaction"
#: apps/note/models/transactions.py:31
msgid "transaction categories"
msgstr "catégories de transaction"
#: apps/note/models/transactions.py:47
#, fuzzy
msgid "A template with this name already exist"
msgstr "Un modèle de transaction avec un nom similaire existe déjà."
#: apps/note/models/transactions.py:56 apps/note/models/transactions.py:109
msgid "amount"
msgstr "montant"
#: apps/note/models/transactions.py:31
#: apps/note/models/transactions.py:57
msgid "in centimes"
msgstr "en centimes"
#: apps/note/models/transactions.py:39
#: apps/note/models/transactions.py:74
msgid "transaction template"
msgstr "modèle de transaction"
#: apps/note/models/transactions.py:40
#: apps/note/models/transactions.py:75
msgid "transaction templates"
msgstr "modèles de transaction"
#: apps/note/models/transactions.py:64
#: apps/note/models/transactions.py:106
msgid "quantity"
msgstr "quantité"
#: apps/note/models/transactions.py:75
#: apps/note/models/transactions.py:111
msgid "reason"
msgstr "raison"
#: apps/note/models/transactions.py:79
#: apps/note/models/transactions.py:115
msgid "valid"
msgstr "valide"
#: apps/note/models/transactions.py:84
#: apps/note/models/transactions.py:120
msgid "transaction"
msgstr "transaction"
#: apps/note/models/transactions.py:85
#: apps/note/models/transactions.py:121
msgid "transactions"
msgstr "transactions"
#: apps/note/models/transactions.py:118
#: apps/note/models/transactions.py:184
msgid "membership transaction"
msgstr "transaction d'adhésion"
#: apps/note/models/transactions.py:119
#: apps/note/models/transactions.py:185
msgid "membership transactions"
msgstr "transactions d'adhésion"
#: apps/note/views.py:26
#: apps/note/views.py:29
msgid "Transfer money from your account to one or others"
msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres"
#: note_kfet/settings.py:140
#: apps/note/views.py:138
msgid "Consommations"
msgstr "transactions"
#: note_kfet/settings/base.py:155
msgid "German"
msgstr ""
#: note_kfet/settings/base.py:156
msgid "English"
msgstr ""
#: note_kfet/settings.py:141
#: note_kfet/settings/base.py:157
msgid "French"
msgstr ""
#: templates/base.html:14
#: templates/base.html:13
msgid "The ENS Paris-Saclay BDE note."
msgstr ""
msgstr "La note du BDE de l'ENS Paris-Saclay."
#: templates/member/profile_detail.html:12
#: templates/member/club_detail.html:10
msgid "Membership starts on"
msgstr "L'adhésion commence le"
#: templates/member/club_detail.html:12
msgid "Membership ends on"
msgstr "L'adhésion finie le"
#: templates/member/club_detail.html:14
msgid "Membership duration"
msgstr "Durée de l'adhésion"
#: templates/member/club_detail.html:18 templates/member/profile_detail.html:30
msgid "balance"
msgstr "solde du compte"
#: templates/member/manage_auth_tokens.html:16
msgid "Token"
msgstr "Jeton"
#: templates/member/manage_auth_tokens.html:23
msgid "Created"
msgstr "Créé le"
#: templates/member/manage_auth_tokens.html:31
msgid "Regenerate token"
msgstr "Regénérer le jeton"
#: templates/member/profile_detail.html:11
msgid "first name"
msgstr ""
#: templates/member/profile_detail.html:14
#, fuzzy
#| msgid "name"
msgid "username"
msgstr "nom"
msgstr ""
#: templates/member/profile_detail.html:22
#: templates/member/profile_detail.html:17
#, fuzzy
#| msgid "account balance"
msgid "balance"
msgstr "solde du compte"
#| msgid "Change password"
msgid "password"
msgstr ""
#: templates/member/profile_detail.html:26
#: templates/member/profile_detail.html:20
msgid "Change password"
msgstr "Changer le mot de passe"
#: templates/member/profile_detail.html:38
msgid "Manage auth token"
msgstr "Gérer les jetons d'authentification"
#: templates/member/profile_detail.html:51
msgid "Transaction history"
msgstr "Historique des transactions"
#: templates/member/profile_detail.html:54
msgid "View my memberships"
msgstr "Voir mes adhésions"
#: templates/member/profile_update.html:13
msgid "Save Changes"
msgstr "Sauvegarder les changements"
#: templates/member/signup.html:14
msgid "Sign Up"
msgstr ""
#: templates/note/transaction_form.html:35
......
media/pic/default.png

9.04 KiB

File moved