diff --git a/.gitignore b/.gitignore index f1650504..b57ed74a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ coverage # PyCharm project settings .idea +# VSCode project settings +.vscode + # Local data secrets.py *.log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bceb5f51..291ed490 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 6ddf1f3c..00000000 --- a/.pylintrc +++ /dev/null @@ -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*(# )??$ - -# 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 - diff --git a/apps/activity/__init__.py b/apps/activity/__init__.py index 75df9e1f..195d5302 100644 --- a/apps/activity/__init__.py +++ b/apps/activity/__init__.py @@ -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' diff --git a/apps/activity/admin.py b/apps/activity/admin.py index 1efe272c..5ceb4e81 100644 --- a/apps/activity/admin.py +++ b/apps/activity/admin.py @@ -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 diff --git a/apps/activity/api/__init__.py b/apps/activity/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/activity/api/serializers.py b/apps/activity/api/serializers.py new file mode 100644 index 00000000..0b9302f1 --- /dev/null +++ b/apps/activity/api/serializers.py @@ -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__' diff --git a/apps/activity/api/urls.py b/apps/activity/api/urls.py new file mode 100644 index 00000000..79e0ba30 --- /dev/null +++ b/apps/activity/api/urls.py @@ -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) diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py new file mode 100644 index 00000000..5683d458 --- /dev/null +++ b/apps/activity/api/views.py @@ -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 diff --git a/apps/activity/apps.py b/apps/activity/apps.py index 29990f1b..bb72947f 100644 --- a/apps/activity/apps.py +++ b/apps/activity/apps.py @@ -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 diff --git a/apps/activity/models.py b/apps/activity/models.py index 4dbc5522..8f23060c 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -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 diff --git a/apps/api/urls.py b/apps/api/urls.py new file mode 100644 index 00000000..7e59a8c0 --- /dev/null +++ b/apps/api/urls.py @@ -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')), +] diff --git a/apps/member/__init__.py b/apps/member/__init__.py index ec189d6f..298d1dda 100644 --- a/apps/member/__init__.py +++ b/apps/member/__init__.py @@ -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' diff --git a/apps/member/admin.py b/apps/member/admin.py index f45d5f55..fb107377 100644 --- a/apps/member/admin.py +++ b/apps/member/admin.py @@ -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): diff --git a/apps/member/api/__init__.py b/apps/member/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/member/api/serializers.py b/apps/member/api/serializers.py new file mode 100644 index 00000000..f4df6799 --- /dev/null +++ b/apps/member/api/serializers.py @@ -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__' diff --git a/apps/member/api/urls.py b/apps/member/api/urls.py new file mode 100644 index 00000000..15bb83ca --- /dev/null +++ b/apps/member/api/urls.py @@ -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) diff --git a/apps/member/api/views.py b/apps/member/api/views.py new file mode 100644 index 00000000..79ba4c12 --- /dev/null +++ b/apps/member/api/views.py @@ -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 diff --git a/apps/member/apps.py b/apps/member/apps.py index 928c00e4..2d7f4ab7 100644 --- a/apps/member/apps.py +++ b/apps/member/apps.py @@ -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 diff --git a/apps/member/filters.py b/apps/member/filters.py index fb1a2128..418e52fc 100644 --- a/apps/member/filters.py +++ b/apps/member/filters.py @@ -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'), ) diff --git a/apps/member/forms.py b/apps/member/forms.py index 59d3fec2..66844cf4 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -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, + form=MembershipForm, + extra=2, + can_delete=True, +) -MemberFormSet = forms.modelformset_factory(Membership, - form=MembershipForm, - extra=2, - 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", - ) - ) + )) diff --git a/apps/member/models.py b/apps/member/models.py index 883f9b49..cd754bd8 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -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'), diff --git a/apps/member/signals.py b/apps/member/signals.py index 6688516b..4e945ad5 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -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 - diff --git a/apps/member/tables.py b/apps/member/tables.py index 4218948c..a6de17d2 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -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 diff --git a/apps/member/urls.py b/apps/member/urls.py index 9bcc1095..6a7ed5ce 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -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//',views.ClubDetailView.as_view(),name="club_detail"), - path('club//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/',views.UserDetailView.as_view(),name="user_detail"), - path('user//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//', views.ClubDetailView.as_view(), name="club_detail"), + path('club//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/', views.UserDetailView.as_view(), name="user_detail"), + path('user//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"), ] diff --git a/apps/member/views.py b/apps/member/views.py index 90ea5ec3..6f982c64 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -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() - profile = profile_form.save(commit=False) + 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']}) + if kwargs: + 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) diff --git a/apps/note/__init__.py b/apps/note/__init__.py index 4773b310..f7c331b2 100644 --- a/apps/note/__init__.py +++ b/apps/note/__init__.py @@ -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' diff --git a/apps/note/admin.py b/apps/note/admin.py index 298b91c0..3a9721ae 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -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', ) diff --git a/apps/note/api/__init__.py b/apps/note/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py new file mode 100644 index 00000000..db0e3531 --- /dev/null +++ b/apps/note/api/serializers.py @@ -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__' diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py new file mode 100644 index 00000000..54218796 --- /dev/null +++ b/apps/note/api/urls.py @@ -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) diff --git a/apps/note/api/views.py b/apps/note/api/views.py new file mode 100644 index 00000000..94b4a47a --- /dev/null +++ b/apps/note/api/views.py @@ -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 diff --git a/apps/note/apps.py b/apps/note/apps.py index c53f915a..4881e3b9 100644 --- a/apps/note/apps.py +++ b/apps/note/apps.py @@ -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', ) diff --git a/apps/note/forms.py b/apps/note/forms.py index d74fa5b4..e4fd344c 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -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, + }, + ), + } diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py index b00572ce..7e6cc310 100644 --- a/apps/note/models/__init__.py +++ b/apps/note/models/__init__.py @@ -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', ] diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index e3ab7931..3b616f0e 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -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 ' - 'already exists.')) + 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) diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 7a058607..4db2eda1 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -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): diff --git a/apps/note/signals.py b/apps/note/signals.py index 6e5d5c9e..ad376ee0 100644 --- a/apps/note/signals.py +++ b/apps/note/signals.py @@ -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 diff --git a/apps/note/tables.py b/apps/note/tables.py index 4d4e9608..43a1ef74 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -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) diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index ec0a0d5b..12530c6e 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -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() diff --git a/apps/note/urls.py b/apps/note/urls.py index 5e423d46..fea911f6 100644 --- a/apps/note/urls.py +++ b/apps/note/urls.py @@ -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//',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//', views.TransactionTemplateUpdateView.as_view(), name='template_update'), + path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'), + path('consos/', views.ConsoView.as_view(), name='consos'), + + # API for the note autocompleter + path('note-autocomplete/', views.NoteAutocomplete.as_view(model=Note), name='note_autocomplete'), ] diff --git a/apps/note/views.py b/apps/note/views.py index 08f4f630..167ef4f0 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -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') diff --git a/entrypoint.sh b/entrypoint.sh index da32571a..f05e962a 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -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 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po new file mode 100644 index 00000000..3aadf83e --- /dev/null +++ b/locale/de/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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 "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 96da86e1..bdf4fc8f 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 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 \n" "Language-Team: LANGUAGE \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 diff --git a/note_kfet/middlewares.py b/note_kfet/middlewares.py new file mode 100644 index 00000000..73b87e36 --- /dev/null +++ b/note_kfet/middlewares.py @@ -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 + diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 234e70b9..68a40b88 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -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 ! diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index e583d8a6..9019b4e0 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -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')), ] diff --git a/note_kfet/settings/development.py b/note_kfet/settings/development.py index f6d48776..60055ee2 100644 --- a/note_kfet/settings/development.py +++ b/note_kfet/settings/development.py @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + ######################## # Development Settings # ######################## diff --git a/note_kfet/settings/production.py b/note_kfet/settings/production.py index 02c46ed2..296c17a4 100644 --- a/note_kfet/settings/production.py +++ b/note_kfet/settings/production.py @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + ######################## # Production Settings # ######################## diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 88bb6bb9..303e229a 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -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')), ] diff --git a/note_kfet/wsgi.py b/note_kfet/wsgi.py index 94a8e054..b89430ec 100644 --- a/note_kfet/wsgi.py +++ b/note_kfet/wsgi.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2016-2019 by BDE # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/requirements.txt b/requirements.txt index 39b32fdf..21c24808 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/static/favicon/android-chrome-192x192.png b/static/favicon/android-chrome-192x192.png index 5b31f298..38b7efee 100644 Binary files a/static/favicon/android-chrome-192x192.png and b/static/favicon/android-chrome-192x192.png differ diff --git a/static/favicon/android-chrome-512x512.png b/static/favicon/android-chrome-512x512.png index bb9e4daa..321f2b5a 100644 Binary files a/static/favicon/android-chrome-512x512.png and b/static/favicon/android-chrome-512x512.png differ diff --git a/static/favicon/apple-touch-icon.png b/static/favicon/apple-touch-icon.png index c0b462bd..8f6be04d 100644 Binary files a/static/favicon/apple-touch-icon.png and b/static/favicon/apple-touch-icon.png differ diff --git a/static/favicon/browserconfig.xml b/static/favicon/browserconfig.xml index 49604f0e..c8771dff 100644 --- a/static/favicon/browserconfig.xml +++ b/static/favicon/browserconfig.xml @@ -3,7 +3,7 @@ - #da532c + #00a300 diff --git a/static/favicon/favicon-16x16.png b/static/favicon/favicon-16x16.png index 5db7ed94..14ff80a4 100644 Binary files a/static/favicon/favicon-16x16.png and b/static/favicon/favicon-16x16.png differ diff --git a/static/favicon/favicon-32x32.png b/static/favicon/favicon-32x32.png index a3d9263f..cb082734 100644 Binary files a/static/favicon/favicon-32x32.png and b/static/favicon/favicon-32x32.png differ diff --git a/static/favicon/favicon.ico b/static/favicon/favicon.ico index 53393cda..0bf544e0 100644 Binary files a/static/favicon/favicon.ico and b/static/favicon/favicon.ico differ diff --git a/static/favicon/mstile-150x150.png b/static/favicon/mstile-150x150.png index 378274c9..0c44361b 100644 Binary files a/static/favicon/mstile-150x150.png and b/static/favicon/mstile-150x150.png differ diff --git a/static/favicon/safari-pinned-tab.svg b/static/favicon/safari-pinned-tab.svg index a8425e8a..cb5555d6 100644 --- a/static/favicon/safari-pinned-tab.svg +++ b/static/favicon/safari-pinned-tab.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 05328d0c..4b5f9872 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,8 +6,7 @@ SPDX-License-Identifier: GPL-3.0-or-later - - + {% block title %}{{ title }}{% endblock title %} - {{ request.site.name }} @@ -23,19 +22,41 @@ SPDX-License-Identifier: GPL-3.0-or-later + {% if no_cache %} + + {% endif %} {# Bootstrap CSS #} + + {# JQuery, Bootstrap and Turbolinks JavaScript #} + + + + + + {# 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 %} - -
-