Merge branch 'master' into import_nk15

This commit is contained in:
Pierre-antoine Comby 2020-02-22 16:34:17 +01:00
commit 0db2881468
68 changed files with 2147 additions and 830 deletions

3
.gitignore vendored
View File

@ -31,6 +31,9 @@ coverage
# PyCharm project settings
.idea
# VSCode project settings
.vscode
# Local data
secrets.py
*.log

View File

@ -2,20 +2,25 @@ image: python:3.6
stages:
- test
- quality-assurance
before_script:
- pip install tox
python36:
py36-django22:
image: python:3.6
stage: test
script: tox -e py36
script: tox -e py36-django22
python37:
py37-django22:
image: python:3.7
stage: test
script: tox -e py37
script: tox -e py37-django22
linters:
stage: test
image: python:3.6
stage: quality-assurance
script: tox -e linters
# Be nice to new contributors, but please use `tox`
allow_failure: true

379
.pylintrc
View File

@ -1,379 +0,0 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS,.git
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Use multiple processes to speed up Pylint.
jobs=4
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Allow optimization of some AST trees. This will activate a peephole AST
# optimizer, which will apply various small optimizations. For instance, it can
# be used to obtain the result of joining multiple strings with the addition
# operator. Joining a lot of strings can lead to a maximum recursion error in
# Pylint and this flag can prevent that. It has one side effect, the resulting
# AST will be different than the one from reality.
optimize-ast=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=INFERENCE_FAILURE
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin,map-builtin-not-iterating,delslice-method,apply-builtin,cmp-method,setslice-method,coerce-method,long-suffix,raising-string,import-star-module-level,buffer-builtin,reload-builtin,unpacking-in-except,print-statement,hex-method,old-octal-literal,metaclass-assignment,dict-iter-method,range-builtin-not-iterating,using-cmp-argument,indexing-exception,no-absolute-import,coerce-builtin,getslice-method,suppressed-message,execfile-builtin,round-builtin,useless-suppression,reduce-builtin,old-raise-syntax,zip-builtin-not-iterating,cmp-builtin,xrange-builtin,standarderror-builtin,old-division,oct-method,next-method-called,old-ne-operator,basestring-builtin
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=no
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[BASIC]
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=yes
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[ELIF]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=100
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set). This supports can work
# with qualified names.
ignored-classes=
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[DESIGN]
# Maximum number of arguments for function / method
max-args=20
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of attributes for a class (see R0902).
max-attributes=10
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of boolean expressions in an if statement
max-bool-expr=5
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

View File

@ -1,5 +1,4 @@
# -*- 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
default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,5 +1,4 @@
# -*- 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.contrib import admin
@ -12,7 +11,7 @@ class ActivityAdmin(admin.ModelAdmin):
Admin customisation for Activity
"""
list_display = ('name', 'activity_type', 'organizer')
list_filter = ('activity_type',)
list_filter = ('activity_type', )
search_fields = ['name', 'organizer__name']
# Organize activities by start date

View File

View File

@ -0,0 +1,36 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import ActivityType, Activity, Guest
class ActivityTypeSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Activity types.
The djangorestframework plugin will analyse the model `ActivityType` and parse all fields in the API.
"""
class Meta:
model = ActivityType
fields = '__all__'
class ActivitySerializer(serializers.ModelSerializer):
"""
REST API Serializer for Activities.
The djangorestframework plugin will analyse the model `Activity` and parse all fields in the API.
"""
class Meta:
model = Activity
fields = '__all__'
class GuestSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Guests.
The djangorestframework plugin will analyse the model `Guest` and parse all fields in the API.
"""
class Meta:
model = Guest
fields = '__all__'

13
apps/activity/api/urls.py Normal file
View File

@ -0,0 +1,13 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet
def register_activity_urls(router, path):
"""
Configure router for Activity REST API.
"""
router.register(path + '/activity', ActivityViewSet)
router.register(path + '/type', ActivityTypeViewSet)
router.register(path + '/guest', GuestViewSet)

View File

@ -0,0 +1,37 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import viewsets
from ..models import ActivityType, Activity, Guest
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
class ActivityTypeViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/type/
"""
queryset = ActivityType.objects.all()
serializer_class = ActivityTypeSerializer
class ActivityViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/activity/
"""
queryset = Activity.objects.all()
serializer_class = ActivitySerializer
class GuestViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
then render it on /api/activity/guest/
"""
queryset = Guest.objects.all()
serializer_class = GuestSerializer

View File

@ -1,5 +1,4 @@
# -*- 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

View File

@ -1,5 +1,4 @@
# -*- 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.conf import settings

51
apps/api/urls.py Normal file
View File

@ -0,0 +1,51 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf.urls import url, include
from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets
from activity.api.urls import register_activity_urls
from member.api.urls import register_members_urls
from note.api.urls import register_note_urls
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',
)
class UserViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
then render it on /api/users/
"""
queryset = User.objects.all()
serializer_class = UserSerializer
# Routers provide an easy way of automatically determining the URL conf.
# Register each app API router and user viewset
router = routers.DefaultRouter()
router.register('user', UserViewSet)
register_members_urls(router, 'members')
register_activity_urls(router, 'activity')
register_note_urls(router, 'note')
app_name = 'api'
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url('^', include(router.urls)),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

View File

@ -1,5 +1,4 @@
# -*- 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
default_app_config = 'member.apps.MemberConfig'

View File

@ -1,5 +1,4 @@
# -*- 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.contrib import admin
@ -19,9 +18,9 @@ class ProfileInline(admin.StackedInline):
class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,)
inlines = (ProfileInline, )
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
list_select_related = ('profile',)
list_select_related = ('profile', )
form = ProfileForm
def get_inline_instances(self, request, obj=None):

View File

View File

@ -0,0 +1,46 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import Profile, Club, Role, Membership
class ProfileSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Profiles.
The djangorestframework plugin will analyse the model `Profile` and parse all fields in the API.
"""
class Meta:
model = Profile
fields = '__all__'
class ClubSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Clubs.
The djangorestframework plugin will analyse the model `Club` and parse all fields in the API.
"""
class Meta:
model = Club
fields = '__all__'
class RoleSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Roles.
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
"""
class Meta:
model = Role
fields = '__all__'
class MembershipSerializer(serializers.ModelSerializer):
"""
REST API Serializer for Memberships.
The djangorestframework plugin will analyse the model `Memberships` and parse all fields in the API.
"""
class Meta:
model = Membership
fields = '__all__'

14
apps/member/api/urls.py Normal file
View File

@ -0,0 +1,14 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet
def register_members_urls(router, path):
"""
Configure router for Member REST API.
"""
router.register(path + '/profile', ProfileViewSet)
router.register(path + '/club', ClubViewSet)
router.register(path + '/role', RoleViewSet)
router.register(path + '/membership', MembershipViewSet)

47
apps/member/api/views.py Normal file
View File

@ -0,0 +1,47 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import viewsets
from ..models import Profile, Club, Role, Membership
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
class ProfileViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
then render it on /api/members/profile/
"""
queryset = Profile.objects.all()
serializer_class = ProfileSerializer
class ClubViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
then render it on /api/members/club/
"""
queryset = Club.objects.all()
serializer_class = ClubSerializer
class RoleViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
then render it on /api/members/role/
"""
queryset = Role.objects.all()
serializer_class = RoleSerializer
class MembershipViewSet(viewsets.ModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
then render it on /api/members/membership/
"""
queryset = Membership.objects.all()
serializer_class = MembershipSerializer

View File

@ -1,5 +1,4 @@
# -*- 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

View File

@ -1,31 +1,33 @@
# -*- 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_filters import FilterSet, CharFilter,NumberFilter
from django_filters import FilterSet, CharFilter
from django.contrib.auth.models import User
from django.db.models import CharField
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
from .models import Club
class UserFilter(FilterSet):
class Meta:
model = User
fields = ['last_name','first_name','username','profile__section']
filter_overrides={
CharField:{
'filter_class':CharFilter,
'extra': lambda f:{
'lookup_expr':'icontains'
fields = ['last_name', 'first_name', 'username', 'profile__section']
filter_overrides = {
CharField: {
'filter_class': CharFilter,
'extra': lambda f: {
'lookup_expr': 'icontains'
}
}
}
class UserFilterFormHelper(FormHelper):
form_method = 'GET'
layout = Layout(
'last_name','first_name','username','profile__section',
Submit('Submit','Apply Filter'),
'last_name',
'first_name',
'username',
'profile__section',
Submit('Submit', 'Apply Filter'),
)

View File

@ -1,26 +1,23 @@
# -*- 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.contrib.auth.forms import UserChangeForm, UserCreationForm
from dal import autocomplete
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django import forms
from .models import Profile, Club, Membership
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms import layout, bootstrap
from crispy_forms.bootstrap import InlineField, FormActions, StrictButton, Div, Field
from crispy_forms.bootstrap import Div
from crispy_forms.layout import Layout
class SignUpForm(UserCreationForm):
class Meta:
model = User
fields = ['first_name','last_name','username','email']
fields = ['first_name', 'last_name', 'username', 'email']
class ProfileForm(forms.ModelForm):
"""
@ -31,37 +28,56 @@ class ProfileForm(forms.ModelForm):
fields = '__all__'
exclude = ['user']
class ClubForm(forms.ModelForm):
class Meta:
model = Club
fields ='__all__'
fields = '__all__'
class AddMembersForm(forms.Form):
class Meta:
fields = ('',)
fields = ('', )
class MembershipForm(forms.ModelForm):
class Meta:
model = Membership
fields = ('user','roles','date_start')
fields = ('user', 'roles', 'date_start')
# 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 noms d'utilisateur valides
widgets = {
'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):
super().__init__(*args,**kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_tag = False
self.form_method = 'POST'
self.form_class='form-inline'
self.form_class = 'form-inline'
# self.template = 'bootstrap/table_inline_formset.html'
self.layout = Layout(
Div(
Div('user',css_class='col-sm-2'),
Div('roles',css_class='col-sm-2'),
Div('date_start',css_class='col-sm-2'),
Div('user', css_class='col-sm-2'),
Div('roles', css_class='col-sm-2'),
Div('date_start', css_class='col-sm-2'),
css_class="row formset-row",
)
)
))

View File

@ -1,14 +1,12 @@
# -*- 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.conf import settings
from django.db import models
from django.db.models.signals import post_save
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
@ -50,7 +48,8 @@ class Profile(models.Model):
verbose_name_plural = _('user profile')
def get_absolute_url(self):
return reverse('user_detail',args=(self.pk,))
return reverse('user_detail', args=(self.pk, ))
class Club(models.Model):
"""
@ -98,7 +97,7 @@ class Club(models.Model):
return self.name
def get_absolute_url(self):
return reverse_lazy('member:club_detail', args=(self.pk,))
return reverse_lazy('member:club_detail', args=(self.pk, ))
class Role(models.Model):
@ -118,6 +117,9 @@ class Role(models.Model):
verbose_name = _('role')
verbose_name_plural = _('roles')
def __str__(self):
return str(self.name)
class Membership(models.Model):
"""
@ -126,15 +128,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,2 @@
#!/usr/bin/env python
# -*- 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

View File

@ -1,19 +1,24 @@
#!/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 .models import Club
from django.conf import settings
from django.contrib.auth.models import User
from .models import Club
class ClubTable(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 = Club
template_name = 'django_tables2/bootstrap.html'
fields = ('id','name','email')
row_attrs = {'class':'table-row',
'data-href': lambda record: record.pk }
template_name = 'django_tables2/bootstrap4.html'
fields = ('id', 'name', 'email')
row_attrs = {
'class': 'table-row',
'data-href': lambda record: record.pk
}
class UserTable(tables.Table):
@ -21,7 +26,9 @@ class UserTable(tables.Table):
solde = tables.Column(accessor='note.balance')
class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'}
template_name = 'django_tables2/bootstrap.html'
fields = ('last_name','first_name','username','email')
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
template_name = 'django_tables2/bootstrap4.html'
fields = ('last_name', 'first_name', 'username', 'email')
model = User

View File

@ -1,7 +1,4 @@
#!/usr/bin/env python
# -*- 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
@ -10,12 +7,16 @@ from . import views
app_name = 'member'
urlpatterns = [
path('signup/',views.UserCreateView.as_view(),name="signup"),
path('club/',views.ClubListView.as_view(),name="club_list"),
path('club/<int:pk>/',views.ClubDetailView.as_view(),name="club_detail"),
path('club/<int:pk>/add_member/',views.ClubAddMemberView.as_view(),name="club_add_member"),
path('club/create/',views.ClubCreateView.as_view(),name="club_create"),
path('user/',views.UserListView.as_view(),name="user_list"),
path('user/<int:pk>',views.UserDetailView.as_view(),name="user_detail"),
path('user/<int:pk>/update',views.UserUpdateView.as_view(),name="user_update_profile"),
path('signup/', views.UserCreateView.as_view(), name="signup"),
path('club/', views.ClubListView.as_view(), name="club_list"),
path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
path('user/', views.UserListView.as_view(), name="user_list"),
path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"),
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

@ -1,29 +1,26 @@
#!/usr/bin/env python
# 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.shortcuts import redirect
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, DetailView, UpdateView
from django.http import HttpResponseRedirect
from django.contrib.auth.forms import UserCreationForm
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.contrib.auth.models import User
from django.urls import reverse_lazy
from django.db.models import Q
from django_tables2.views import SingleTableView
from .models import Profile, Club, Membership
from .forms import SignUpForm, ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper
from .tables import ClubTable,UserTable
from .filters import UserFilter, UserFilterFormHelper
from rest_framework.authtoken.models import Token
from note.models import Alias, NoteUser
from note.models.transactions import Transaction
from note.tables import HistoryTable
from .models import Profile, Club, Membership
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
from .tables import ClubTable, UserTable
from .filters import UserFilter, UserFilterFormHelper
class UserCreateView(CreateView):
"""
Une vue pour inscrire un utilisateur et lui créer un profile
@ -31,10 +28,10 @@ class UserCreateView(CreateView):
form_class = SignUpForm
success_url = reverse_lazy('login')
template_name ='member/signup.html'
template_name = 'member/signup.html'
second_form = ProfileForm
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form()
@ -49,40 +46,78 @@ class UserCreateView(CreateView):
profile.save()
return super().form_valid(form)
class UserUpdateView(LoginRequiredMixin,UpdateView):
class UserUpdateView(LoginRequiredMixin, UpdateView):
model = User
fields = ['first_name','last_name','username','email']
fields = ['first_name', 'last_name', 'username', 'email']
template_name = 'member/profile_update.html'
second_form = ProfileForm
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form(instance=context['user'].profile)
context['user_modified'] = context['user']
context['user'] = self.request.user
context["profile_form"] = self.second_form(
instance=context['user_modified'].profile)
context['title'] = _("Update Profile")
return context
def get_form(self, form_class=None):
form = super().get_form(form_class)
if 'username' not in form.data:
return form
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))
if note.exists() and note.get().user != self.request.user:
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():
user = form.save()
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))
if similar.exists():
similar.delete()
user = form.save(commit=False)
profile = profile_form.save(commit=False)
profile.user = user
profile.save()
user.save()
return super().form_valid(form)
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,))
return reverse_lazy('member:user_detail', args=(self.object.id, ))
class UserDetailView(LoginRequiredMixin,DetailView):
class UserDetailView(LoginRequiredMixin, DetailView):
"""
Affiche les informations sur un utilisateur, sa note, ses clubs ...
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 = \
@ -91,9 +126,14 @@ class UserDetailView(LoginRequiredMixin,DetailView):
club_list = \
Membership.objects.all().filter(user=user).only("club")
context['club_list'] = ClubTable(club_list)
context['title'] = _("Account #%(id)s: %(username)s") % {
'id': user.pk,
'username': user.username,
}
return context
class UserListView(LoginRequiredMixin,SingleTableView):
class UserListView(LoginRequiredMixin, SingleTableView):
"""
Affiche la liste des utilisateurs, avec une fonction de recherche statique
"""
@ -103,44 +143,91 @@ class UserListView(LoginRequiredMixin,SingleTableView):
filter_class = UserFilter
formhelper_class = UserFilterFormHelper
def get_queryset(self,**kwargs):
def get_queryset(self, **kwargs):
qs = super().get_queryset()
self.filter = self.filter_class(self.request.GET,queryset=qs)
self.filter = self.filter_class(self.request.GET, queryset=qs)
self.filter.form.helper = self.formhelper_class()
return self.filter.qs
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["filter"] = self.filter
return context
###################################
############## CLUB ###############
###################################
class ManageAuthTokens(LoginRequiredMixin, TemplateView):
"""
Affiche le jeton d'authentification, et permet de le regénérer
"""
model = Token
template_name = "member/manage_auth_tokens.html"
class ClubCreateView(LoginRequiredMixin,CreateView):
def get(self, request, *args, **kwargs):
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 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]
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.all()
if self.q:
qs = qs.filter(username__regex=self.q)
return qs
# ******************************* #
# CLUB #
# ******************************* #
class ClubCreateView(LoginRequiredMixin, CreateView):
"""
Create Club
"""
model = Club
form_class = ClubForm
def form_valid(self,form):
def form_valid(self, form):
return super().form_valid(form)
class ClubListView(LoginRequiredMixin,SingleTableView):
class ClubListView(LoginRequiredMixin, SingleTableView):
"""
List existing Clubs
"""
model = Club
table_class = ClubTable
class ClubDetailView(LoginRequiredMixin,DetailView):
model = Club
context_object_name="club"
def get_context_data(self,**kwargs):
class ClubDetailView(LoginRequiredMixin, DetailView):
model = Club
context_object_name = "club"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = context["club"]
club_transactions = \
@ -152,23 +239,30 @@ class ClubDetailView(LoginRequiredMixin,DetailView):
context['member_list'] = club_member
return context
class ClubAddMemberView(LoginRequiredMixin,CreateView):
class ClubAddMemberView(LoginRequiredMixin, CreateView):
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['formset'] = MemberFormSet()
context['helper'] = FormSetHelper()
context['no_cache'] = True
return context
def post(self,request,*args,**kwargs):
formset = MembershipFormset(request.POST)
if formset.is_valid():
return self.form_valid(formset)
else:
return self.form_invalid(formset)
def post(self, request, *args, **kwargs):
return
# TODO: Implement POST
# formset = MembershipFormset(request.POST)
# if formset.is_valid():
# return self.form_valid(formset)
# else:
# return self.form_invalid(formset)
def form_valid(self,formset):
def form_valid(self, formset):
formset.save()
return super().form_valid(formset)

View File

@ -1,5 +1,4 @@
# -*- 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
default_app_config = 'note.apps.NoteConfig'

View File

@ -1,5 +1,4 @@
# -*- 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.contrib import admin
@ -8,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
from .models.transactions import Transaction, TransactionTemplate
from .models.transactions import Transaction, TransactionCategory, TransactionTemplate
class AliasInlines(admin.TabularInline):
@ -25,7 +24,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')
@ -44,11 +46,12 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
"""
Child for a club note, see NoteAdmin
"""
inlines = (AliasInlines,)
inlines = (AliasInlines, )
# We can't change club after creation or the balance
readonly_fields = ('club', 'balance')
search_fields = ('club',)
search_fields = ('club', )
def has_add_permission(self, request):
"""
A club note should not be manually added
@ -67,7 +70,7 @@ class NoteSpecialAdmin(PolymorphicChildModelAdmin):
"""
Child for a special note, see NoteAdmin
"""
readonly_fields = ('balance',)
readonly_fields = ('balance', )
@admin.register(NoteUser)
@ -75,7 +78,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
"""
Child for an user note, see NoteAdmin
"""
inlines = (AliasInlines,)
inlines = (AliasInlines, )
# We can't change user after creation or the balance
readonly_fields = ('user', 'balance')
@ -101,7 +104,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):
"""
@ -136,8 +142,8 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
Admin customisation for TransactionTemplate
"""
list_display = ('name', 'poly_destination', 'amount', 'template_type')
list_filter = ('template_type',)
autocomplete_fields = ('destination',)
list_filter = ('template_type', )
autocomplete_fields = ('destination', )
def poly_destination(self, obj):
"""
@ -146,3 +152,12 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
return str(obj.destination)
poly_destination.short_description = _('destination')
@admin.register(TransactionCategory)
class TransactionCategoryAdmin(admin.ModelAdmin):
"""
Admin customisation for TransactionTemplate
"""
list_display = ('name', )
list_filter = ('name', )

View File

View File

@ -0,0 +1,103 @@
# 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__'

17
apps/note/api/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 .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)

161
apps/note/api/views.py Normal file
View File

@ -0,0 +1,161 @@
# 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

View File

@ -1,5 +1,4 @@
# -*- 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',
)

View File

@ -1,9 +1,94 @@
#!/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 .models import Transaction, TransactionTemplate
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):
self.instance.transaction_type = 'transfert'
super().save(commit)
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.transaction_type = 'bouton'
self.instance.reason = button.name
super().save(commit)
class Meta:
model = Transaction
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,
},
),
}

View File

@ -1,14 +1,13 @@
# -*- 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
TransactionCategory, TransactionTemplate
__all__ = [
# Notes
'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser',
# Transactions
'MembershipTransaction', 'Transaction', 'TransactionTemplate',
'MembershipTransaction', 'Transaction', 'TransactionCategory', 'TransactionTemplate',
]

View File

@ -1,5 +1,4 @@
# -*- 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
"""
@ -34,8 +32,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 +82,8 @@ 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:
@ -181,15 +179,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 +207,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 +225,13 @@ 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
def delete(self, using=None, keep_parents=False):
if self.name == str(self.note):
raise ValidationError(_("You can't delete your main alias."))
return super().delete(using, keep_parents)

View File

@ -1,5 +1,4 @@
# -*- 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
@ -7,16 +6,36 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
from .notes import Note,NoteClub
from .notes import Note, NoteClub
"""
Defines transactions
"""
class TransactionCategory(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 reccurent transaction
Defined a recurrent transaction
associated to selling something (a burger, a beer, ...)
"""
@ -35,9 +54,11 @@ class TransactionTemplate(models.Model):
verbose_name=_('amount'),
help_text=_('in centimes'),
)
template_type = models.CharField(
template_type = models.ForeignKey(
TransactionCategory,
on_delete=models.PROTECT,
verbose_name=_('type'),
max_length=31
max_length=31,
)
description = models.CharField(
@ -50,7 +71,7 @@ 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(models.Model):
@ -83,9 +104,7 @@ class Transaction(models.Model):
verbose_name=_('quantity'),
default=1,
)
amount = models.PositiveIntegerField(
verbose_name=_('amount'),
)
amount = models.PositiveIntegerField(verbose_name=_('amount'), )
transaction_type = models.CharField(
verbose_name=_('type'),
max_length=31,
@ -127,7 +146,7 @@ class Transaction(models.Model):
@property
def total(self):
return self.amount*self.quantity
return self.amount * self.quantity
class MembershipTransaction(Transaction):

View File

@ -1,5 +1,4 @@
# -*- 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

View File

@ -1,20 +1,26 @@
#!/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 .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-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)

View File

@ -1,11 +1,21 @@
# 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 "{:s}{:d}".format("- " if value < 0 else "", abs(value) // 100)
if value % 100 == 0:
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

@ -1,15 +1,19 @@
# -*- 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'),
]

View File

@ -1,13 +1,16 @@
# -*- 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
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,127 @@ 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']
return form
def form_valid(self, form):
"""
If the user has no right to transfer funds, then it will be the source of the transfer by default.
"""
if False: # TODO: fix it with "if %user has no right to transfer funds"
form.instance.source = self.request.user.note
return super().form_valid(form)
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 = Transaction
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.all() \
.order_by('template_type')
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')

View File

@ -1,6 +1,11 @@
#!/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

View File

@ -0,0 +1,517 @@
# 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-21 13:50+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:176 apps/note/models/transactions.py:23
#: apps/note/models/transactions.py:43 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
msgid "description"
msgstr ""
#: apps/activity/models.py:54 apps/note/models/notes.py:152
#: apps/note/models/transactions.py:60 apps/note/models/transactions.py:104
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/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:127
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 apps/note/models/notes.py:229
msgid "An alias with a similar name already exists."
msgstr ""
#: apps/member/views.py:129
#, python-format
msgid "Account #%(id)s: %(username)s"
msgstr ""
#: apps/note/admin.py:118 apps/note/models/transactions.py:86
msgid "source"
msgstr ""
#: apps/note/admin.py:126 apps/note/admin.py:154
#: apps/note/models/transactions.py:51 apps/note/models/transactions.py:92
msgid "destination"
msgstr ""
#: apps/note/apps.py:14 apps/note/models/notes.py:48
msgid "note"
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 "active"
msgstr ""
#: apps/note/models/notes.py:34
msgid ""
"Designates whether this note should be treated as active. Unselect this "
"instead of deleting notes."
msgstr ""
#: apps/note/models/notes.py:38
msgid "display image"
msgstr ""
#: apps/note/models/notes.py:43 apps/note/models/transactions.py:95
msgid "created at"
msgstr ""
#: apps/note/models/notes.py:49
msgid "notes"
msgstr ""
#: apps/note/models/notes.py:57
msgid "Note"
msgstr ""
#: apps/note/models/notes.py:67 apps/note/models/notes.py:90
msgid "This alias is already taken."
msgstr ""
#: apps/note/models/notes.py:105
msgid "user"
msgstr ""
#: apps/note/models/notes.py:109
msgid "one's note"
msgstr ""
#: apps/note/models/notes.py:110
msgid "users note"
msgstr ""
#: apps/note/models/notes.py:116
#, python-format
msgid "%(user)s's note"
msgstr ""
#: apps/note/models/notes.py:131
msgid "club note"
msgstr ""
#: apps/note/models/notes.py:132
msgid "clubs notes"
msgstr ""
#: apps/note/models/notes.py:138
#, python-format
msgid "Note of %(club)s club"
msgstr ""
#: apps/note/models/notes.py:158
msgid "special note"
msgstr ""
#: apps/note/models/notes.py:159
msgid "special notes"
msgstr ""
#: apps/note/models/notes.py:182
msgid "Invalid alias"
msgstr ""
#: apps/note/models/notes.py:198
msgid "alias"
msgstr ""
#: apps/note/models/notes.py:199 templates/member/profile_detail.html:33
msgid "aliases"
msgstr ""
#: apps/note/models/notes.py:225
msgid "Alias too long."
msgstr ""
#: apps/note/models/notes.py:236
msgid "You can't delete your main alias."
msgstr ""
#: apps/note/models/transactions.py:29
msgid "transaction category"
msgstr ""
#: apps/note/models/transactions.py:30
msgid "transaction categories"
msgstr ""
#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:102
msgid "amount"
msgstr ""
#: apps/note/models/transactions.py:55
msgid "in centimes"
msgstr ""
#: apps/note/models/transactions.py:65
msgid "transaction template"
msgstr ""
#: apps/note/models/transactions.py:66
msgid "transaction templates"
msgstr ""
#: apps/note/models/transactions.py:99
msgid "quantity"
msgstr ""
#: apps/note/models/transactions.py:108
msgid "reason"
msgstr ""
#: apps/note/models/transactions.py:112
msgid "valid"
msgstr ""
#: apps/note/models/transactions.py:117
msgid "transaction"
msgstr ""
#: apps/note/models/transactions.py:118
msgid "transactions"
msgstr ""
#: apps/note/models/transactions.py:160
msgid "membership transaction"
msgstr ""
#: apps/note/models/transactions.py:161
msgid "membership transactions"
msgstr ""
#: apps/note/views.py:29
msgid "Transfer money from your account to one or others"
msgstr ""
#: note_kfet/settings/base.py:148
msgid "German"
msgstr ""
#: note_kfet/settings/base.py:149
msgid "English"
msgstr ""
#: note_kfet/settings/base.py:150
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 ""

View File

@ -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-21 13:50+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,427 @@ 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:176 apps/note/models/transactions.py:23
#: apps/note/models/transactions.py:43 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
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:152
#: apps/note/models/transactions.py:60 apps/note/models/transactions.py:104
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/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:127
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 apps/note/models/notes.py:229
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
#: apps/member/views.py:129
#, python-format
msgid "Account #%(id)s: %(username)s"
msgstr "Compte n°%(id)s : %(username)s"
#: apps/note/admin.py:118 apps/note/models/transactions.py:86
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:126 apps/note/admin.py:154
#: apps/note/models/transactions.py:51 apps/note/models/transactions.py:92
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:48
msgid "note"
msgstr "note"
#: apps/note/models/notes.py:24
#: 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 "active"
msgstr "actif"
#: apps/note/models/notes.py:32
#: apps/note/models/notes.py:34
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:38
msgid "display image"
msgstr "image affichée"
#: apps/note/models/notes.py:42 apps/note/models/transactions.py:60
#: apps/note/models/notes.py:43 apps/note/models/transactions.py:95
msgid "created at"
msgstr "créée le"
#: apps/note/models/notes.py:48
#: apps/note/models/notes.py:49
msgid "notes"
msgstr "notes"
#: apps/note/models/notes.py:56
#: apps/note/models/notes.py:57
msgid "Note"
msgstr "Note"
#: apps/note/models/notes.py:66 apps/note/models/notes.py:88
#: apps/note/models/notes.py:67 apps/note/models/notes.py:90
msgid "This alias is already taken."
msgstr "Cet alias est déjà pris."
#: apps/note/models/notes.py:103
#: apps/note/models/notes.py:105
msgid "user"
msgstr "utilisateur"
#: apps/note/models/notes.py:107
#: apps/note/models/notes.py:109
msgid "one's note"
msgstr "note d'un utilisateur"
#: apps/note/models/notes.py:108
#: apps/note/models/notes.py:110
msgid "users note"
msgstr "notes des utilisateurs"
#: apps/note/models/notes.py:114
#: apps/note/models/notes.py:116
#, python-format
msgid "%(user)s's note"
msgstr "Note de %(user)s"
#: apps/note/models/notes.py:129
#: apps/note/models/notes.py:131
msgid "club note"
msgstr "note d'un club"
#: apps/note/models/notes.py:130
#: apps/note/models/notes.py:132
msgid "clubs notes"
msgstr "notes des clubs"
#: apps/note/models/notes.py:136
#: apps/note/models/notes.py:138
#, 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:158
msgid "special note"
msgstr "note spéciale"
#: apps/note/models/notes.py:156
#: apps/note/models/notes.py:159
msgid "special notes"
msgstr "notes spéciales"
#: apps/note/models/notes.py:173
#: apps/note/models/notes.py:182
msgid "Invalid alias"
msgstr "Alias invalide"
#: apps/note/models/notes.py:189
#: apps/note/models/notes.py:198
msgid "alias"
msgstr "alias"
#: apps/note/models/notes.py:190
#: apps/note/models/notes.py:199 templates/member/profile_detail.html:33
msgid "aliases"
msgstr "alias"
#: apps/note/models/notes.py:218
#: apps/note/models/notes.py:225
msgid "Alias too long."
msgstr "L'alias est trop long."
#: apps/note/models/notes.py:221
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
#: apps/note/models/notes.py:236
msgid "You can't delete your main alias."
msgstr "Vous ne pouvez pas supprimer votre alias principal."
#: apps/note/models/transactions.py:30 apps/note/models/transactions.py:68
#: apps/note/models/transactions.py:29
msgid "transaction category"
msgstr "catégorie de transaction"
#: apps/note/models/transactions.py:30
msgid "transaction categories"
msgstr "catégories de transaction"
#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:102
msgid "amount"
msgstr "montant"
#: apps/note/models/transactions.py:31
#: apps/note/models/transactions.py:55
msgid "in centimes"
msgstr "en centimes"
#: apps/note/models/transactions.py:39
#: apps/note/models/transactions.py:65
msgid "transaction template"
msgstr "modèle de transaction"
#: apps/note/models/transactions.py:40
#: apps/note/models/transactions.py:66
msgid "transaction templates"
msgstr "modèles de transaction"
#: apps/note/models/transactions.py:64
#: apps/note/models/transactions.py:99
msgid "quantity"
msgstr "quantité"
#: apps/note/models/transactions.py:75
#: apps/note/models/transactions.py:108
msgid "reason"
msgstr "raison"
#: apps/note/models/transactions.py:79
#: apps/note/models/transactions.py:112
msgid "valid"
msgstr "valide"
#: apps/note/models/transactions.py:84
#: apps/note/models/transactions.py:117
msgid "transaction"
msgstr "transaction"
#: apps/note/models/transactions.py:85
#: apps/note/models/transactions.py:118
msgid "transactions"
msgstr "transactions"
#: apps/note/models/transactions.py:118
#: apps/note/models/transactions.py:160
msgid "membership transaction"
msgstr "transaction d'adhésion"
#: apps/note/models/transactions.py:119
#: apps/note/models/transactions.py:161
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
#: note_kfet/settings/base.py:148
msgid "German"
msgstr ""
#: note_kfet/settings/base.py:149
msgid "English"
msgstr ""
#: note_kfet/settings.py:141
#: note_kfet/settings/base.py:150
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 "nom d'utilisateur"
#: 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 "Changer le mot de passe"
#: 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: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

38
note_kfet/middlewares.py Normal file
View File

@ -0,0 +1,38 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.http import HttpResponseRedirect
from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit
class TurbolinksMiddleware(object):
"""
Send the `Turbolinks-Location` header in response to a visit that was redirected,
and Turbolinks will replace the browser's topmost history entry.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
is_turbolinks = request.META.get('HTTP_TURBOLINKS_REFERRER')
is_response_redirect = response.has_header('Location')
if is_turbolinks:
if is_response_redirect:
location = response['Location']
prev_location = request.session.pop('_turbolinks_redirect_to', None)
if prev_location is not None:
# relative subsequent redirect
if location.startswith('.'):
location = prev_location.split('?')[0] + location
request.session['_turbolinks_redirect_to'] = location
else:
if request.session.get('_turbolinks_redirect_to'):
location = request.session.pop('_turbolinks_redirect_to')
response['Turbolinks-Location'] = location
return response

View File

@ -30,12 +30,17 @@ read_env()
app_stage = os.environ.get('DJANGO_APP_STAGE', 'dev')
if app_stage == 'prod':
from .production import *
DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD','CHANGE_ME_IN_ENV_SETTINGS');
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY','CHANGE_ME_IN_ENV_SETTINGS');
ALLOWED_HOSTS.append(os.environ.get('ALLOWED_HOSTS','localhost'));
DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD','CHANGE_ME_IN_ENV_SETTINGS')
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY','CHANGE_ME_IN_ENV_SETTINGS')
ALLOWED_HOSTS.append(os.environ.get('ALLOWED_HOSTS','localhost'))
else:
from .development import *
try:
from .secrets import *
except ImportError:
pass
# env variables set at the of in /env/bin/activate
# don't forget to unset in deactivate !

View File

@ -1,5 +1,4 @@
# -*- 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 os
@ -50,11 +49,18 @@ INSTALLED_APPS = [
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# API
'rest_framework',
'rest_framework.authtoken',
# Autocomplete
'dal',
'dal_select2',
# Note apps
'activity',
'member',
'note',
'api',
]
LOGIN_REDIRECT_URL = '/note/transfer/'
@ -69,6 +75,7 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.contrib.sites.middleware.CurrentSiteMiddleware',
'note_kfet.middlewares.TurbolinksMiddleware',
]
ROOT_URLCONF = 'note_kfet.urls'
@ -117,6 +124,18 @@ AUTHENTICATION_BACKENDS = (
'guardian.backends.ObjectPermissionBackend',
)
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
# TODO Maybe replace it with our custom permissions system
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
]
}
ANONYMOUS_USER_NAME = None # Disable guardian anonymous user
GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type'
@ -127,6 +146,7 @@ GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_c
LANGUAGE_CODE = 'en'
LANGUAGES = [
('de', _('German')),
('en', _('English')),
('fr', _('French')),
]

View File

@ -1,3 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
########################
# Development Settings #
########################

View File

@ -1,3 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
########################
# Production Settings #
########################

View File

@ -1,5 +1,4 @@
# -*- 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.contrib import admin
@ -19,4 +18,7 @@ urlpatterns = [
path('accounts/', include('django.contrib.auth.urls')),
path('admin/doc/', include('django.contrib.admindocs.urls')),
path('admin/', admin.site.urls),
# Include Django REST API
path('api/', include('api.urls')),
]

View File

@ -1,4 +1,3 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2016-2019 by BDE
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,16 +1,20 @@
certifi==2019.6.16
chardet==3.0.4
defusedxml==0.6.0
Django==2.2.3
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
django-guardian==2.1.0
django-polymorphic==2.0.3
djangorestframework==3.9.0
django-rest-polymorphic==0.1.8
django-reversion==3.0.3
django-tables2==2.1.0
docutils==0.14
psycopg2==2.8.4
idna==2.8
oauthlib==3.1.0
Pillow==6.1.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -3,7 +3,7 @@
<msapplication>
<tile>
<square150x150logo src="/static/favicon/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
<TileColor>#00a300</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 690 B

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@ -6,8 +6,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<html lang="en" class="position-relative h-100">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>
{% block title %}{{ title }}{% endblock title %} - {{ request.site.name }}
</title>
@ -23,19 +22,41 @@ SPDX-License-Identifier: GPL-3.0-or-later
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{% static "favicon/browserconfig.xml" %}">
<meta name="theme-color" content="#ffffff">
{% if no_cache %}
<meta name="turbolinks-cache-control" content="no-cache">
{% endif %}
{# Bootstrap CSS #}
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css"
integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous">
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
{# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
crossorigin="anonymous"></script>
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
{% if form.media %}
{{ form.media }}
{% endif %}
{% block extracss %}{% endblock %}
</head>
<body>
<main>
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar">
<body class="d-flex w-100 h-100 flex-column">
<main class="mb-auto">
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm">
<a class="navbar-brand" href="/">{{ request.site.name }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarNavAltMarkup"
@ -46,7 +67,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
<div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" href="#"><i class="fa fa-coffee"></i> Consos</a>
<a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> Consos</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> Clubs</a>
@ -89,31 +110,23 @@ SPDX-License-Identifier: GPL-3.0-or-later
</ul>
</div>
</nav>
<div class="container-fluid mb-5 mt-2">
<div class="row">
<div class="col-md-1">
{% block sidebar %}
{% endblock %}
</div>
<div class="col-md-10 text-justify">
<div class="container-fluid my-3" style="max-width: 1600px;">
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
{% block content %}
<p>Default content...</p>
{% endblock content %}
</div>
</div>
</div>
</main>
<footer class="bg-light fixed-bottom py-2">
<footer class="bg-light mt-auto py-2">
<div class="container-fluid">
<div class="row">
<div class="col-sm">
<form action="{% url 'set_language' %}" method="post"
class="form-inline">
<span class="text-muted mr-1">
NoteKfet2020 -
NoteKfet2020 &mdash;
<a href="mailto:tresorie.bde@lists.crans.org"
class="text-muted">Nous contacter</a> -
class="text-muted">Nous contacter</a> &mdash;
</span>
{% csrf_token %}
<select title="language" name="language"
@ -142,16 +155,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
</div>
</footer>
{# Bootstrap JavaScript #}
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js"
integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js"
integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy"
crossorigin="anonymous"></script>
{% block extrajavascript %}
{% endblock extrajavascript %}
</body>

View File

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% load i18n static pretty_money django_tables2 %}
{% block content %}
<div class="alert alert-info">
<h4>À quoi sert un jeton d'authentification ?</h4>
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a>.<br />
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token &lt;TOKEN&gt;</code>
pour pouvoir vous identifier.<br /><br />
Une documentation de l'API arrivera ultérieurement.
</div>
<div class="alert alert-info">
<strong>{%trans 'Token' %} :</strong>
{% if 'show' in request.GET %}
{{ token.key }} (<a href="?">cacher</a>)
{% else %}
<em>caché</em> (<a href="?show">montrer</a>)
{% endif %}
<br />
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
</div>
<div class="alert alert-warning">
<strong>Attention :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
</div>
<a href="?regenerate">
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
</a>
{% endblock %}

View File

@ -2,64 +2,76 @@
{% load i18n static pretty_money django_tables2 %}
{% block content %}
<h3>Compte n° {{ object.pk }}</h3>
<img src="{{ object.note.display_image.url }}" alt=""/>
<div class="row mt-4">
<div class="col-md-3 mb-4">
<div class="card bg-light shadow">
<img src="{{ object.note.display_image.url }}" class="card-img-top" alt="">
<div class="card-body">
<dl class="row">
<dt class="col-6 col-md-3">{% trans 'name'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.user.name }}</dd>
<dt class="col-6 col-md-3">{% trans 'first name'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.user.first_name }}</dd>
<dt class="col-6 col-md-3">{% trans 'username'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.user.username }}</dd>
<dt class="col-6 col-md-3">Aliases</dt>
<dd class="col-6 col-md-3">{{ object.user.note.aliases_set.all }}</dd>
<dt class="col-6 col-md-3">{% trans 'section'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.section }}</dd>
<dt class="col-6 col-md-3">{% trans 'address'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.address }}</dd>
<dt class="col-6 col-md-3">{% trans 'balance'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.user.note.balance | pretty_money }}</dd>
</dl>
<center>
<a class="btn btn-primary" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a>
<a class="btn btn-primary" href="{% url 'password_change' %}">{% trans 'Change password' %}</a>
</center>
<dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt>
<dd class="col-xl-6">{{ object.user.last_name }} {{ object.user.first_name }}</dd>
<div class="accordion" id="accordionProfile">
<div class="card">
<div class="card-header" id="headingOne">
<h5 class="mb-0">
<button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<i class="fa fa-users"></i> {% trans "View my memberships" %}
</button>
</h5>
<dt class="col-xl-6">{% trans 'username'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.username }}</dd>
<dt class="col-xl-6">{% trans 'password'|capfirst %}</dt>
<dd class="col-xl-6">
<a class="small" href="{% url 'password_change' %}">
{% trans 'Change password' %}
</a>
</dd>
<dt class="col-xl-6">{% trans 'section'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.section }}</dd>
<dt class="col-xl-6">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.address }}</dd>
<dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.note.balance | pretty_money }}</dd>
<dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt>
<dd class="col-xl-6">{{ object.user.note.alias_set.all|join:", " }}</dd>
</dl>
{% if object.user.pk == user.pk %}
<a class="small" href="{% url 'member:auth_token' %}">{% trans 'Manage auth token' %}</a>
{% endif %}
</div>
<div class="card-footer">
<a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a>
</div>
</div>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordionProfile">
<div class="card-body">
<div class="col-md-9">
<div class="accordion shadow" id="accordionProfile">
<div class="card">
<div class="card-header position-relative" id="clubListHeading">
<a class="btn btn-link stretched-link font-weight-bold"
data-toggle="collapse" data-target="#clubListCollapse"
aria-expanded="true" aria-controls="clubListCollapse">
<i class="fa fa-users"></i> {% trans "View my memberships" %}
</a>
</div>
<div id="clubListCollapse" class="collapse show" style="overflow:auto hidden" aria-labelledby="clubListHeading" data-parent="#accordionProfile">
{% render_table club_list %}
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingTwo">
<h5 class="mb-0">
<button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<div class="card-header position-relative" id="historyListHeading">
<a class="btn btn-link stretched-link collapsed font-weight-bold"
data-toggle="collapse" data-target="#historyListCollapse"
aria-expanded="false" aria-controls="historyListCollapse">
<i class="fa fa-euro"></i> Historique des transactions
</button>
</h5>
</a>
</div>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionProfile">
<div class="card-body">
<div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile">
{% render_table history_list %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@ -1,17 +1,16 @@
<!doctype html>
{% extends "base.html" %}
{% load crispy_forms_tags %}
{% load i18n static pretty_money django_tables2 %}
{% load i18n crispy_forms_tags %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
{{ profile_form|crispy }}
<button class="btn btn-link" type="submit">
<button class="btn btn-primary" type="submit">
{% trans "Save Changes" %}
</button>
</form>
</form>
{% endblock %}

View File

@ -0,0 +1,97 @@
{% extends "base.html" %}
{% load i18n static pretty_money %}
{# Remove page title #}
{% block contenttitle %}{% endblock %}
{% block content %}
{# Regroup buttons under categories #}
{% regroup transaction_templates by template_type as template_types %}
<form method="post" onsubmit="window.onbeforeunload=null">
{% csrf_token %}
<div class="row">
<div class="col-sm-5 mb-4">
{% if form.non_field_errors %}
<p class="errornote">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</p>
{% endif %}
{% for field in form %}
<div class="form-row{% if field.errors %} errors{% endif %}">
{{ field.errors }}
<div>
{{ field.label_tag }}
{% if field.is_readonly %}
<div class="readonly">{{ field.contents }}</div>
{% else %}
{{ field }}
{% endif %}
{% if field.field.help_text %}
<div class="help">{{ field.field.help_text|safe }}</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<div class="col-sm-7">
<div class="card text-center shadow">
{# Tabs for button categories #}
<div class="card-header">
<ul class="nav nav-tabs nav-fill card-header-tabs">
{% for template_type in template_types %}
<li class="nav-item">
<a class="nav-link" data-toggle="tab" href="#{{ template_type.grouper|slugify }}">
{{ template_type.grouper }}
</a>
</li>
{% endfor %}
</ul>
</div>
{# Tabs content #}
<div class="card-body">
<div class="tab-content">
{% for template_type in template_types %}
<div class="tab-pane" id="{{ template_type.grouper|slugify }}">
<div class="d-inline-flex flex-wrap justify-content-center">
{% for button in template_type.list %}
<button class="btn btn-outline-dark rounded-0 flex-fill"
name="button" value="{{ button.name }}">
{{ button.name }} ({{ button.amount | pretty_money }})
</button>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(document).ready(function() {
// If hash of a category in the URL, then select this category
// else select the first one
if (location.hash) {
$("a[href='" + location.hash + "']").tab("show");
} else {
$("a[data-toggle='tab']").first().tab("show");
}
// When selecting a category, change URL
$(document.body).on("click", "a[data-toggle='tab']", function(event) {
location.hash = this.getAttribute("href");
});
});
</script>
{% endblock %}

22
tox.ini
View File

@ -1,9 +1,13 @@
[tox]
envlist = py36,py37,linters
envlist =
py36-django22
py37-django22
linters
skipsdist = True
[testenv]
basepython = python3
setenv =
PYTHONWARNINGS = all
deps =
-r{toxinidir}/requirements.txt
coverage
@ -12,11 +16,6 @@ commands =
coverage run ./manage.py test {posargs}
coverage report -m
[testenv:pre-commit]
deps = pre-commit
commands =
pre-commit run --all-files --show-diff-on-failure
[testenv:linters]
deps =
-r{toxinidir}/requirements.txt
@ -26,13 +25,12 @@ deps =
flake8-typing-imports
pep8-naming
pyflakes
pylint
commands =
flake8 app/activity app/member app/note
pylint .
flake8 apps/activity apps/api apps/member apps/note
[flake8]
ignore = D203, W503, E203
# Ignore too many errors, should be reduced in the future
ignore = D203, W503, E203, I100, I101
exclude =
.tox,
.git,
@ -45,7 +43,7 @@ exclude =
.eggs,
*migrations*
max-complexity = 10
max-line-length = 160
import-order-style = google
application-import-names = flake8
format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s