Format code

This commit is contained in:
Alexandre Iooss 2020-02-18 12:31:15 +01:00
parent 0d7db68372
commit f89d91e524
No known key found for this signature in database
GPG Key ID: 6C79278F3FCDCC02
19 changed files with 262 additions and 167 deletions

View File

@ -5,6 +5,7 @@
from ..models import ActivityType, Activity, Guest
from rest_framework import serializers
class ActivityTypeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Activity types.

View File

@ -17,10 +17,13 @@ class UserSerializer(serializers.ModelSerializer):
REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
"""
class Meta:
model = User
exclude = ('password', 'groups', 'user_permissions',)
exclude = (
'password',
'groups',
'user_permissions',
)
class UserViewSet(viewsets.ModelViewSet):

View File

@ -10,6 +10,7 @@ from crispy_forms.layout import Layout, Submit
from .models import Club
class UserFilter(FilterSet):
class Meta:
model = User
@ -23,9 +24,13 @@ class UserFilter(FilterSet):
}
}
class UserFilterFormHelper(FormHelper):
form_method = 'GET'
layout = Layout(
'last_name','first_name','username','profile__section',
'last_name',
'first_name',
'username',
'profile__section',
Submit('Submit', 'Apply Filter'),
)

View File

@ -16,12 +16,12 @@ from crispy_forms.bootstrap import InlineField, FormActions, StrictButton, Div,
from crispy_forms.layout import Layout
class SignUpForm(UserCreationForm):
class Meta:
model = User
fields = ['first_name', 'last_name', 'username', 'email']
class ProfileForm(forms.ModelForm):
"""
A form for the extras field provided by the :model:`member.Profile` model.
@ -31,15 +31,18 @@ class ProfileForm(forms.ModelForm):
fields = '__all__'
exclude = ['user']
class ClubForm(forms.ModelForm):
class Meta:
model = Club
fields = '__all__'
class AddMembersForm(forms.Form):
class Meta:
fields = ('', )
class MembershipForm(forms.ModelForm):
class Meta:
model = Membership
@ -48,18 +51,24 @@ class MembershipForm(forms.ModelForm):
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
# et récupère les noms d'utilisateur valides
widgets = {
'user': autocomplete.ModelSelect2(url='member:user_autocomplete',
'user':
autocomplete.ModelSelect2(
url='member:user_autocomplete',
attrs={
'data-placeholder': 'Nom ...',
'data-minimum-input-length': 1,
}),
},
),
}
MemberFormSet = forms.modelformset_factory(Membership,
MemberFormSet = forms.modelformset_factory(
Membership,
form=MembershipForm,
extra=2,
can_delete=True)
can_delete=True,
)
class FormSetHelper(FormHelper):
def __init__(self, *args, **kwargs):
@ -74,5 +83,4 @@ class FormSetHelper(FormHelper):
Div('roles', css_class='col-sm-2'),
Div('date_start', css_class='col-sm-2'),
css_class="row formset-row",
)
)
))

View File

@ -9,6 +9,7 @@ from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy
class Profile(models.Model):
"""
An user profile
@ -52,6 +53,7 @@ class Profile(models.Model):
def get_absolute_url(self):
return reverse('user_detail', args=(self.pk, ))
class Club(models.Model):
"""
A club is a group of people, whose membership is handle by their
@ -129,15 +131,15 @@ class Membership(models.Model):
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT
on_delete=models.PROTECT,
)
club = models.ForeignKey(
Club,
on_delete=models.PROTECT
on_delete=models.PROTECT,
)
roles = models.ForeignKey(
Role,
on_delete=models.PROTECT
on_delete=models.PROTECT,
)
date_start = models.DateField(
verbose_name=_('membership starts on'),

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -5,15 +5,20 @@ from .models import Club
from django.conf import settings
from django.contrib.auth.models import User
class ClubTable(tables.Table):
class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'}
attrs = {
'class':
'table table-bordered table-condensed table-striped table-hover'
}
model = Club
template_name = 'django_tables2/bootstrap.html'
fields = ('id', 'name', 'email')
row_attrs = {'class':'table-row',
'data-href': lambda record: record.pk }
row_attrs = {
'class': 'table-row',
'data-href': lambda record: record.pk
}
class UserTable(tables.Table):
@ -21,7 +26,10 @@ class UserTable(tables.Table):
solde = tables.Column(accessor='note.balance')
class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'}
attrs = {
'class':
'table table-bordered table-condensed table-striped table-hover'
}
template_name = 'django_tables2/bootstrap.html'
fields = ('last_name', 'first_name', 'username', 'email')
model = User

View File

@ -20,10 +20,10 @@ from .forms import SignUpForm, ProfileForm, ClubForm,MembershipForm, MemberForm
from .tables import ClubTable, UserTable
from .filters import UserFilter, UserFilterFormHelper
from note.models.transactions import Transaction
from note.tables import HistoryTable
class UserCreateView(CreateView):
"""
Une vue pour inscrire un utilisateur et lui créer un profile
@ -49,17 +49,20 @@ class UserCreateView(CreateView):
profile.save()
return super().form_valid(form)
class UserUpdateView(LoginRequiredMixin, UpdateView):
model = User
fields = ['first_name', 'last_name', 'username', 'email']
template_name = 'member/profile_update.html'
second_form = ProfileForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_modified'] = context['user']
context['user'] = self.request.user
context["profile_form"] = self.second_form(instance=context['user_modified'].profile)
context["profile_form"] = self.second_form(
instance=context['user_modified'].profile)
return context
@ -71,21 +74,26 @@ class UserUpdateView(LoginRequiredMixin,UpdateView):
new_username = form.data['username']
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
note = NoteUser.objects.filter(alias__normalized_name=Alias.normalize(new_username))
note = NoteUser.objects.filter(
alias__normalized_name=Alias.normalize(new_username))
if note.exists() and note.get().user != self.request.user:
form.add_error('username', _("An alias with a similar name already exists."))
form.add_error('username',
_("An alias with a similar name already exists."))
return form
def form_valid(self, form):
profile_form = ProfileForm(data=self.request.POST,instance=self.request.user.profile)
profile_form = ProfileForm(
data=self.request.POST,
instance=self.request.user.profile,
)
if form.is_valid() and profile_form.is_valid():
new_username = form.data['username']
alias = Alias.objects.filter(name=new_username)
# Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer
if not alias.exists():
similar = Alias.objects.filter(normalized_name=Alias.normalize(new_username))
similar = Alias.objects.filter(
normalized_name=Alias.normalize(new_username))
if similar.exists():
similar.delete()
@ -98,17 +106,20 @@ class UserUpdateView(LoginRequiredMixin,UpdateView):
def get_success_url(self, **kwargs):
if kwargs:
return reverse_lazy('member:user_detail', kwargs = {'pk': kwargs['id']})
return reverse_lazy('member:user_detail',
kwargs={'pk': kwargs['id']})
else:
return reverse_lazy('member:user_detail', args=(self.object.id, ))
class UserDetailView(LoginRequiredMixin, DetailView):
"""
Affiche les informations sur un utilisateur, sa note, ses clubs ...
"""
model = Profile
context_object_name = "profile"
def get_context_data(slef,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = context['profile'].user
history_list = \
@ -119,6 +130,7 @@ class UserDetailView(LoginRequiredMixin,DetailView):
context['club_list'] = ClubTable(club_list)
return context
class UserListView(LoginRequiredMixin, SingleTableView):
"""
Affiche la liste des utilisateurs, avec une fonction de recherche statique
@ -149,22 +161,25 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
template_name = "member/manage_auth_tokens.html"
def get(self, request, *args, **kwargs):
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
if 'regenerate' in request.GET and Token.objects.filter(
user=request.user).exists():
Token.objects.get(user=self.request.user).delete()
return redirect(reverse_lazy('member:auth_token') + "?show", permanent=True)
return redirect(reverse_lazy('member:auth_token') + "?show",
permanent=True)
return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
context['token'] = Token.objects.get_or_create(
user=self.request.user)[0]
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.
@ -181,10 +196,12 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
return qs
###################################
############## CLUB ###############
###################################
class ClubCreateView(LoginRequiredMixin, CreateView):
"""
Create Club
@ -195,6 +212,7 @@ class ClubCreateView(LoginRequiredMixin,CreateView):
def form_valid(self, form):
return super().form_valid(form)
class ClubListView(LoginRequiredMixin, SingleTableView):
"""
List existing Clubs
@ -202,6 +220,7 @@ class ClubListView(LoginRequiredMixin,SingleTableView):
model = Club
table_class = ClubTable
class ClubDetailView(LoginRequiredMixin, DetailView):
model = Club
context_object_name = "club"
@ -218,10 +237,12 @@ class ClubDetailView(LoginRequiredMixin,DetailView):
context['member_list'] = club_member
return context
class ClubAddMemberView(LoginRequiredMixin, CreateView):
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['formset'] = MemberFormSet()

View File

@ -25,7 +25,10 @@ class NoteAdmin(PolymorphicParentModelAdmin):
Parent regrouping all note types as children
"""
child_models = (NoteClub, NoteSpecial, NoteUser)
list_filter = (PolymorphicChildModelFilter, 'is_active',)
list_filter = (
PolymorphicChildModelFilter,
'is_active',
)
# Use a polymorphic list
list_display = ('pretty', 'balance', 'is_active')
@ -49,6 +52,7 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
# We can't change club after creation or the balance
readonly_fields = ('club', 'balance')
search_fields = ('club', )
def has_add_permission(self, request):
"""
A club note should not be manually added
@ -101,7 +105,10 @@ class TransactionAdmin(admin.ModelAdmin):
list_display = ('created_at', 'poly_source', 'poly_destination',
'quantity', 'amount', 'transaction_type', 'valid')
list_filter = ('transaction_type', 'valid')
autocomplete_fields = ('source', 'destination',)
autocomplete_fields = (
'source',
'destination',
)
def poly_source(self, obj):
"""

View File

@ -17,7 +17,10 @@ class NoteSerializer(serializers.ModelSerializer):
model = Note
fields = '__all__'
extra_kwargs = {
'url': {'view_name': 'project-detail', 'lookup_field': 'pk'},
'url': {
'view_name': 'project-detail',
'lookup_field': 'pk'
},
}
@ -69,6 +72,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
NoteSpecial: NoteSpecialSerializer
}
class TransactionTemplateSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Transaction templates.

View File

@ -69,7 +69,9 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
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()))
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:
@ -79,7 +81,8 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
elif "club" in l:
queryset = queryset.filter(polymorphic_ctype__model="noteclub")
elif "special" in l:
queryset = queryset.filter(polymorphic_ctype__model="notespecial")
queryset = queryset.filter(
polymorphic_ctype__model="notespecial")
else:
queryset = queryset.none()
@ -104,7 +107,8 @@ class AliasViewSet(viewsets.ModelViewSet):
queryset = Alias.objects.all()
alias = self.request.query_params.get("alias", ".*")
queryset = queryset.filter(Q(name__regex=alias) | Q(normalized_name__regex=alias.lower()))
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:
@ -114,11 +118,14 @@ class AliasViewSet(viewsets.ModelViewSet):
if note_type:
l = str(note_type).lower()
if "user" in l:
queryset = queryset.filter(note__polymorphic_ctype__model="noteuser")
queryset = queryset.filter(
note__polymorphic_ctype__model="noteuser")
elif "club" in l:
queryset = queryset.filter(note__polymorphic_ctype__model="noteclub")
queryset = queryset.filter(
note__polymorphic_ctype__model="noteclub")
elif "special" in l:
queryset = queryset.filter(note__polymorphic_ctype__model="notespecial")
queryset = queryset.filter(
note__polymorphic_ctype__model="notespecial")
else:
queryset = queryset.none()

View File

@ -20,9 +20,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',
)

View File

@ -4,6 +4,7 @@ from dal import autocomplete, forward
from django import forms
from .models import Transaction, TransactionTemplate
class TransactionTemplateForm(forms.ModelForm):
class Meta:
model = TransactionTemplate
@ -15,11 +16,14 @@ class TransactionTemplateForm(forms.ModelForm):
# 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',
'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
}),
},
),
}
@ -31,26 +35,38 @@ class TransactionForm(forms.ModelForm):
class Meta:
model = Transaction
fields = ('source', 'destination', 'reason', 'amount',)
fields = (
'source',
'destination',
'reason',
'amount',
)
# Voir ci-dessus
widgets = {
'source': autocomplete.ModelSelect2(url='note:note_autocomplete',
'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},),
'destination': autocomplete.ModelSelect2(url='note:note_autocomplete',
},
),
'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
},),
},
),
}
class ConsoForm(forms.ModelForm):
class ConsoForm(forms.ModelForm):
def save(self, commit=True):
button: TransactionTemplate = TransactionTemplate.objects.filter(name=self.data['button']).get()
button: TransactionTemplate = TransactionTemplate.objects.filter(
name=self.data['button']).get()
self.instance.destination = button.destination
self.instance.amount = button.amount
self.instance.transaction_type = 'bouton'
@ -65,9 +81,12 @@ class ConsoForm(forms.ModelForm):
# 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',
'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={
'data-placeholder': 'Note ...',
'data-minimum-input-length': 1,
}),
},
),
}

View File

@ -10,7 +10,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
"""
@ -34,8 +33,7 @@ class Note(PolymorphicModel):
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'),
@ -85,7 +83,8 @@ class Note(PolymorphicModel):
"""
Verify alias (simulate save)
"""
aliases = Alias.objects.filter(normalized_name=Alias.normalize(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:
@ -181,15 +180,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,
@ -209,11 +208,9 @@ 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()
for cat in {'M', 'P', 'Z', 'C'})).casefold()
def save(self, *args, **kwargs):
"""
@ -229,7 +226,8 @@ class Alias(models.Model):
raise ValidationError(_('Alias too long.'))
try:
if self != Alias.objects.get(normalized_name=normalized_name):
raise ValidationError(_('An alias with a similar name '
raise ValidationError(
_('An alias with a similar name '
'already exists.'))
except Alias.DoesNotExist:
pass

View File

@ -7,7 +7,10 @@ from .models.transactions import Transaction
class HistoryTable(tables.Table):
class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'}
attrs = {
'class':
'table table-bordered table-condensed table-striped table-hover'
}
model = Transaction
template_name = 'django_tables2/bootstrap.html'
sequence = ('...', 'total', 'valid')
@ -17,6 +20,6 @@ class HistoryTable(tables.Table):
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')
total=F('amount') *
F('quantity')).order_by(('-' if is_descending else '') + 'total')
return (QuerySet, True)

View File

@ -3,9 +3,16 @@ from django import template
def pretty_money(value):
if value % 100 == 0:
return "{:s}{:d}".format("- " if value < 0 else "", abs(value) // 100)
return "{:s}{:d}".format(
"- " if value < 0 else "",
abs(value) // 100,
)
else:
return "{:s}{:d}{:02d}".format("- " if value < 0 else "", abs(value) // 100, abs(value) % 100)
return "{:s}{:d}{:02d}".format(
"- " if value < 0 else "",
abs(value) // 100,
abs(value) % 100,
)
register = template.Library()

View File

@ -12,6 +12,7 @@ from django.views.generic import CreateView, ListView, DetailView, UpdateView
from .models import Note, Transaction, TransactionCategory, TransactionTemplate, Alias
from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
class TransactionCreate(LoginRequiredMixin, CreateView):
"""
Show transfer page
@ -30,7 +31,6 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
'to one or others')
return context
def get_form(self, form_class=None):
"""
If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
@ -56,7 +56,6 @@ 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.
@ -108,6 +107,7 @@ class TransactionTemplateCreateView(LoginRequiredMixin,CreateView):
model = TransactionTemplate
form_class = TransactionTemplateForm
class TransactionTemplateListView(LoginRequiredMixin, ListView):
"""
List TransactionsTemplates
@ -115,12 +115,14 @@ class TransactionTemplateListView(LoginRequiredMixin,ListView):
model = TransactionTemplate
form_class = TransactionTemplateForm
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
"""
"""
model = TransactionTemplate
form_class = TransactionTemplateForm
class ConsoView(LoginRequiredMixin, CreateView):
"""
Consume
@ -139,11 +141,14 @@ class ConsoView(LoginRequiredMixin,CreateView):
if 'template_type' not in self.kwargs.keys():
return context
template_type = TransactionCategory.objects.filter(name=self.kwargs.get('template_type')).get()
context['buttons'] = TransactionTemplate.objects.filter(template_type=template_type)
template_type = TransactionCategory.objects.filter(
name=self.kwargs.get('template_type')).get()
context['buttons'] = TransactionTemplate.objects.filter(
template_type=template_type)
context['title'] = template_type
return context
def get_success_url(self):
return reverse('note:consos',args=(self.kwargs.get('template_type'),))
return reverse('note:consos',
args=(self.kwargs.get('template_type'), ))