Merge branch 'master' into import_nk15
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						| @@ -31,6 +31,9 @@ coverage | ||||
| # PyCharm project settings | ||||
| .idea | ||||
|  | ||||
| # VSCode project settings | ||||
| .vscode | ||||
|  | ||||
| # Local data | ||||
| secrets.py | ||||
| *.log | ||||
|   | ||||
| @@ -2,20 +2,25 @@ image: python:3.6 | ||||
|  | ||||
| stages: | ||||
|   - test | ||||
|   - quality-assurance | ||||
|  | ||||
| before_script: | ||||
|   - pip install tox | ||||
|  | ||||
| python36: | ||||
| py36-django22: | ||||
|   image: python:3.6 | ||||
|   stage: test | ||||
|   script: tox -e py36 | ||||
|   script: tox -e py36-django22 | ||||
|  | ||||
| python37: | ||||
| py37-django22: | ||||
|   image: python:3.7 | ||||
|   stage: test | ||||
|   script: tox -e py37 | ||||
|   script: tox -e py37-django22 | ||||
|  | ||||
| linters: | ||||
|   stage: test | ||||
|   image: python:3.6 | ||||
|   stage: quality-assurance | ||||
|   script: tox -e linters | ||||
|  | ||||
|   # Be nice to new contributors, but please use `tox` | ||||
|   allow_failure: true | ||||
|   | ||||
							
								
								
									
										379
									
								
								.pylintrc
									
									
									
									
									
								
							
							
						
						| @@ -1,379 +0,0 @@ | ||||
| [MASTER] | ||||
|  | ||||
| # Specify a configuration file. | ||||
| #rcfile= | ||||
|  | ||||
| # Python code to execute, usually for sys.path manipulation such as | ||||
| # pygtk.require(). | ||||
| #init-hook= | ||||
|  | ||||
| # Add files or directories to the blacklist. They should be base names, not | ||||
| # paths. | ||||
| ignore=CVS,.git | ||||
|  | ||||
| # Pickle collected data for later comparisons. | ||||
| persistent=yes | ||||
|  | ||||
| # List of plugins (as comma separated values of python modules names) to load, | ||||
| # usually to register additional checkers. | ||||
| load-plugins= | ||||
|  | ||||
| # Use multiple processes to speed up Pylint. | ||||
| jobs=4 | ||||
|  | ||||
| # Allow loading of arbitrary C extensions. Extensions are imported into the | ||||
| # active Python interpreter and may run arbitrary code. | ||||
| unsafe-load-any-extension=no | ||||
|  | ||||
| # A comma-separated list of package or module names from where C extensions may | ||||
| # be loaded. Extensions are loading into the active Python interpreter and may | ||||
| # run arbitrary code | ||||
| extension-pkg-whitelist= | ||||
|  | ||||
| # Allow optimization of some AST trees. This will activate a peephole AST | ||||
| # optimizer, which will apply various small optimizations. For instance, it can | ||||
| # be used to obtain the result of joining multiple strings with the addition | ||||
| # operator. Joining a lot of strings can lead to a maximum recursion error in | ||||
| # Pylint and this flag can prevent that. It has one side effect, the resulting | ||||
| # AST will be different than the one from reality. | ||||
| optimize-ast=no | ||||
|  | ||||
|  | ||||
| [MESSAGES CONTROL] | ||||
|  | ||||
| # Only show warnings with the listed confidence levels. Leave empty to show | ||||
| # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED | ||||
| confidence=INFERENCE_FAILURE | ||||
|  | ||||
| # Enable the message, report, category or checker with the given id(s). You can | ||||
| # either give multiple identifier separated by comma (,) or put this option | ||||
| # multiple time. See also the "--disable" option for examples. | ||||
| #enable= | ||||
|  | ||||
| # Disable the message, report, category or checker with the given id(s). You | ||||
| # can either give multiple identifiers separated by comma (,) or put this | ||||
| # option multiple times (only on the command line, not in the configuration | ||||
| # file where it should appear only once).You can also use "--disable=all" to | ||||
| # disable everything first and then reenable specific checks. For example, if | ||||
| # you want to run only the similarities checker, you can use "--disable=all | ||||
| # --enable=similarities". If you want to run only the classes checker, but have | ||||
| # no Warning level messages displayed, use"--disable=all --enable=classes | ||||
| # --disable=W" | ||||
| disable=intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin,map-builtin-not-iterating,delslice-method,apply-builtin,cmp-method,setslice-method,coerce-method,long-suffix,raising-string,import-star-module-level,buffer-builtin,reload-builtin,unpacking-in-except,print-statement,hex-method,old-octal-literal,metaclass-assignment,dict-iter-method,range-builtin-not-iterating,using-cmp-argument,indexing-exception,no-absolute-import,coerce-builtin,getslice-method,suppressed-message,execfile-builtin,round-builtin,useless-suppression,reduce-builtin,old-raise-syntax,zip-builtin-not-iterating,cmp-builtin,xrange-builtin,standarderror-builtin,old-division,oct-method,next-method-called,old-ne-operator,basestring-builtin | ||||
|  | ||||
|  | ||||
| [REPORTS] | ||||
|  | ||||
| # Set the output format. Available formats are text, parseable, colorized, msvs | ||||
| # (visual studio) and html. You can also give a reporter class, eg | ||||
| # mypackage.mymodule.MyReporterClass. | ||||
| output-format=text | ||||
|  | ||||
| # Put messages in a separate file for each module / package specified on the | ||||
| # command line instead of printing them on stdout. Reports (if any) will be | ||||
| # written in a file name "pylint_global.[txt|html]". | ||||
| files-output=no | ||||
|  | ||||
| # Tells whether to display a full report or only the messages | ||||
| reports=no | ||||
|  | ||||
| # Python expression which should return a note less than 10 (10 is the highest | ||||
| # note). You have access to the variables errors warning, statement which | ||||
| # respectively contain the number of errors / warnings messages and the total | ||||
| # number of statements analyzed. This is used by the global evaluation report | ||||
| # (RP0004). | ||||
| evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) | ||||
|  | ||||
| # Template used to display messages. This is a python new-style format string | ||||
| # used to format the message information. See doc for all details | ||||
| #msg-template= | ||||
|  | ||||
|  | ||||
| [BASIC] | ||||
|  | ||||
| # List of builtins function names that should not be used, separated by a comma | ||||
| bad-functions=map,filter | ||||
|  | ||||
| # Good variable names which should always be accepted, separated by a comma | ||||
| good-names=i,j,k,ex,Run,_ | ||||
|  | ||||
| # Bad variable names which should always be refused, separated by a comma | ||||
| bad-names=foo,bar,baz,toto,tutu,tata | ||||
|  | ||||
| # Colon-delimited sets of names that determine each other's naming style when | ||||
| # the name regexes allow several styles. | ||||
| name-group= | ||||
|  | ||||
| # Include a hint for the correct naming format with invalid-name | ||||
| include-naming-hint=yes | ||||
|  | ||||
| # Regular expression matching correct argument names | ||||
| argument-rgx=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Naming hint for argument names | ||||
| argument-name-hint=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Regular expression matching correct attribute names | ||||
| attr-rgx=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Naming hint for attribute names | ||||
| attr-name-hint=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Regular expression matching correct constant names | ||||
| const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ | ||||
|  | ||||
| # Naming hint for constant names | ||||
| const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ | ||||
|  | ||||
| # Regular expression matching correct class names | ||||
| class-rgx=[A-Z_][a-zA-Z0-9]+$ | ||||
|  | ||||
| # Naming hint for class names | ||||
| class-name-hint=[A-Z_][a-zA-Z0-9]+$ | ||||
|  | ||||
| # Regular expression matching correct inline iteration names | ||||
| inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ | ||||
|  | ||||
| # Naming hint for inline iteration names | ||||
| inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ | ||||
|  | ||||
| # Regular expression matching correct class attribute names | ||||
| class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ | ||||
|  | ||||
| # Naming hint for class attribute names | ||||
| class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ | ||||
|  | ||||
| # Regular expression matching correct function names | ||||
| function-rgx=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Naming hint for function names | ||||
| function-name-hint=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Regular expression matching correct module names | ||||
| module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ | ||||
|  | ||||
| # Naming hint for module names | ||||
| module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ | ||||
|  | ||||
| # Regular expression matching correct method names | ||||
| method-rgx=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Naming hint for method names | ||||
| method-name-hint=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Regular expression matching correct variable names | ||||
| variable-rgx=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Naming hint for variable names | ||||
| variable-name-hint=[a-z_][a-z0-9_]{2,30}$ | ||||
|  | ||||
| # Regular expression which should only match function or class names that do | ||||
| # not require a docstring. | ||||
| no-docstring-rgx=^_ | ||||
|  | ||||
| # Minimum line length for functions/classes that require docstrings, shorter | ||||
| # ones are exempt. | ||||
| docstring-min-length=-1 | ||||
|  | ||||
|  | ||||
| [ELIF] | ||||
|  | ||||
| # Maximum number of nested blocks for function / method body | ||||
| max-nested-blocks=5 | ||||
|  | ||||
|  | ||||
| [FORMAT] | ||||
|  | ||||
| # Maximum number of characters on a single line. | ||||
| max-line-length=100 | ||||
|  | ||||
| # Regexp for a line that is allowed to be longer than the limit. | ||||
| ignore-long-lines=^\s*(# )?<?https?://\S+>?$ | ||||
|  | ||||
| # Allow the body of an if to be on the same line as the test if there is no | ||||
| # else. | ||||
| single-line-if-stmt=no | ||||
|  | ||||
| # List of optional constructs for which whitespace checking is disabled. `dict- | ||||
| # separator` is used to allow tabulation in dicts, etc.: {1  : 1,\n222: 2}. | ||||
| # `trailing-comma` allows a space between comma and closing bracket: (a, ). | ||||
| # `empty-line` allows space-only lines. | ||||
| no-space-check=trailing-comma,dict-separator | ||||
|  | ||||
| # Maximum number of lines in a module | ||||
| max-module-lines=1000 | ||||
|  | ||||
| # String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1 | ||||
| # tab). | ||||
| indent-string='    ' | ||||
|  | ||||
| # Number of spaces of indent required inside a hanging  or continued line. | ||||
| indent-after-paren=4 | ||||
|  | ||||
| # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. | ||||
| expected-line-ending-format= | ||||
|  | ||||
|  | ||||
| [LOGGING] | ||||
|  | ||||
| # Logging modules to check that the string format arguments are in logging | ||||
| # function parameter format | ||||
| logging-modules=logging | ||||
|  | ||||
|  | ||||
| [MISCELLANEOUS] | ||||
|  | ||||
| # List of note tags to take in consideration, separated by a comma. | ||||
| notes=FIXME,XXX,TODO | ||||
|  | ||||
|  | ||||
| [SIMILARITIES] | ||||
|  | ||||
| # Minimum lines number of a similarity. | ||||
| min-similarity-lines=4 | ||||
|  | ||||
| # Ignore comments when computing similarities. | ||||
| ignore-comments=yes | ||||
|  | ||||
| # Ignore docstrings when computing similarities. | ||||
| ignore-docstrings=yes | ||||
|  | ||||
| # Ignore imports when computing similarities. | ||||
| ignore-imports=no | ||||
|  | ||||
|  | ||||
| [SPELLING] | ||||
|  | ||||
| # Spelling dictionary name. Available dictionaries: none. To make it working | ||||
| # install python-enchant package. | ||||
| spelling-dict= | ||||
|  | ||||
| # List of comma separated words that should not be checked. | ||||
| spelling-ignore-words= | ||||
|  | ||||
| # A path to a file that contains private dictionary; one word per line. | ||||
| spelling-private-dict-file= | ||||
|  | ||||
| # Tells whether to store unknown words to indicated private dictionary in | ||||
| # --spelling-private-dict-file option instead of raising a message. | ||||
| spelling-store-unknown-words=no | ||||
|  | ||||
|  | ||||
| [TYPECHECK] | ||||
|  | ||||
| # Tells whether missing members accessed in mixin class should be ignored. A | ||||
| # mixin class is detected if its name ends with "mixin" (case insensitive). | ||||
| ignore-mixin-members=yes | ||||
|  | ||||
| # List of module names for which member attributes should not be checked | ||||
| # (useful for modules/projects where namespaces are manipulated during runtime | ||||
| # and thus existing member attributes cannot be deduced by static analysis. It | ||||
| # supports qualified module names, as well as Unix pattern matching. | ||||
| ignored-modules= | ||||
|  | ||||
| # List of classes names for which member attributes should not be checked | ||||
| # (useful for classes with attributes dynamically set). This supports can work | ||||
| # with qualified names. | ||||
| ignored-classes= | ||||
|  | ||||
| # List of members which are set dynamically and missed by pylint inference | ||||
| # system, and so shouldn't trigger E1101 when accessed. Python regular | ||||
| # expressions are accepted. | ||||
| generated-members= | ||||
|  | ||||
|  | ||||
| [VARIABLES] | ||||
|  | ||||
| # Tells whether we should check for unused import in __init__ files. | ||||
| init-import=no | ||||
|  | ||||
| # A regular expression matching the name of dummy variables (i.e. expectedly | ||||
| # not used). | ||||
| dummy-variables-rgx=_$|dummy | ||||
|  | ||||
| # List of additional names supposed to be defined in builtins. Remember that | ||||
| # you should avoid to define new builtins when possible. | ||||
| additional-builtins= | ||||
|  | ||||
| # List of strings which can identify a callback function by name. A callback | ||||
| # name must start or end with one of those strings. | ||||
| callbacks=cb_,_cb | ||||
|  | ||||
|  | ||||
| [CLASSES] | ||||
|  | ||||
| # List of method names used to declare (i.e. assign) instance attributes. | ||||
| defining-attr-methods=__init__,__new__,setUp | ||||
|  | ||||
| # List of valid names for the first argument in a class method. | ||||
| valid-classmethod-first-arg=cls | ||||
|  | ||||
| # List of valid names for the first argument in a metaclass class method. | ||||
| valid-metaclass-classmethod-first-arg=mcs | ||||
|  | ||||
| # List of member names, which should be excluded from the protected access | ||||
| # warning. | ||||
| exclude-protected=_asdict,_fields,_replace,_source,_make | ||||
|  | ||||
|  | ||||
| [DESIGN] | ||||
|  | ||||
| # Maximum number of arguments for function / method | ||||
| max-args=20 | ||||
|  | ||||
| # Argument names that match this expression will be ignored. Default to name | ||||
| # with leading underscore | ||||
| ignored-argument-names=_.* | ||||
|  | ||||
| # Maximum number of locals for function / method body | ||||
| max-locals=20 | ||||
|  | ||||
| # Maximum number of return / yield for function / method body | ||||
| max-returns=6 | ||||
|  | ||||
| # Maximum number of branch for function / method body | ||||
| max-branches=12 | ||||
|  | ||||
| # Maximum number of statements in function / method body | ||||
| max-statements=50 | ||||
|  | ||||
| # Maximum number of parents for a class (see R0901). | ||||
| max-parents=7 | ||||
|  | ||||
| # Maximum number of attributes for a class (see R0902). | ||||
| max-attributes=10 | ||||
|  | ||||
| # Minimum number of public methods for a class (see R0903). | ||||
| min-public-methods=2 | ||||
|  | ||||
| # Maximum number of public methods for a class (see R0904). | ||||
| max-public-methods=20 | ||||
|  | ||||
| # Maximum number of boolean expressions in an if statement | ||||
| max-bool-expr=5 | ||||
|  | ||||
|  | ||||
| [IMPORTS] | ||||
|  | ||||
| # Deprecated modules which should not be used, separated by a comma | ||||
| deprecated-modules=optparse | ||||
|  | ||||
| # Create a graph of every (i.e. internal and external) dependencies in the | ||||
| # given file (report RP0402 must not be disabled) | ||||
| import-graph= | ||||
|  | ||||
| # Create a graph of external dependencies in the given file (report RP0402 must | ||||
| # not be disabled) | ||||
| ext-import-graph= | ||||
|  | ||||
| # Create a graph of internal dependencies in the given file (report RP0402 must | ||||
| # not be disabled) | ||||
| int-import-graph= | ||||
|  | ||||
|  | ||||
| [EXCEPTIONS] | ||||
|  | ||||
| # Exceptions that will emit a warning when being caught. Defaults to | ||||
| # "Exception" | ||||
| overgeneral-exceptions=Exception | ||||
|  | ||||
| @@ -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' | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										0
									
								
								apps/activity/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										36
									
								
								apps/activity/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from ..models import ActivityType, Activity, Guest | ||||
|  | ||||
|  | ||||
| class ActivityTypeSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Activity types. | ||||
|     The djangorestframework plugin will analyse the model `ActivityType` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = ActivityType | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class ActivitySerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Activities. | ||||
|     The djangorestframework plugin will analyse the model `Activity` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Activity | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class GuestSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Guests. | ||||
|     The djangorestframework plugin will analyse the model `Guest` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Guest | ||||
|         fields = '__all__' | ||||
							
								
								
									
										13
									
								
								apps/activity/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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) | ||||
							
								
								
									
										37
									
								
								apps/activity/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,37 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import viewsets | ||||
|  | ||||
| from ..models import ActivityType, Activity, Guest | ||||
| from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer | ||||
|  | ||||
|  | ||||
| class ActivityTypeViewSet(viewsets.ModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/activity/type/ | ||||
|     """ | ||||
|     queryset = ActivityType.objects.all() | ||||
|     serializer_class = ActivityTypeSerializer | ||||
|  | ||||
|  | ||||
| class ActivityViewSet(viewsets.ModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/activity/activity/ | ||||
|     """ | ||||
|     queryset = Activity.objects.all() | ||||
|     serializer_class = ActivitySerializer | ||||
|  | ||||
|  | ||||
| class GuestViewSet(viewsets.ModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/activity/guest/ | ||||
|     """ | ||||
|     queryset = Guest.objects.all() | ||||
|     serializer_class = GuestSerializer | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| # -*- mode: python; coding: utf-8 -*- | ||||
| # Copyright (C) 2018-2019 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.conf import settings | ||||
|   | ||||
							
								
								
									
										51
									
								
								apps/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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')), | ||||
| ] | ||||
| @@ -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' | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
							
								
								
									
										0
									
								
								apps/member/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										46
									
								
								apps/member/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,46 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from ..models import Profile, Club, Role, Membership | ||||
|  | ||||
|  | ||||
| class ProfileSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Profiles. | ||||
|     The djangorestframework plugin will analyse the model `Profile` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Profile | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class ClubSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Clubs. | ||||
|     The djangorestframework plugin will analyse the model `Club` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Club | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class RoleSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Roles. | ||||
|     The djangorestframework plugin will analyse the model `Role` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Role | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class MembershipSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Memberships. | ||||
|     The djangorestframework plugin will analyse the model `Memberships` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Membership | ||||
|         fields = '__all__' | ||||
							
								
								
									
										14
									
								
								apps/member/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,14 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet | ||||
|  | ||||
|  | ||||
| def register_members_urls(router, path): | ||||
|     """ | ||||
|     Configure router for Member REST API. | ||||
|     """ | ||||
|     router.register(path + '/profile', ProfileViewSet) | ||||
|     router.register(path + '/club', ClubViewSet) | ||||
|     router.register(path + '/role', RoleViewSet) | ||||
|     router.register(path + '/membership', MembershipViewSet) | ||||
							
								
								
									
										47
									
								
								apps/member/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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 | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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'), | ||||
|     ) | ||||
|   | ||||
| @@ -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", | ||||
|             ) | ||||
|         ) | ||||
|             )) | ||||
|   | ||||
| @@ -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'), | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -1,7 +1,4 @@ | ||||
| #!/usr/bin/env python | ||||
|  | ||||
| # -*- mode: python; coding: utf-8 -*- | ||||
| # Copyright (C) 2018-2019 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.urls import path | ||||
| @@ -10,12 +7,16 @@ from . import views | ||||
|  | ||||
| app_name = 'member' | ||||
| urlpatterns = [ | ||||
|     path('signup/',views.UserCreateView.as_view(),name="signup"), | ||||
|     path('club/',views.ClubListView.as_view(),name="club_list"), | ||||
|     path('club/<int:pk>/',views.ClubDetailView.as_view(),name="club_detail"), | ||||
|     path('club/<int:pk>/add_member/',views.ClubAddMemberView.as_view(),name="club_add_member"), | ||||
|     path('club/create/',views.ClubCreateView.as_view(),name="club_create"), | ||||
|     path('user/',views.UserListView.as_view(),name="user_list"), | ||||
|     path('user/<int:pk>',views.UserDetailView.as_view(),name="user_detail"), | ||||
|     path('user/<int:pk>/update',views.UserUpdateView.as_view(),name="user_update_profile"), | ||||
|     path('signup/', views.UserCreateView.as_view(), name="signup"), | ||||
|     path('club/', views.ClubListView.as_view(), name="club_list"), | ||||
|     path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"), | ||||
|     path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), | ||||
|     path('club/create/', views.ClubCreateView.as_view(), name="club_create"), | ||||
|     path('user/', views.UserListView.as_view(), name="user_list"), | ||||
|     path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"), | ||||
|     path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"), | ||||
|     path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), | ||||
|  | ||||
|     # API for the user autocompleter | ||||
|     path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"), | ||||
| ] | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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' | ||||
|   | ||||
| @@ -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', ) | ||||
|   | ||||
							
								
								
									
										0
									
								
								apps/note/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										103
									
								
								apps/note/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,103 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import serializers | ||||
| from rest_polymorphic.serializers import PolymorphicSerializer | ||||
|  | ||||
| from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias | ||||
| from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction | ||||
|  | ||||
|  | ||||
| class NoteSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Notes. | ||||
|     The djangorestframework plugin will analyse the model `Note` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Note | ||||
|         fields = '__all__' | ||||
|         extra_kwargs = { | ||||
|             'url': { | ||||
|                 'view_name': 'project-detail', | ||||
|                 'lookup_field': 'pk' | ||||
|             }, | ||||
|         } | ||||
|  | ||||
|  | ||||
| class NoteClubSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Club's notes. | ||||
|     The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = NoteClub | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class NoteSpecialSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for special notes. | ||||
|     The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = NoteSpecial | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class NoteUserSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for User's notes. | ||||
|     The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = NoteUser | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class AliasSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Aliases. | ||||
|     The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Alias | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class NotePolymorphicSerializer(PolymorphicSerializer): | ||||
|     model_serializer_mapping = { | ||||
|         Note: NoteSerializer, | ||||
|         NoteUser: NoteUserSerializer, | ||||
|         NoteClub: NoteClubSerializer, | ||||
|         NoteSpecial: NoteSpecialSerializer | ||||
|     } | ||||
|  | ||||
|  | ||||
| class TransactionTemplateSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Transaction templates. | ||||
|     The djangorestframework plugin will analyse the model `TransactionTemplate` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = TransactionTemplate | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class TransactionSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Transactions. | ||||
|     The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = Transaction | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class MembershipTransactionSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Membership transactions. | ||||
|     The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API. | ||||
|     """ | ||||
|     class Meta: | ||||
|         model = MembershipTransaction | ||||
|         fields = '__all__' | ||||
							
								
								
									
										17
									
								
								apps/note/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import NotePolymorphicViewSet, AliasViewSet, \ | ||||
|     TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet | ||||
|  | ||||
|  | ||||
| def register_note_urls(router, path): | ||||
|     """ | ||||
|     Configure router for Note REST API. | ||||
|     """ | ||||
|     router.register(path + '/note', NotePolymorphicViewSet) | ||||
|     router.register(path + '/alias', AliasViewSet) | ||||
|  | ||||
|     router.register(path + '/transaction/transaction', TransactionViewSet) | ||||
|     router.register(path + '/transaction/template', TransactionTemplateViewSet) | ||||
|     router.register(path + '/transaction/membership', MembershipTransactionViewSet) | ||||
							
								
								
									
										161
									
								
								apps/note/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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 | ||||
| @@ -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', | ||||
|         ) | ||||
|   | ||||
| @@ -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, | ||||
|                 }, | ||||
|             ), | ||||
|         } | ||||
|   | ||||
| @@ -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', | ||||
| ] | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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): | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -1,15 +1,19 @@ | ||||
| # -*- mode: python; coding: utf-8 -*- | ||||
| # Copyright (C) 2018-2019 by BDE ENS Paris-Saclay | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.urls import path | ||||
|  | ||||
| from . import views | ||||
| from .models import Note | ||||
|  | ||||
| app_name = 'note' | ||||
| urlpatterns = [ | ||||
|     path('transfer/', views.TransactionCreate.as_view(), name='transfer'), | ||||
|     path('buttons/create/',views.TransactionTemplateCreateView.as_view(),name='template_create'), | ||||
|     path('buttons/update/<int:pk>/',views.TransactionTemplateUpdateView.as_view(),name='template_update'), | ||||
|     path('buttons/',views.TransactionTemplateListView.as_view(),name='template_list') | ||||
|     path('buttons/create/', views.TransactionTemplateCreateView.as_view(), name='template_create'), | ||||
|     path('buttons/update/<int:pk>/', views.TransactionTemplateUpdateView.as_view(), name='template_update'), | ||||
|     path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'), | ||||
|     path('consos/', views.ConsoView.as_view(), name='consos'), | ||||
|  | ||||
|     # API for the note autocompleter | ||||
|     path('note-autocomplete/', views.NoteAutocomplete.as_view(model=Note), name='note_autocomplete'), | ||||
| ] | ||||
|   | ||||
| @@ -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') | ||||
|   | ||||
| @@ -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 | ||||
|  | ||||
|   | ||||
							
								
								
									
										517
									
								
								locale/de/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,517 @@ | ||||
| # SOME DESCRIPTIVE TITLE. | ||||
| # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER | ||||
| # This file is distributed under the same license as the PACKAGE package. | ||||
| # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. | ||||
| # | ||||
| #, fuzzy | ||||
| msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2020-02-21 13:50+0100\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| "Language: \n" | ||||
| "MIME-Version: 1.0\n" | ||||
| "Content-Type: text/plain; charset=UTF-8\n" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Plural-Forms: nplurals=2; plural=(n != 1);\n" | ||||
|  | ||||
| #: apps/activity/apps.py:10 apps/activity/models.py:76 | ||||
| msgid "activity" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:19 apps/activity/models.py:44 | ||||
| #: apps/member/models.py:60 apps/member/models.py:111 | ||||
| #: apps/note/models/notes.py:176 apps/note/models/transactions.py:23 | ||||
| #: apps/note/models/transactions.py:43 templates/member/profile_detail.html:11 | ||||
| msgid "name" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:23 | ||||
| msgid "can invite" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:26 | ||||
| msgid "guest entry fee" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:30 | ||||
| msgid "activity type" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:31 | ||||
| msgid "activity types" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:48 | ||||
| msgid "description" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:54 apps/note/models/notes.py:152 | ||||
| #: apps/note/models/transactions.py:60 apps/note/models/transactions.py:104 | ||||
| msgid "type" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:60 | ||||
| msgid "organizer" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:66 | ||||
| msgid "attendees club" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:69 | ||||
| msgid "start date" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:72 | ||||
| msgid "end date" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:77 | ||||
| msgid "activities" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:108 | ||||
| msgid "guest" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:109 | ||||
| msgid "guests" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/apps.py:10 | ||||
| msgid "member" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:23 | ||||
| msgid "phone number" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:29 templates/member/profile_detail.html:24 | ||||
| msgid "section" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:30 | ||||
| msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:36 templates/member/profile_detail.html:27 | ||||
| msgid "address" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:42 | ||||
| msgid "paid" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:47 apps/member/models.py:48 | ||||
| msgid "user profile" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:65 | ||||
| msgid "email" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:70 | ||||
| msgid "membership fee" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:74 | ||||
| msgid "membership duration" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:75 | ||||
| msgid "The longest time a membership can last (NULL = infinite)." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:80 | ||||
| msgid "membership start" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:81 | ||||
| msgid "How long after January 1st the members can renew their membership." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:86 | ||||
| msgid "membership end" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:87 | ||||
| msgid "" | ||||
| "How long the membership can last after January 1st of the next year after " | ||||
| "members can renew their membership." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:93 apps/note/models/notes.py:127 | ||||
| msgid "club" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:94 | ||||
| msgid "clubs" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:117 | ||||
| msgid "role" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:118 | ||||
| msgid "roles" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:142 | ||||
| msgid "membership starts on" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:145 | ||||
| msgid "membership ends on" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:149 | ||||
| msgid "fee" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:153 | ||||
| msgid "membership" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:154 | ||||
| msgid "memberships" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:63 templates/member/profile_detail.html:42 | ||||
| msgid "Update Profile" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:79 apps/note/models/notes.py:229 | ||||
| msgid "An alias with a similar name already exists." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:129 | ||||
| #, python-format | ||||
| msgid "Account #%(id)s: %(username)s" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/admin.py:118 apps/note/models/transactions.py:86 | ||||
| msgid "source" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/admin.py:126 apps/note/admin.py:154 | ||||
| #: apps/note/models/transactions.py:51 apps/note/models/transactions.py:92 | ||||
| msgid "destination" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/apps.py:14 apps/note/models/notes.py:48 | ||||
| msgid "note" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:26 | ||||
| msgid "account balance" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:27 | ||||
| msgid "in centimes, money credited for this instance" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:31 | ||||
| msgid "active" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:34 | ||||
| msgid "" | ||||
| "Designates whether this note should be treated as active. Unselect this " | ||||
| "instead of deleting notes." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:38 | ||||
| msgid "display image" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:43 apps/note/models/transactions.py:95 | ||||
| msgid "created at" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:49 | ||||
| msgid "notes" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:57 | ||||
| msgid "Note" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:67 apps/note/models/notes.py:90 | ||||
| msgid "This alias is already taken." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:105 | ||||
| msgid "user" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:109 | ||||
| msgid "one's note" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:110 | ||||
| msgid "users note" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:116 | ||||
| #, python-format | ||||
| msgid "%(user)s's note" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:131 | ||||
| msgid "club note" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:132 | ||||
| msgid "clubs notes" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:138 | ||||
| #, python-format | ||||
| msgid "Note of %(club)s club" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:158 | ||||
| msgid "special note" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:159 | ||||
| msgid "special notes" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:182 | ||||
| msgid "Invalid alias" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:198 | ||||
| msgid "alias" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:199 templates/member/profile_detail.html:33 | ||||
| msgid "aliases" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:225 | ||||
| msgid "Alias too long." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:236 | ||||
| msgid "You can't delete your main alias." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:29 | ||||
| msgid "transaction category" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:30 | ||||
| msgid "transaction categories" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:54 apps/note/models/transactions.py:102 | ||||
| msgid "amount" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:55 | ||||
| msgid "in centimes" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:65 | ||||
| msgid "transaction template" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:66 | ||||
| msgid "transaction templates" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:99 | ||||
| msgid "quantity" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:108 | ||||
| msgid "reason" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:112 | ||||
| msgid "valid" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:117 | ||||
| msgid "transaction" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:118 | ||||
| msgid "transactions" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:160 | ||||
| msgid "membership transaction" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:161 | ||||
| msgid "membership transactions" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/views.py:29 | ||||
| msgid "Transfer money from your account to one or others" | ||||
| msgstr "" | ||||
|  | ||||
| #: note_kfet/settings/base.py:148 | ||||
| msgid "German" | ||||
| msgstr "" | ||||
|  | ||||
| #: note_kfet/settings/base.py:149 | ||||
| msgid "English" | ||||
| msgstr "" | ||||
|  | ||||
| #: note_kfet/settings/base.py:150 | ||||
| msgid "French" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/base.html:13 | ||||
| msgid "The ENS Paris-Saclay BDE note." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_detail.html:10 | ||||
| msgid "Membership starts on" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_detail.html:12 | ||||
| msgid "Membership ends on" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_detail.html:14 | ||||
| msgid "Membership duration" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_detail.html:18 templates/member/profile_detail.html:30 | ||||
| msgid "balance" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/manage_auth_tokens.html:16 | ||||
| msgid "Token" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/manage_auth_tokens.html:23 | ||||
| msgid "Created" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/manage_auth_tokens.html:31 | ||||
| msgid "Regenerate token" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/profile_detail.html:11 | ||||
| msgid "first name" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/profile_detail.html:14 | ||||
| msgid "username" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/profile_detail.html:17 | ||||
| msgid "password" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/profile_detail.html:20 | ||||
| msgid "Change password" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/profile_detail.html:38 | ||||
| msgid "Manage auth token" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/profile_detail.html:54 | ||||
| msgid "View my memberships" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/profile_update.html:13 | ||||
| msgid "Save Changes" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/signup.html:14 | ||||
| msgid "Sign Up" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/note/transaction_form.html:35 | ||||
| msgid "Transfer" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/logged_out.html:8 | ||||
| msgid "Thanks for spending some quality time with the Web site today." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/logged_out.html:9 | ||||
| msgid "Log in again" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/login.html:7 templates/registration/login.html:8 | ||||
| #: templates/registration/login.html:22 | ||||
| #: templates/registration/password_reset_complete.html:10 | ||||
| msgid "Log in" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/login.html:13 | ||||
| #, python-format | ||||
| msgid "" | ||||
| "You are authenticated as %(username)s, but are not authorized to access this " | ||||
| "page. Would you like to login to a different account?" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/login.html:23 | ||||
| msgid "Forgotten your password or username?" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_change_done.html:8 | ||||
| msgid "Your password was changed." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_change_form.html:9 | ||||
| msgid "" | ||||
| "Please enter your old password, for security's sake, and then enter your new " | ||||
| "password twice so we can verify you typed it in correctly." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_change_form.html:11 | ||||
| #: templates/registration/password_reset_confirm.html:12 | ||||
| msgid "Change my password" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_reset_complete.html:8 | ||||
| msgid "Your password has been set.  You may go ahead and log in now." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_reset_confirm.html:9 | ||||
| msgid "" | ||||
| "Please enter your new password twice so we can verify you typed it in " | ||||
| "correctly." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_reset_confirm.html:15 | ||||
| msgid "" | ||||
| "The password reset link was invalid, possibly because it has already been " | ||||
| "used.  Please request a new password reset." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_reset_done.html:8 | ||||
| msgid "" | ||||
| "We've emailed you instructions for setting your password, if an account " | ||||
| "exists with the email you entered. You should receive them shortly." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_reset_done.html:9 | ||||
| msgid "" | ||||
| "If you don't receive an email, please make sure you've entered the address " | ||||
| "you registered with, and check your spam folder." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_reset_form.html:8 | ||||
| msgid "" | ||||
| "Forgotten your password? Enter your email address below, and we'll email " | ||||
| "instructions for setting a new one." | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/registration/password_reset_form.html:11 | ||||
| msgid "Reset my password" | ||||
| msgstr "" | ||||
| @@ -3,7 +3,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2019-08-14 15:14+0200\n" | ||||
| "POT-Creation-Date: 2020-02-21 13:50+0100\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @@ -13,356 +13,427 @@ msgstr "" | ||||
| "Content-Transfer-Encoding: 8bit\n" | ||||
| "Plural-Forms: nplurals=2; plural=(n > 1);\n" | ||||
|  | ||||
| #: apps/activity/apps.py:11 apps/activity/models.py:70 | ||||
| #: apps/activity/apps.py:10 apps/activity/models.py:76 | ||||
| msgid "activity" | ||||
| msgstr "activité" | ||||
|  | ||||
| #: apps/activity/models.py:15 apps/activity/models.py:38 | ||||
| #: apps/member/models.py:59 apps/member/models.py:107 | ||||
| #: apps/note/models/notes.py:167 apps/note/models/transactions.py:19 | ||||
| #: templates/member/profile_detail.html:10 | ||||
| #: apps/activity/models.py:19 apps/activity/models.py:44 | ||||
| #: apps/member/models.py:60 apps/member/models.py:111 | ||||
| #: apps/note/models/notes.py:176 apps/note/models/transactions.py:23 | ||||
| #: apps/note/models/transactions.py:43 templates/member/profile_detail.html:11 | ||||
| msgid "name" | ||||
| msgstr "nom" | ||||
|  | ||||
| #: apps/activity/models.py:19 | ||||
| #: apps/activity/models.py:23 | ||||
| msgid "can invite" | ||||
| msgstr "peut inviter" | ||||
|  | ||||
| #: apps/activity/models.py:22 | ||||
| #: apps/activity/models.py:26 | ||||
| msgid "guest entry fee" | ||||
| msgstr "cotisation de l'entrée invité" | ||||
|  | ||||
| #: apps/activity/models.py:26 | ||||
| #: apps/activity/models.py:30 | ||||
| msgid "activity type" | ||||
| msgstr "type d'activité" | ||||
|  | ||||
| #: apps/activity/models.py:27 | ||||
| #: apps/activity/models.py:31 | ||||
| msgid "activity types" | ||||
| msgstr "types d'activité" | ||||
|  | ||||
| #: apps/activity/models.py:42 | ||||
| #: apps/activity/models.py:48 | ||||
| msgid "description" | ||||
| msgstr "description" | ||||
|  | ||||
| #: apps/activity/models.py:48 apps/note/models/notes.py:149 | ||||
| #: apps/note/models/transactions.py:34 apps/note/models/transactions.py:71 | ||||
| #: apps/activity/models.py:54 apps/note/models/notes.py:152 | ||||
| #: apps/note/models/transactions.py:60 apps/note/models/transactions.py:104 | ||||
| msgid "type" | ||||
| msgstr "type" | ||||
|  | ||||
| #: apps/activity/models.py:54 | ||||
| #: apps/activity/models.py:60 | ||||
| msgid "organizer" | ||||
| msgstr "organisateur" | ||||
|  | ||||
| #: apps/activity/models.py:60 | ||||
| #: apps/activity/models.py:66 | ||||
| msgid "attendees club" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:63 | ||||
| #: apps/activity/models.py:69 | ||||
| msgid "start date" | ||||
| msgstr "date de début" | ||||
|  | ||||
| #: apps/activity/models.py:66 | ||||
| #: apps/activity/models.py:72 | ||||
| msgid "end date" | ||||
| msgstr "date de fin" | ||||
|  | ||||
| #: apps/activity/models.py:71 | ||||
| #: apps/activity/models.py:77 | ||||
| msgid "activities" | ||||
| msgstr "activités" | ||||
|  | ||||
| #: apps/activity/models.py:100 | ||||
| #: apps/activity/models.py:108 | ||||
| msgid "guest" | ||||
| msgstr "invité" | ||||
|  | ||||
| #: apps/activity/models.py:101 | ||||
| #: apps/activity/models.py:109 | ||||
| msgid "guests" | ||||
| msgstr "invités" | ||||
|  | ||||
| #: apps/member/apps.py:11 | ||||
| #: apps/member/apps.py:10 | ||||
| msgid "member" | ||||
| msgstr "adhérent" | ||||
|  | ||||
| #: apps/member/models.py:24 | ||||
| #: apps/member/models.py:23 | ||||
| msgid "phone number" | ||||
| msgstr "numéro de téléphone" | ||||
|  | ||||
| #: apps/member/models.py:30 templates/member/profile_detail.html:18 | ||||
| #: apps/member/models.py:29 templates/member/profile_detail.html:24 | ||||
| msgid "section" | ||||
| msgstr "section" | ||||
|  | ||||
| #: apps/member/models.py:31 | ||||
| #: apps/member/models.py:30 | ||||
| msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" | ||||
| msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" | ||||
|  | ||||
| #: apps/member/models.py:37 templates/member/profile_detail.html:20 | ||||
| #: apps/member/models.py:36 templates/member/profile_detail.html:27 | ||||
| msgid "address" | ||||
| msgstr "adresse" | ||||
|  | ||||
| #: apps/member/models.py:43 | ||||
| #: apps/member/models.py:42 | ||||
| msgid "paid" | ||||
| msgstr "payé" | ||||
|  | ||||
| #: apps/member/models.py:48 apps/member/models.py:49 | ||||
| #: apps/member/models.py:47 apps/member/models.py:48 | ||||
| msgid "user profile" | ||||
| msgstr "profil utilisateur" | ||||
|  | ||||
| #: apps/member/models.py:64 | ||||
| #: apps/member/models.py:65 | ||||
| msgid "email" | ||||
| msgstr "courriel" | ||||
|  | ||||
| #: apps/member/models.py:69 | ||||
| #: apps/member/models.py:70 | ||||
| msgid "membership fee" | ||||
| msgstr "cotisation pour adhérer" | ||||
|  | ||||
| #: apps/member/models.py:73 | ||||
| #: apps/member/models.py:74 | ||||
| msgid "membership duration" | ||||
| msgstr "durée de l'adhésion" | ||||
|  | ||||
| #: apps/member/models.py:74 | ||||
| #: apps/member/models.py:75 | ||||
| msgid "The longest time a membership can last (NULL = infinite)." | ||||
| msgstr "La durée maximale d'une adhésion (NULL = infinie)." | ||||
|  | ||||
| #: apps/member/models.py:79 | ||||
| #: apps/member/models.py:80 | ||||
| msgid "membership start" | ||||
| msgstr "début de l'adhésion" | ||||
|  | ||||
| #: apps/member/models.py:80 | ||||
| #: apps/member/models.py:81 | ||||
| msgid "How long after January 1st the members can renew their membership." | ||||
| msgstr "" | ||||
| "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur " | ||||
| "adhésion." | ||||
|  | ||||
| #: apps/member/models.py:85 | ||||
| #: apps/member/models.py:86 | ||||
| msgid "membership end" | ||||
| msgstr "fin de l'adhésion" | ||||
|  | ||||
| #: apps/member/models.py:86 | ||||
| #: apps/member/models.py:87 | ||||
| msgid "" | ||||
| "How long the membership can last after January 1st of the next year after " | ||||
| "members can renew their membership." | ||||
| msgstr "" | ||||
| "Combien de temps l'adhésion peut durer après le 1er Janvier de l'année " | ||||
| "suivante avant que les adhérents peuvent renouveler leur adhésion." | ||||
|  | ||||
| #: apps/member/models.py:92 apps/note/models/notes.py:125 | ||||
| #: apps/member/models.py:93 apps/note/models/notes.py:127 | ||||
| msgid "club" | ||||
| msgstr "club" | ||||
|  | ||||
| #: apps/member/models.py:93 | ||||
| #: apps/member/models.py:94 | ||||
| msgid "clubs" | ||||
| msgstr "clubs" | ||||
|  | ||||
| #: apps/member/models.py:113 | ||||
| #: apps/member/models.py:117 | ||||
| msgid "role" | ||||
| msgstr "rôle" | ||||
|  | ||||
| #: apps/member/models.py:114 | ||||
| #: apps/member/models.py:118 | ||||
| msgid "roles" | ||||
| msgstr "rôles" | ||||
|  | ||||
| #: apps/member/models.py:134 | ||||
| #: apps/member/models.py:142 | ||||
| msgid "membership starts on" | ||||
| msgstr "l'adhésion commence le" | ||||
|  | ||||
| #: apps/member/models.py:137 | ||||
| #: apps/member/models.py:145 | ||||
| msgid "membership ends on" | ||||
| msgstr "l'adhésion finie le" | ||||
|  | ||||
| #: apps/member/models.py:141 | ||||
| #: apps/member/models.py:149 | ||||
| msgid "fee" | ||||
| msgstr "cotisation" | ||||
|  | ||||
| #: apps/member/models.py:145 | ||||
| #: apps/member/models.py:153 | ||||
| msgid "membership" | ||||
| msgstr "adhésion" | ||||
|  | ||||
| #: apps/member/models.py:146 | ||||
| #: apps/member/models.py:154 | ||||
| msgid "memberships" | ||||
| msgstr "adhésions" | ||||
|  | ||||
| #: apps/note/admin.py:112 apps/note/models/transactions.py:51 | ||||
| #: apps/member/views.py:63 templates/member/profile_detail.html:42 | ||||
| msgid "Update Profile" | ||||
| msgstr "Modifier le profil" | ||||
|  | ||||
| #: apps/member/views.py:79 apps/note/models/notes.py:229 | ||||
| msgid "An alias with a similar name already exists." | ||||
| msgstr "Un alias avec un nom similaire existe déjà." | ||||
|  | ||||
| #: apps/member/views.py:129 | ||||
| #, python-format | ||||
| msgid "Account #%(id)s: %(username)s" | ||||
| msgstr "Compte n°%(id)s : %(username)s" | ||||
|  | ||||
| #: apps/note/admin.py:118 apps/note/models/transactions.py:86 | ||||
| msgid "source" | ||||
| msgstr "source" | ||||
|  | ||||
| #: apps/note/admin.py:120 apps/note/admin.py:148 | ||||
| #: apps/note/models/transactions.py:27 apps/note/models/transactions.py:57 | ||||
| #: apps/note/admin.py:126 apps/note/admin.py:154 | ||||
| #: apps/note/models/transactions.py:51 apps/note/models/transactions.py:92 | ||||
| msgid "destination" | ||||
| msgstr "destination" | ||||
|  | ||||
| #: apps/note/apps.py:15 apps/note/models/notes.py:47 | ||||
| #: apps/note/apps.py:14 apps/note/models/notes.py:48 | ||||
| msgid "note" | ||||
| msgstr "note" | ||||
|  | ||||
| #: apps/note/models/notes.py:24 | ||||
| #: apps/note/models/notes.py:26 | ||||
| msgid "account balance" | ||||
| msgstr "solde du compte" | ||||
|  | ||||
| #: apps/note/models/notes.py:25 | ||||
| #: apps/note/models/notes.py:27 | ||||
| msgid "in centimes, money credited for this instance" | ||||
| msgstr "en centimes, argent crédité pour cette instance" | ||||
|  | ||||
| #: apps/note/models/notes.py:29 | ||||
| #: apps/note/models/notes.py:31 | ||||
| msgid "active" | ||||
| msgstr "actif" | ||||
|  | ||||
| #: apps/note/models/notes.py:32 | ||||
| #: apps/note/models/notes.py:34 | ||||
| msgid "" | ||||
| "Designates whether this note should be treated as active. Unselect this " | ||||
| "instead of deleting notes." | ||||
| msgstr "" | ||||
| "Indique si la note est active. Désactiver cela plutôt que supprimer la note." | ||||
|  | ||||
| #: apps/note/models/notes.py:37 | ||||
| #: apps/note/models/notes.py:38 | ||||
| msgid "display image" | ||||
| msgstr "image affichée" | ||||
|  | ||||
| #: apps/note/models/notes.py:42 apps/note/models/transactions.py:60 | ||||
| #: apps/note/models/notes.py:43 apps/note/models/transactions.py:95 | ||||
| msgid "created at" | ||||
| msgstr "créée le" | ||||
|  | ||||
| #: apps/note/models/notes.py:48 | ||||
| #: apps/note/models/notes.py:49 | ||||
| msgid "notes" | ||||
| msgstr "notes" | ||||
|  | ||||
| #: apps/note/models/notes.py:56 | ||||
| #: apps/note/models/notes.py:57 | ||||
| msgid "Note" | ||||
| msgstr "Note" | ||||
|  | ||||
| #: apps/note/models/notes.py:66 apps/note/models/notes.py:88 | ||||
| #: apps/note/models/notes.py:67 apps/note/models/notes.py:90 | ||||
| msgid "This alias is already taken." | ||||
| msgstr "Cet alias est déjà pris." | ||||
|  | ||||
| #: apps/note/models/notes.py:103 | ||||
| #: apps/note/models/notes.py:105 | ||||
| msgid "user" | ||||
| msgstr "utilisateur" | ||||
|  | ||||
| #: apps/note/models/notes.py:107 | ||||
| #: apps/note/models/notes.py:109 | ||||
| msgid "one's note" | ||||
| msgstr "note d'un utilisateur" | ||||
|  | ||||
| #: apps/note/models/notes.py:108 | ||||
| #: apps/note/models/notes.py:110 | ||||
| msgid "users note" | ||||
| msgstr "notes des utilisateurs" | ||||
|  | ||||
| #: apps/note/models/notes.py:114 | ||||
| #: apps/note/models/notes.py:116 | ||||
| #, python-format | ||||
| msgid "%(user)s's note" | ||||
| msgstr "Note de %(user)s" | ||||
|  | ||||
| #: apps/note/models/notes.py:129 | ||||
| #: apps/note/models/notes.py:131 | ||||
| msgid "club note" | ||||
| msgstr "note d'un club" | ||||
|  | ||||
| #: apps/note/models/notes.py:130 | ||||
| #: apps/note/models/notes.py:132 | ||||
| msgid "clubs notes" | ||||
| msgstr "notes des clubs" | ||||
|  | ||||
| #: apps/note/models/notes.py:136 | ||||
| #: apps/note/models/notes.py:138 | ||||
| #, python-format | ||||
| msgid "Note for %(club)s club" | ||||
| msgid "Note of %(club)s club" | ||||
| msgstr "Note du club %(club)s" | ||||
|  | ||||
| #: apps/note/models/notes.py:155 | ||||
| #: apps/note/models/notes.py:158 | ||||
| msgid "special note" | ||||
| msgstr "note spéciale" | ||||
|  | ||||
| #: apps/note/models/notes.py:156 | ||||
| #: apps/note/models/notes.py:159 | ||||
| msgid "special notes" | ||||
| msgstr "notes spéciales" | ||||
|  | ||||
| #: apps/note/models/notes.py:173 | ||||
| #: apps/note/models/notes.py:182 | ||||
| msgid "Invalid alias" | ||||
| msgstr "Alias invalide" | ||||
|  | ||||
| #: apps/note/models/notes.py:189 | ||||
| #: apps/note/models/notes.py:198 | ||||
| msgid "alias" | ||||
| msgstr "alias" | ||||
|  | ||||
| #: apps/note/models/notes.py:190 | ||||
| #: apps/note/models/notes.py:199 templates/member/profile_detail.html:33 | ||||
| msgid "aliases" | ||||
| msgstr "alias" | ||||
|  | ||||
| #: apps/note/models/notes.py:218 | ||||
| #: apps/note/models/notes.py:225 | ||||
| msgid "Alias too long." | ||||
| msgstr "L'alias est trop long." | ||||
|  | ||||
| #: apps/note/models/notes.py:221 | ||||
| msgid "An alias with a similar name already exists." | ||||
| msgstr "Un alias avec un nom similaire existe déjà." | ||||
| #: apps/note/models/notes.py:236 | ||||
| msgid "You can't delete your main alias." | ||||
| msgstr "Vous ne pouvez pas supprimer votre alias principal." | ||||
|  | ||||
| #: apps/note/models/transactions.py:30 apps/note/models/transactions.py:68 | ||||
| #: apps/note/models/transactions.py:29 | ||||
| msgid "transaction category" | ||||
| msgstr "catégorie de transaction" | ||||
|  | ||||
| #: apps/note/models/transactions.py:30 | ||||
| msgid "transaction categories" | ||||
| msgstr "catégories de transaction" | ||||
|  | ||||
| #: apps/note/models/transactions.py:54 apps/note/models/transactions.py:102 | ||||
| msgid "amount" | ||||
| msgstr "montant" | ||||
|  | ||||
| #: apps/note/models/transactions.py:31 | ||||
| #: apps/note/models/transactions.py:55 | ||||
| msgid "in centimes" | ||||
| msgstr "en centimes" | ||||
|  | ||||
| #: apps/note/models/transactions.py:39 | ||||
| #: apps/note/models/transactions.py:65 | ||||
| msgid "transaction template" | ||||
| msgstr "modèle de transaction" | ||||
|  | ||||
| #: apps/note/models/transactions.py:40 | ||||
| #: apps/note/models/transactions.py:66 | ||||
| msgid "transaction templates" | ||||
| msgstr "modèles de transaction" | ||||
|  | ||||
| #: apps/note/models/transactions.py:64 | ||||
| #: apps/note/models/transactions.py:99 | ||||
| msgid "quantity" | ||||
| msgstr "quantité" | ||||
|  | ||||
| #: apps/note/models/transactions.py:75 | ||||
| #: apps/note/models/transactions.py:108 | ||||
| msgid "reason" | ||||
| msgstr "raison" | ||||
|  | ||||
| #: apps/note/models/transactions.py:79 | ||||
| #: apps/note/models/transactions.py:112 | ||||
| msgid "valid" | ||||
| msgstr "valide" | ||||
|  | ||||
| #: apps/note/models/transactions.py:84 | ||||
| #: apps/note/models/transactions.py:117 | ||||
| msgid "transaction" | ||||
| msgstr "transaction" | ||||
|  | ||||
| #: apps/note/models/transactions.py:85 | ||||
| #: apps/note/models/transactions.py:118 | ||||
| msgid "transactions" | ||||
| msgstr "transactions" | ||||
|  | ||||
| #: apps/note/models/transactions.py:118 | ||||
| #: apps/note/models/transactions.py:160 | ||||
| msgid "membership transaction" | ||||
| msgstr "transaction d'adhésion" | ||||
|  | ||||
| #: apps/note/models/transactions.py:119 | ||||
| #: apps/note/models/transactions.py:161 | ||||
| msgid "membership transactions" | ||||
| msgstr "transactions d'adhésion" | ||||
|  | ||||
| #: apps/note/views.py:26 | ||||
| #: apps/note/views.py:29 | ||||
| msgid "Transfer money from your account to one or others" | ||||
| msgstr "Transfert d'argent de ton compte vers un ou plusieurs autres" | ||||
|  | ||||
| #: note_kfet/settings.py:140 | ||||
| #: note_kfet/settings/base.py:148 | ||||
| msgid "German" | ||||
| msgstr "" | ||||
|  | ||||
| #: note_kfet/settings/base.py:149 | ||||
| msgid "English" | ||||
| msgstr "" | ||||
|  | ||||
| #: note_kfet/settings.py:141 | ||||
| #: note_kfet/settings/base.py:150 | ||||
| msgid "French" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/base.html:14 | ||||
| #: templates/base.html:13 | ||||
| msgid "The ENS Paris-Saclay BDE note." | ||||
| msgstr "" | ||||
| msgstr "La note du BDE de l'ENS Paris-Saclay." | ||||
|  | ||||
| #: templates/member/profile_detail.html:12 | ||||
| #: templates/member/club_detail.html:10 | ||||
| msgid "Membership starts on" | ||||
| msgstr "L'adhésion commence le" | ||||
|  | ||||
| #: templates/member/club_detail.html:12 | ||||
| msgid "Membership ends on" | ||||
| msgstr "L'adhésion finie le" | ||||
|  | ||||
| #: templates/member/club_detail.html:14 | ||||
| msgid "Membership duration" | ||||
| msgstr "Durée de l'adhésion" | ||||
|  | ||||
| #: templates/member/club_detail.html:18 templates/member/profile_detail.html:30 | ||||
| msgid "balance" | ||||
| msgstr "solde du compte" | ||||
|  | ||||
| #: templates/member/manage_auth_tokens.html:16 | ||||
| msgid "Token" | ||||
| msgstr "Jeton" | ||||
|  | ||||
| #: templates/member/manage_auth_tokens.html:23 | ||||
| msgid "Created" | ||||
| msgstr "Créé le" | ||||
|  | ||||
| #: templates/member/manage_auth_tokens.html:31 | ||||
| msgid "Regenerate token" | ||||
| msgstr "Regénérer le jeton" | ||||
|  | ||||
| #: templates/member/profile_detail.html:11 | ||||
| msgid "first name" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/profile_detail.html:14 | ||||
| #, fuzzy | ||||
| #| msgid "name" | ||||
| msgid "username" | ||||
| msgstr "nom" | ||||
| msgstr "nom d'utilisateur" | ||||
|  | ||||
| #: templates/member/profile_detail.html:22 | ||||
| #: templates/member/profile_detail.html:17 | ||||
| #, fuzzy | ||||
| #| msgid "account balance" | ||||
| msgid "balance" | ||||
| msgstr "solde du compte" | ||||
| #| msgid "Change password" | ||||
| msgid "password" | ||||
| msgstr "Changer le mot de passe" | ||||
|  | ||||
| #: templates/member/profile_detail.html:26 | ||||
| #: templates/member/profile_detail.html:20 | ||||
| msgid "Change password" | ||||
| msgstr "Changer le mot de passe" | ||||
|  | ||||
| #: templates/member/profile_detail.html:38 | ||||
| msgid "Manage auth token" | ||||
| msgstr "Gérer les jetons d'authentification" | ||||
|  | ||||
| #: templates/member/profile_detail.html:54 | ||||
| msgid "View my memberships" | ||||
| msgstr "Voir mes adhésions" | ||||
|  | ||||
| #: templates/member/profile_update.html:13 | ||||
| msgid "Save Changes" | ||||
| msgstr "Sauvegarder les changements" | ||||
|  | ||||
| #: templates/member/signup.html:14 | ||||
| msgid "Sign Up" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/note/transaction_form.html:35 | ||||
|   | ||||
							
								
								
									
										38
									
								
								note_kfet/middlewares.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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 | ||||
|  | ||||
| @@ -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 ! | ||||
|  | ||||
|   | ||||
| @@ -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')), | ||||
| ] | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| ######################## | ||||
| # Development Settings # | ||||
| ######################## | ||||
|   | ||||
| @@ -1,3 +1,6 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| ######################## | ||||
| # Production  Settings # | ||||
| ######################## | ||||
|   | ||||
| @@ -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')), | ||||
| ] | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| # -*- mode: python; coding: utf-8 -*- | ||||
| # Copyright (C) 2016-2019 by BDE | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 4.3 KiB | 
| Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 12 KiB | 
| Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 2.7 KiB | 
| @@ -3,7 +3,7 @@ | ||||
|     <msapplication> | ||||
|         <tile> | ||||
|             <square150x150logo src="/static/favicon/mstile-150x150.png"/> | ||||
|             <TileColor>#da532c</TileColor> | ||||
|             <TileColor>#00a300</TileColor> | ||||
|         </tile> | ||||
|     </msapplication> | ||||
| </browserconfig> | ||||
|   | ||||
| Before Width: | Height: | Size: 690 B After Width: | Height: | Size: 573 B | 
| Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 905 B | 
| Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB | 
| Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 2.2 KiB | 
| Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 9.5 KiB | 
| @@ -6,8 +6,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
| <html lang="en" class="position-relative h-100"> | ||||
| <head> | ||||
|     <meta charset="utf-8"> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | ||||
|     <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | ||||
|     <title> | ||||
|         {% block title %}{{ title }}{% endblock title %} - {{ request.site.name }} | ||||
|     </title> | ||||
| @@ -23,19 +22,41 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|     <meta name="msapplication-TileColor" content="#da532c"> | ||||
|     <meta name="msapplication-config" content="{% static "favicon/browserconfig.xml" %}"> | ||||
|     <meta name="theme-color" content="#ffffff"> | ||||
|     {% if no_cache %} | ||||
|         <meta name="turbolinks-cache-control" content="no-cache"> | ||||
|     {% endif %} | ||||
|  | ||||
|     {# Bootstrap CSS #} | ||||
|     <link rel="stylesheet" | ||||
|           href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" | ||||
|           integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" | ||||
|           href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" | ||||
|           integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" | ||||
|           crossorigin="anonymous"> | ||||
|     <link rel="stylesheet" | ||||
|           href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css"> | ||||
|  | ||||
|     {# JQuery, Bootstrap and Turbolinks JavaScript #} | ||||
|     <script src="https://code.jquery.com/jquery-3.4.1.min.js" | ||||
|             integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh" | ||||
|             crossorigin="anonymous"></script> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" | ||||
|             integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" | ||||
|             crossorigin="anonymous"></script> | ||||
|     <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" | ||||
|             integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" | ||||
|             crossorigin="anonymous"></script> | ||||
|     <script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js" | ||||
|             crossorigin="anonymous"></script> | ||||
|  | ||||
|     {# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #} | ||||
|     {% if form.media %} | ||||
|         {{ form.media }} | ||||
|     {% endif %} | ||||
|  | ||||
|     {% block extracss %}{% endblock %} | ||||
| </head> | ||||
| <body> | ||||
| <main> | ||||
|     <nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar"> | ||||
| <body class="d-flex w-100 h-100 flex-column"> | ||||
| <main class="mb-auto"> | ||||
|     <nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm"> | ||||
|         <a class="navbar-brand" href="/">{{ request.site.name }}</a> | ||||
|         <button class="navbar-toggler" type="button" data-toggle="collapse" | ||||
|                 data-target="#navbarNavAltMarkup" | ||||
| @@ -46,7 +67,7 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|         <div class="collapse navbar-collapse" id="navbarNavDropdown"> | ||||
|             <ul class="navbar-nav"> | ||||
|                 <li class="nav-item active"> | ||||
|                     <a class="nav-link" href="#"><i class="fa fa-coffee"></i> Consos</a> | ||||
|                     <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> Consos</a> | ||||
|                 </li> | ||||
|                 <li class="nav-item active"> | ||||
|                     <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> Clubs</a> | ||||
| @@ -89,31 +110,23 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|             </ul> | ||||
|         </div> | ||||
|     </nav> | ||||
|     <div class="container-fluid mb-5 mt-2"> | ||||
|         <div class="row"> | ||||
|             <div class="col-md-1"> | ||||
|                 {% block sidebar %} | ||||
|                 {% endblock %} | ||||
|             </div> | ||||
|             <div class="col-md-10 text-justify"> | ||||
|                 {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %} | ||||
|                 {% block content %} | ||||
|                     <p>Default content...</p> | ||||
|                 {% endblock content %} | ||||
|             </div> | ||||
|         </div> | ||||
|     <div class="container-fluid my-3" style="max-width: 1600px;"> | ||||
|         {% block contenttitle %}<h1>{{ title }}</h1>{% endblock %} | ||||
|         {% block content %} | ||||
|             <p>Default content...</p> | ||||
|         {% endblock content %} | ||||
|     </div> | ||||
| </main> | ||||
| <footer class="bg-light fixed-bottom  py-2"> | ||||
| <footer class="bg-light mt-auto py-2"> | ||||
|     <div class="container-fluid"> | ||||
|         <div class="row"> | ||||
|             <div class="col-sm"> | ||||
|                 <form action="{% url 'set_language' %}" method="post" | ||||
|                       class="form-inline"> | ||||
|                     <span class="text-muted mr-1"> | ||||
|                         NoteKfet2020 - | ||||
|                         NoteKfet2020 — | ||||
|                         <a href="mailto:tresorie.bde@lists.crans.org" | ||||
|                            class="text-muted">Nous contacter</a> - | ||||
|                            class="text-muted">Nous contacter</a> — | ||||
|                     </span> | ||||
|                     {% csrf_token %} | ||||
|                     <select title="language" name="language" | ||||
| @@ -142,16 +155,6 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|     </div> | ||||
| </footer> | ||||
|  | ||||
| {# Bootstrap JavaScript #} | ||||
| <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" | ||||
|         integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" | ||||
|         crossorigin="anonymous"></script> | ||||
| <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" | ||||
|         integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" | ||||
|         crossorigin="anonymous"></script> | ||||
| <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" | ||||
|         integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" | ||||
|         crossorigin="anonymous"></script> | ||||
| {% block extrajavascript %} | ||||
| {% endblock extrajavascript %} | ||||
| </body> | ||||
|   | ||||
							
								
								
									
										33
									
								
								templates/member/manage_auth_tokens.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,33 @@ | ||||
| {% extends "base.html" %} | ||||
| {% load i18n static pretty_money django_tables2 %} | ||||
|  | ||||
| {% block content %} | ||||
|     <div class="alert alert-info"> | ||||
|     <h4>À quoi sert un jeton d'authentification ?</h4> | ||||
|  | ||||
|     Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a>.<br /> | ||||
|     Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token <TOKEN></code> | ||||
|     pour pouvoir vous identifier.<br /><br /> | ||||
|  | ||||
|     Une documentation de l'API arrivera ultérieurement. | ||||
|     </div> | ||||
|  | ||||
|     <div class="alert alert-info"> | ||||
|         <strong>{%trans  'Token' %} :</strong> | ||||
|         {% if 'show' in request.GET %} | ||||
|             {{ token.key }} (<a href="?">cacher</a>) | ||||
|         {% else %} | ||||
|             <em>caché</em> (<a href="?show">montrer</a>) | ||||
|         {% endif %} | ||||
|         <br /> | ||||
|         <strong>{%trans  'Created' %} :</strong> {{ token.created }} | ||||
|     </div> | ||||
|  | ||||
|     <div class="alert alert-warning"> | ||||
|         <strong>Attention :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton ! | ||||
|     </div> | ||||
|  | ||||
|     <a href="?regenerate"> | ||||
|         <button class="btn btn-primary">{% trans 'Regenerate token' %}</button> | ||||
|     </a> | ||||
| {% endblock %} | ||||
| @@ -2,64 +2,76 @@ | ||||
| {% load i18n static pretty_money django_tables2 %} | ||||
|  | ||||
| {% block content %} | ||||
|     <h3>Compte n° {{ object.pk }}</h3> | ||||
| <div class="row mt-4"> | ||||
|     <div class="col-md-3 mb-4"> | ||||
|         <div class="card bg-light shadow"> | ||||
|             <img src="{{ object.note.display_image.url }}" class="card-img-top" alt=""> | ||||
|             <div class="card-body"> | ||||
|                 <dl class="row"> | ||||
|                     <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt> | ||||
|                     <dd class="col-xl-6">{{ object.user.last_name }} {{ object.user.first_name }}</dd> | ||||
|  | ||||
|     <img src="{{ object.note.display_image.url }}" alt=""/> | ||||
|                     <dt class="col-xl-6">{% trans 'username'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6">{{ object.user.username }}</dd> | ||||
|  | ||||
|     <dl class="row"> | ||||
|         <dt class="col-6 col-md-3">{% trans 'name'|capfirst %}</dt> | ||||
|         <dd class="col-6 col-md-3">{{ object.user.name }}</dd> | ||||
|         <dt class="col-6 col-md-3">{% trans 'first name'|capfirst %}</dt> | ||||
|         <dd class="col-6 col-md-3">{{ object.user.first_name }}</dd> | ||||
|         <dt class="col-6 col-md-3">{% trans 'username'|capfirst %}</dt> | ||||
|         <dd class="col-6 col-md-3">{{ object.user.username }}</dd> | ||||
|         <dt class="col-6 col-md-3">Aliases</dt> | ||||
|         <dd class="col-6 col-md-3">{{ object.user.note.aliases_set.all }}</dd> | ||||
|         <dt class="col-6 col-md-3">{% trans 'section'|capfirst %}</dt> | ||||
|         <dd class="col-6 col-md-3">{{ object.section }}</dd> | ||||
|         <dt class="col-6 col-md-3">{% trans 'address'|capfirst %}</dt> | ||||
|         <dd class="col-6 col-md-3">{{ object.address }}</dd> | ||||
|         <dt class="col-6 col-md-3">{% trans 'balance'|capfirst %}</dt> | ||||
|         <dd class="col-6 col-md-3">{{ object.user.note.balance | pretty_money }}</dd> | ||||
|     </dl> | ||||
|     <center> | ||||
|         <a class="btn btn-primary" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a> | ||||
|         <a class="btn btn-primary" href="{% url 'password_change' %}">{% trans 'Change password' %}</a> | ||||
|     </center> | ||||
|                     <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6"> | ||||
|                         <a class="small" href="{% url 'password_change' %}"> | ||||
|                             {% trans 'Change password' %} | ||||
|                         </a> | ||||
|                     </dd> | ||||
|  | ||||
| <div class="accordion" id="accordionProfile"> | ||||
|   <div class="card"> | ||||
|     <div class="card-header" id="headingOne"> | ||||
|       <h5 class="mb-0"> | ||||
|         <button class="btn btn-link" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne"> | ||||
|             <i class="fa fa-users"></i> {% trans "View my memberships" %} | ||||
|         </button> | ||||
|       </h5> | ||||
|                     <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6">{{ object.section }}</dd> | ||||
|  | ||||
|                     <dt class="col-xl-6">{% trans 'address'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6">{{ object.address }}</dd> | ||||
|  | ||||
|                     <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6">{{ object.user.note.balance | pretty_money }}</dd> | ||||
|  | ||||
|                     <dt class="col-xl-6">{% trans 'aliases'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6">{{ object.user.note.alias_set.all|join:", " }}</dd> | ||||
|                 </dl> | ||||
|  | ||||
|                 {% if object.user.pk == user.pk %} | ||||
|                     <a class="small" href="{% url 'member:auth_token' %}">{% trans 'Manage auth token' %}</a> | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|             <div class="card-footer"> | ||||
|                 <a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordionProfile"> | ||||
|       <div class="card-body"> | ||||
|         {% render_table club_list %} | ||||
|       </div> | ||||
|     <div class="col-md-9"> | ||||
|         <div class="accordion shadow" id="accordionProfile"> | ||||
|             <div class="card"> | ||||
|                 <div class="card-header position-relative" id="clubListHeading"> | ||||
|                     <a class="btn btn-link stretched-link font-weight-bold" | ||||
|                        data-toggle="collapse" data-target="#clubListCollapse" | ||||
|                        aria-expanded="true" aria-controls="clubListCollapse"> | ||||
|                         <i class="fa fa-users"></i> {% trans "View my memberships" %} | ||||
|                     </a> | ||||
|                 </div> | ||||
|                 <div id="clubListCollapse" class="collapse show" style="overflow:auto hidden" aria-labelledby="clubListHeading" data-parent="#accordionProfile"> | ||||
|                     {% render_table club_list %} | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div class="card"> | ||||
|                 <div class="card-header position-relative" id="historyListHeading"> | ||||
|                     <a class="btn btn-link stretched-link collapsed font-weight-bold" | ||||
|                        data-toggle="collapse" data-target="#historyListCollapse" | ||||
|                        aria-expanded="false" aria-controls="historyListCollapse"> | ||||
|                         <i class="fa fa-euro"></i> Historique des transactions | ||||
|                     </a> | ||||
|                 </div> | ||||
|                 <div id="historyListCollapse" class="collapse" style="overflow:auto hidden" aria-labelledby="historyListHeading" data-parent="#accordionProfile"> | ||||
|                     {% render_table history_list %} | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="card"> | ||||
|     <div class="card-header" id="headingTwo"> | ||||
|       <h5 class="mb-0"> | ||||
|         <button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"> | ||||
|             <i class="fa fa-euro"></i> Historique des transactions | ||||
|         </button> | ||||
|       </h5> | ||||
|     </div> | ||||
|     <div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionProfile"> | ||||
|       <div class="card-body"> | ||||
|           {% render_table history_list %} | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|     {% endblock %} | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| <!doctype html> | ||||
| {% extends "base.html" %} | ||||
| {% load crispy_forms_tags %} | ||||
| {% load i18n static pretty_money django_tables2 %} | ||||
| {% load i18n crispy_forms_tags %} | ||||
| {% comment %} | ||||
| SPDX-License-Identifier: GPL-3.0-or-later | ||||
| {% endcomment %} | ||||
|  | ||||
| {% block content %} | ||||
|  | ||||
| <form method="post"> | ||||
|       {% csrf_token %} | ||||
|       {{ form|crispy }} | ||||
|       {{ profile_form|crispy }} | ||||
|       <button class="btn btn-link" type="submit"> | ||||
|           {% trans "Save Changes" %} | ||||
|       </button> | ||||
|   </form> | ||||
|  | ||||
|     {% csrf_token %} | ||||
|     {{ form|crispy }} | ||||
|     {{ profile_form|crispy }} | ||||
|     <button class="btn btn-primary" type="submit"> | ||||
|         {% trans "Save Changes" %} | ||||
|     </button> | ||||
| </form> | ||||
| {% endblock %} | ||||
|   | ||||
							
								
								
									
										97
									
								
								templates/note/conso_form.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,97 @@ | ||||
| {% extends "base.html" %} | ||||
|  | ||||
| {% load i18n static pretty_money %} | ||||
|  | ||||
| {# Remove page title #} | ||||
| {% block contenttitle %}{% endblock %} | ||||
|  | ||||
| {% block content %} | ||||
|     {# Regroup buttons under categories #} | ||||
|     {% regroup transaction_templates by template_type as template_types %} | ||||
|  | ||||
|     <form method="post" onsubmit="window.onbeforeunload=null"> | ||||
|         {% csrf_token %} | ||||
|  | ||||
|         <div class="row"> | ||||
|             <div class="col-sm-5 mb-4"> | ||||
|                 {% if form.non_field_errors %} | ||||
|                     <p class="errornote"> | ||||
|                         {% for error in form.non_field_errors %} | ||||
|                             {{ error }} | ||||
|                         {% endfor %} | ||||
|                     </p> | ||||
|                 {% endif %} | ||||
|                 {% for field in form %} | ||||
|                     <div class="form-row{% if field.errors %} errors{% endif %}"> | ||||
|                         {{ field.errors }} | ||||
|                         <div> | ||||
|                             {{ field.label_tag }} | ||||
|                             {% if field.is_readonly %} | ||||
|                                 <div class="readonly">{{ field.contents }}</div> | ||||
|                             {% else %} | ||||
|                                 {{ field }} | ||||
|                             {% endif %} | ||||
|                             {% if field.field.help_text %} | ||||
|                                 <div class="help">{{ field.field.help_text|safe }}</div> | ||||
|                             {% endif %} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 {% endfor %} | ||||
|             </div> | ||||
|  | ||||
|             <div class="col-sm-7"> | ||||
|                 <div class="card text-center shadow"> | ||||
|                     {# Tabs for button categories #} | ||||
|                     <div class="card-header"> | ||||
|                         <ul class="nav nav-tabs nav-fill card-header-tabs"> | ||||
|                             {% for template_type in template_types %} | ||||
|                                 <li class="nav-item"> | ||||
|                                     <a class="nav-link" data-toggle="tab" href="#{{ template_type.grouper|slugify }}"> | ||||
|                                         {{ template_type.grouper }} | ||||
|                                     </a> | ||||
|                                 </li> | ||||
|                             {% endfor %} | ||||
|                         </ul> | ||||
|                     </div> | ||||
|  | ||||
|                     {# Tabs content #} | ||||
|                     <div class="card-body"> | ||||
|                         <div class="tab-content"> | ||||
|                             {% for template_type in template_types %} | ||||
|                                 <div class="tab-pane" id="{{ template_type.grouper|slugify }}"> | ||||
|                                     <div class="d-inline-flex flex-wrap justify-content-center"> | ||||
|                                         {% for button in template_type.list %} | ||||
|                                             <button class="btn btn-outline-dark rounded-0 flex-fill" | ||||
|                                                     name="button" value="{{ button.name }}"> | ||||
|                                                 {{ button.name }} ({{ button.amount | pretty_money }}) | ||||
|                                             </button> | ||||
|                                         {% endfor %} | ||||
|                                     </div> | ||||
|                                 </div> | ||||
|                             {% endfor %} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </form> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block extrajavascript %} | ||||
|     <script type="text/javascript"> | ||||
|         $(document).ready(function() { | ||||
|             // If hash of a category in the URL, then select this category | ||||
|             // else select the first one | ||||
|             if (location.hash) { | ||||
|                 $("a[href='" + location.hash + "']").tab("show"); | ||||
|             } else { | ||||
|                 $("a[data-toggle='tab']").first().tab("show"); | ||||
|             } | ||||
|  | ||||
|             // When selecting a category, change URL | ||||
|             $(document.body).on("click", "a[data-toggle='tab']", function(event) { | ||||
|                 location.hash = this.getAttribute("href"); | ||||
|             }); | ||||
|         }); | ||||
|     </script> | ||||
| {% endblock %} | ||||
							
								
								
									
										22
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						| @@ -1,9 +1,13 @@ | ||||
| [tox] | ||||
| envlist = py36,py37,linters | ||||
| envlist = | ||||
|     py36-django22 | ||||
|     py37-django22 | ||||
|     linters | ||||
| skipsdist = True | ||||
|  | ||||
| [testenv] | ||||
| basepython = python3 | ||||
| setenv = | ||||
| 	PYTHONWARNINGS = all | ||||
| deps = | ||||
|     -r{toxinidir}/requirements.txt | ||||
|     coverage | ||||
| @@ -12,11 +16,6 @@ commands = | ||||
|     coverage run ./manage.py test {posargs} | ||||
|     coverage report -m | ||||
|  | ||||
| [testenv:pre-commit] | ||||
| deps = pre-commit | ||||
| commands = | ||||
|     pre-commit run --all-files --show-diff-on-failure | ||||
|  | ||||
| [testenv:linters] | ||||
| deps = | ||||
|     -r{toxinidir}/requirements.txt | ||||
| @@ -26,13 +25,12 @@ deps = | ||||
|     flake8-typing-imports | ||||
|     pep8-naming | ||||
|     pyflakes | ||||
|     pylint | ||||
| commands = | ||||
|     flake8 app/activity app/member app/note | ||||
|     pylint . | ||||
|     flake8 apps/activity apps/api apps/member apps/note | ||||
|  | ||||
| [flake8] | ||||
| ignore = D203, W503, E203 | ||||
| # Ignore too many errors, should be reduced in the future | ||||
| ignore = D203, W503, E203, I100, I101 | ||||
| exclude = | ||||
|     .tox, | ||||
|     .git, | ||||
| @@ -45,7 +43,7 @@ exclude = | ||||
|     .eggs, | ||||
|     *migrations* | ||||
| max-complexity = 10 | ||||
| max-line-length = 160 | ||||
| import-order-style = google | ||||
| application-import-names = flake8 | ||||
| format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s | ||||
|  | ||||
|   | ||||