diff --git a/.gitignore b/.gitignore index f1650504b7282b0b04bb93ee8e905cc353d7f65d..b57ed74ab225239f4c21de167f26f6f1ed7f2aff 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ coverage # PyCharm project settings .idea +# VSCode project settings +.vscode + # Local data secrets.py *.log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bceb5f51c36ffc38495a45859c5e3083f55144af..291ed49016fc76224378fe52dbea4bb5de0beca1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,20 +2,25 @@ image: python:3.6 stages: - test + - quality-assurance before_script: - pip install tox -python36: +py36-django22: image: python:3.6 stage: test - script: tox -e py36 + script: tox -e py36-django22 -python37: +py37-django22: image: python:3.7 stage: test - script: tox -e py37 + script: tox -e py37-django22 linters: - stage: test + image: python:3.6 + stage: quality-assurance script: tox -e linters + + # Be nice to new contributors, but please use `tox` + allow_failure: true diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 6ddf1f3ccfdf1d0f28617c5eb811e610c548d900..0000000000000000000000000000000000000000 --- a/.pylintrc +++ /dev/null @@ -1,379 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS,.git - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=4 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence=INFERENCE_FAILURE - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin,map-builtin-not-iterating,delslice-method,apply-builtin,cmp-method,setslice-method,coerce-method,long-suffix,raising-string,import-star-module-level,buffer-builtin,reload-builtin,unpacking-in-except,print-statement,hex-method,old-octal-literal,metaclass-assignment,dict-iter-method,range-builtin-not-iterating,using-cmp-argument,indexing-exception,no-absolute-import,coerce-builtin,getslice-method,suppressed-message,execfile-builtin,round-builtin,useless-suppression,reduce-builtin,old-raise-syntax,zip-builtin-not-iterating,cmp-builtin,xrange-builtin,standarderror-builtin,old-division,oct-method,next-method-called,old-ne-operator,basestring-builtin - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[BASIC] - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=yes - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )?<?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 - diff --git a/apps/activity/__init__.py b/apps/activity/__init__.py index 75df9e1f408b34c35e9138d12fc2c6b3ac479724..195d53023dbb2e82edfce87166c8373532cd3e77 100644 --- a/apps/activity/__init__.py +++ b/apps/activity/__init__.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later default_app_config = 'activity.apps.ActivityConfig' diff --git a/apps/activity/admin.py b/apps/activity/admin.py index 1efe272c50beac2b537936063b489d656aac1d16..5ceb4e8146bcf1c1392059117c34e2497d04c73a 100644 --- a/apps/activity/admin.py +++ b/apps/activity/admin.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin @@ -12,7 +11,7 @@ class ActivityAdmin(admin.ModelAdmin): Admin customisation for Activity """ list_display = ('name', 'activity_type', 'organizer') - list_filter = ('activity_type',) + list_filter = ('activity_type', ) search_fields = ['name', 'organizer__name'] # Organize activities by start date diff --git a/apps/activity/api/__init__.py b/apps/activity/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/activity/api/serializers.py b/apps/activity/api/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..0b9302f17aa24b961be3f792a7afb7a87c6b5e8a --- /dev/null +++ b/apps/activity/api/serializers.py @@ -0,0 +1,36 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import serializers + +from ..models import ActivityType, Activity, Guest + + +class ActivityTypeSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Activity types. + The djangorestframework plugin will analyse the model `ActivityType` and parse all fields in the API. + """ + class Meta: + model = ActivityType + fields = '__all__' + + +class ActivitySerializer(serializers.ModelSerializer): + """ + REST API Serializer for Activities. + The djangorestframework plugin will analyse the model `Activity` and parse all fields in the API. + """ + class Meta: + model = Activity + fields = '__all__' + + +class GuestSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Guests. + The djangorestframework plugin will analyse the model `Guest` and parse all fields in the API. + """ + class Meta: + model = Guest + fields = '__all__' diff --git a/apps/activity/api/urls.py b/apps/activity/api/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..79e0ba30db826b856170a54e5561674fc06c7037 --- /dev/null +++ b/apps/activity/api/urls.py @@ -0,0 +1,13 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet + + +def register_activity_urls(router, path): + """ + Configure router for Activity REST API. + """ + router.register(path + '/activity', ActivityViewSet) + router.register(path + '/type', ActivityTypeViewSet) + router.register(path + '/guest', GuestViewSet) diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py new file mode 100644 index 0000000000000000000000000000000000000000..5683d458011f3acba0e06bda1b0e6dc575ea513b --- /dev/null +++ b/apps/activity/api/views.py @@ -0,0 +1,37 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import viewsets + +from ..models import ActivityType, Activity, Guest +from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer + + +class ActivityTypeViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer, + then render it on /api/activity/type/ + """ + queryset = ActivityType.objects.all() + serializer_class = ActivityTypeSerializer + + +class ActivityViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer, + then render it on /api/activity/activity/ + """ + queryset = Activity.objects.all() + serializer_class = ActivitySerializer + + +class GuestViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer, + then render it on /api/activity/guest/ + """ + queryset = Guest.objects.all() + serializer_class = GuestSerializer diff --git a/apps/activity/apps.py b/apps/activity/apps.py index 29990f1b6338a186ded3d9dbbef20ef78fa3586f..bb72947f861f74172ecba66202f39e6eef6e5057 100644 --- a/apps/activity/apps.py +++ b/apps/activity/apps.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.apps import AppConfig diff --git a/apps/activity/models.py b/apps/activity/models.py index 4dbc5522bcc09168eab2d1c34bdeea50be6dc33e..8f23060cddd71543003821bf3d27e0366fcb89c2 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.conf import settings diff --git a/apps/api/urls.py b/apps/api/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..7e59a8c0acfc87ff928d4392fd06f46be41ac769 --- /dev/null +++ b/apps/api/urls.py @@ -0,0 +1,51 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.conf.urls import url, include +from django.contrib.auth.models import User +from rest_framework import routers, serializers, viewsets +from activity.api.urls import register_activity_urls +from member.api.urls import register_members_urls +from note.api.urls import register_note_urls + + +class UserSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Users. + The djangorestframework plugin will analyse the model `User` and parse all fields in the API. + """ + class Meta: + model = User + exclude = ( + 'password', + 'groups', + 'user_permissions', + ) + + +class UserViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, + then render it on /api/users/ + """ + queryset = User.objects.all() + serializer_class = UserSerializer + + +# Routers provide an easy way of automatically determining the URL conf. +# Register each app API router and user viewset +router = routers.DefaultRouter() +router.register('user', UserViewSet) +register_members_urls(router, 'members') +register_activity_urls(router, 'activity') +register_note_urls(router, 'note') + +app_name = 'api' + +# Wire up our API using automatic URL routing. +# Additionally, we include login URLs for the browsable API. +urlpatterns = [ + url('^', include(router.urls)), + url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')), +] diff --git a/apps/member/__init__.py b/apps/member/__init__.py index ec189d6f8ca59293a2695d7749e9ba0c6062700e..298d1dda62f47f954a4df4922cf24ed6f16598e8 100644 --- a/apps/member/__init__.py +++ b/apps/member/__init__.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later default_app_config = 'member.apps.MemberConfig' diff --git a/apps/member/admin.py b/apps/member/admin.py index f45d5f55741ff0f665ef632cfc20bc31999e5014..fb1073778fdae66126cf01bd6d3011d704a7e59a 100644 --- a/apps/member/admin.py +++ b/apps/member/admin.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin @@ -19,9 +18,9 @@ class ProfileInline(admin.StackedInline): class CustomUserAdmin(UserAdmin): - inlines = (ProfileInline,) + inlines = (ProfileInline, ) list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_select_related = ('profile',) + list_select_related = ('profile', ) form = ProfileForm def get_inline_instances(self, request, obj=None): diff --git a/apps/member/api/__init__.py b/apps/member/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/member/api/serializers.py b/apps/member/api/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..f4df67993dcfd3b4728d02d240d0e59aeb853a12 --- /dev/null +++ b/apps/member/api/serializers.py @@ -0,0 +1,46 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import serializers + +from ..models import Profile, Club, Role, Membership + + +class ProfileSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Profiles. + The djangorestframework plugin will analyse the model `Profile` and parse all fields in the API. + """ + class Meta: + model = Profile + fields = '__all__' + + +class ClubSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Clubs. + The djangorestframework plugin will analyse the model `Club` and parse all fields in the API. + """ + class Meta: + model = Club + fields = '__all__' + + +class RoleSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Roles. + The djangorestframework plugin will analyse the model `Role` and parse all fields in the API. + """ + class Meta: + model = Role + fields = '__all__' + + +class MembershipSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Memberships. + The djangorestframework plugin will analyse the model `Memberships` and parse all fields in the API. + """ + class Meta: + model = Membership + fields = '__all__' diff --git a/apps/member/api/urls.py b/apps/member/api/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..15bb83cac1204ddd45aef151887f878ccbd70b5d --- /dev/null +++ b/apps/member/api/urls.py @@ -0,0 +1,14 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet + + +def register_members_urls(router, path): + """ + Configure router for Member REST API. + """ + router.register(path + '/profile', ProfileViewSet) + router.register(path + '/club', ClubViewSet) + router.register(path + '/role', RoleViewSet) + router.register(path + '/membership', MembershipViewSet) diff --git a/apps/member/api/views.py b/apps/member/api/views.py new file mode 100644 index 0000000000000000000000000000000000000000..79ba4c12afca3dee2c00445bd280d33b46934101 --- /dev/null +++ b/apps/member/api/views.py @@ -0,0 +1,47 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import viewsets + +from ..models import Profile, Club, Role, Membership +from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer + + +class ProfileViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer, + then render it on /api/members/profile/ + """ + queryset = Profile.objects.all() + serializer_class = ProfileSerializer + + +class ClubViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer, + then render it on /api/members/club/ + """ + queryset = Club.objects.all() + serializer_class = ClubSerializer + + +class RoleViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer, + then render it on /api/members/role/ + """ + queryset = Role.objects.all() + serializer_class = RoleSerializer + + +class MembershipViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer, + then render it on /api/members/membership/ + """ + queryset = Membership.objects.all() + serializer_class = MembershipSerializer diff --git a/apps/member/apps.py b/apps/member/apps.py index 928c00e4a10c5b00db44ab7428941944b8414c45..2d7f4ab7daf5701bd0c54e4714b87d245a79c5f7 100644 --- a/apps/member/apps.py +++ b/apps/member/apps.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.apps import AppConfig diff --git a/apps/member/filters.py b/apps/member/filters.py index fb1a212871254cb3717b044184f5334d20009b18..418e52fc680fb7888574924eb06623176a03680c 100644 --- a/apps/member/filters.py +++ b/apps/member/filters.py @@ -1,31 +1,33 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django_filters import FilterSet, CharFilter,NumberFilter +from django_filters import FilterSet, CharFilter from django.contrib.auth.models import User from django.db.models import CharField from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit -from .models import Club class UserFilter(FilterSet): class Meta: model = User - fields = ['last_name','first_name','username','profile__section'] - filter_overrides={ - CharField:{ - 'filter_class':CharFilter, - 'extra': lambda f:{ - 'lookup_expr':'icontains' + fields = ['last_name', 'first_name', 'username', 'profile__section'] + filter_overrides = { + CharField: { + 'filter_class': CharFilter, + 'extra': lambda f: { + 'lookup_expr': 'icontains' } } } + class UserFilterFormHelper(FormHelper): form_method = 'GET' layout = Layout( - 'last_name','first_name','username','profile__section', - Submit('Submit','Apply Filter'), + 'last_name', + 'first_name', + 'username', + 'profile__section', + Submit('Submit', 'Apply Filter'), ) diff --git a/apps/member/forms.py b/apps/member/forms.py index 59d3fec2c78a4a18d54a56ead1481fed077d59fe..66844cf4f37b3c0ebedc9625ea9beb9158dd714c 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -1,26 +1,23 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django.contrib.auth.forms import UserChangeForm, UserCreationForm +from dal import autocomplete +from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User from django import forms from .models import Profile, Club, Membership -from django.utils.translation import gettext_lazy as _ - from crispy_forms.helper import FormHelper -from crispy_forms import layout, bootstrap -from crispy_forms.bootstrap import InlineField, FormActions, StrictButton, Div, Field +from crispy_forms.bootstrap import Div from crispy_forms.layout import Layout - class SignUpForm(UserCreationForm): class Meta: model = User - fields = ['first_name','last_name','username','email'] + fields = ['first_name', 'last_name', 'username', 'email'] + class ProfileForm(forms.ModelForm): """ @@ -31,37 +28,56 @@ class ProfileForm(forms.ModelForm): fields = '__all__' exclude = ['user'] + class ClubForm(forms.ModelForm): class Meta: model = Club - fields ='__all__' + fields = '__all__' + class AddMembersForm(forms.Form): class Meta: - fields = ('',) + fields = ('', ) + class MembershipForm(forms.ModelForm): class Meta: model = Membership - fields = ('user','roles','date_start') + fields = ('user', 'roles', 'date_start') + # Le champ d'utilisateur est remplacé par un champ d'auto-complétion. + # Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion + # et récupère les noms d'utilisateur valides + widgets = { + 'user': + autocomplete.ModelSelect2( + url='member:user_autocomplete', + attrs={ + 'data-placeholder': 'Nom ...', + 'data-minimum-input-length': 1, + }, + ), + } + + +MemberFormSet = forms.modelformset_factory( + Membership, + form=MembershipForm, + extra=2, + can_delete=True, +) -MemberFormSet = forms.modelformset_factory(Membership, - form=MembershipForm, - extra=2, - can_delete=True) class FormSetHelper(FormHelper): - def __init__(self,*args,**kwargs): - super().__init__(*args,**kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.form_tag = False self.form_method = 'POST' - self.form_class='form-inline' + self.form_class = 'form-inline' # self.template = 'bootstrap/table_inline_formset.html' self.layout = Layout( Div( - Div('user',css_class='col-sm-2'), - Div('roles',css_class='col-sm-2'), - Div('date_start',css_class='col-sm-2'), + Div('user', css_class='col-sm-2'), + Div('roles', css_class='col-sm-2'), + Div('date_start', css_class='col-sm-2'), css_class="row formset-row", - ) - ) + )) diff --git a/apps/member/models.py b/apps/member/models.py index 883f9b4956ab78b31a3d8cb4b628efe465b9d8b6..cd754bd8d90eb648f1366a0de9505021fa35030d 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -1,14 +1,12 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.conf import settings from django.db import models -from django.db.models.signals import post_save -from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django.urls import reverse, reverse_lazy + class Profile(models.Model): """ An user profile @@ -50,7 +48,8 @@ class Profile(models.Model): verbose_name_plural = _('user profile') def get_absolute_url(self): - return reverse('user_detail',args=(self.pk,)) + return reverse('user_detail', args=(self.pk, )) + class Club(models.Model): """ @@ -98,7 +97,7 @@ class Club(models.Model): return self.name def get_absolute_url(self): - return reverse_lazy('member:club_detail', args=(self.pk,)) + return reverse_lazy('member:club_detail', args=(self.pk, )) class Role(models.Model): @@ -118,6 +117,9 @@ class Role(models.Model): verbose_name = _('role') verbose_name_plural = _('roles') + def __str__(self): + return str(self.name) + class Membership(models.Model): """ @@ -126,15 +128,15 @@ class Membership(models.Model): """ user = models.ForeignKey( settings.AUTH_USER_MODEL, - on_delete=models.PROTECT + on_delete=models.PROTECT, ) club = models.ForeignKey( Club, - on_delete=models.PROTECT + on_delete=models.PROTECT, ) roles = models.ForeignKey( Role, - on_delete=models.PROTECT + on_delete=models.PROTECT, ) date_start = models.DateField( verbose_name=_('membership starts on'), diff --git a/apps/member/signals.py b/apps/member/signals.py index 6688516b615a89c7ca57f80e6b6706841b546b55..4e945ad504ec4889419b9064e021183f6bc5aaaa 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -1,6 +1,2 @@ -#!/usr/bin/env python - -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - diff --git a/apps/member/tables.py b/apps/member/tables.py index 4218948c11aa5e516a12057e12594cde95bb5abd..a6de17d2f3a0cc3406f928ee8eb4376cd5f13e66 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -1,19 +1,24 @@ -#!/usr/bin/env python +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later import django_tables2 as tables -from .models import Club -from django.conf import settings from django.contrib.auth.models import User +from .models import Club + + class ClubTable(tables.Table): class Meta: - attrs = {'class':'table table-bordered table-condensed table-striped table-hover'} + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } model = Club - template_name = 'django_tables2/bootstrap.html' - fields = ('id','name','email') - row_attrs = {'class':'table-row', - 'data-href': lambda record: record.pk } - + template_name = 'django_tables2/bootstrap4.html' + fields = ('id', 'name', 'email') + row_attrs = { + 'class': 'table-row', + 'data-href': lambda record: record.pk + } class UserTable(tables.Table): @@ -21,7 +26,9 @@ class UserTable(tables.Table): solde = tables.Column(accessor='note.balance') class Meta: - attrs = {'class':'table table-bordered table-condensed table-striped table-hover'} - template_name = 'django_tables2/bootstrap.html' - fields = ('last_name','first_name','username','email') + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + template_name = 'django_tables2/bootstrap4.html' + fields = ('last_name', 'first_name', 'username', 'email') model = User diff --git a/apps/member/urls.py b/apps/member/urls.py index 9bcc10959180f9da8eb5757f8ebbfd5e888ec38a..6a7ed5ce4004879805378e7645ebb78a2e374684 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -1,7 +1,4 @@ -#!/usr/bin/env python - -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.urls import path @@ -10,12 +7,16 @@ from . import views app_name = 'member' urlpatterns = [ - path('signup/',views.UserCreateView.as_view(),name="signup"), - path('club/',views.ClubListView.as_view(),name="club_list"), - path('club/<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"), ] diff --git a/apps/member/views.py b/apps/member/views.py index 90ea5ec351311902db2243a5771809cea1973e6f..6f982c64f6463a954de664c04640ee20dd7fd9db 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -1,29 +1,26 @@ -#!/usr/bin/env python - -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from dal import autocomplete from django.contrib.auth.mixins import LoginRequiredMixin +from django.shortcuts import redirect from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, ListView, DetailView, UpdateView -from django.http import HttpResponseRedirect -from django.contrib.auth.forms import UserCreationForm +from django.views.generic import CreateView, DetailView, UpdateView, TemplateView from django.contrib.auth.models import User from django.urls import reverse_lazy from django.db.models import Q - from django_tables2.views import SingleTableView - +from 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 .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper +from .tables import ClubTable, UserTable from .filters import UserFilter, UserFilterFormHelper -from note.models.transactions import Transaction -from note.tables import HistoryTable - class UserCreateView(CreateView): """ Une vue pour inscrire un utilisateur et lui créer un profile @@ -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" + + 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) -class ClubCreateView(LoginRequiredMixin,CreateView): + 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): + +class ClubDetailView(LoginRequiredMixin, DetailView): model = Club - context_object_name="club" + context_object_name = "club" - def get_context_data(self,**kwargs): + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) club = context["club"] club_transactions = \ @@ -152,23 +239,30 @@ class ClubDetailView(LoginRequiredMixin,DetailView): context['member_list'] = club_member return context -class ClubAddMemberView(LoginRequiredMixin,CreateView): + +class ClubAddMemberView(LoginRequiredMixin, CreateView): model = Membership form_class = MembershipForm template_name = 'member/add_members.html' - def get_context_data(self,**kwargs): + + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['formset'] = MemberFormSet() context['helper'] = FormSetHelper() + + context['no_cache'] = True + return context - def post(self,request,*args,**kwargs): - formset = MembershipFormset(request.POST) - if formset.is_valid(): - return self.form_valid(formset) - else: - return self.form_invalid(formset) + def post(self, request, *args, **kwargs): + return + # TODO: Implement POST + # formset = MembershipFormset(request.POST) + # if formset.is_valid(): + # return self.form_valid(formset) + # else: + # return self.form_invalid(formset) - def form_valid(self,formset): + def form_valid(self, formset): formset.save() return super().form_valid(formset) diff --git a/apps/note/__init__.py b/apps/note/__init__.py index 4773b310daa1e485c9301f33514e27ef13411a4b..f7c331b2dbe800989aa5662518bfaec1e54dbf81 100644 --- a/apps/note/__init__.py +++ b/apps/note/__init__.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later default_app_config = 'note.apps.NoteConfig' diff --git a/apps/note/admin.py b/apps/note/admin.py index 298b91c052294d5a3a1b7a64d4fc6637adefb631..3a9721aeee3eaa7c30750ee4a31109a97fb6f5cb 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin @@ -8,7 +7,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \ PolymorphicChildModelFilter, PolymorphicParentModelAdmin from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser -from .models.transactions import Transaction, TransactionTemplate +from .models.transactions import Transaction, TransactionCategory, TransactionTemplate class AliasInlines(admin.TabularInline): @@ -25,7 +24,10 @@ class NoteAdmin(PolymorphicParentModelAdmin): Parent regrouping all note types as children """ child_models = (NoteClub, NoteSpecial, NoteUser) - list_filter = (PolymorphicChildModelFilter, 'is_active',) + list_filter = ( + PolymorphicChildModelFilter, + 'is_active', + ) # Use a polymorphic list list_display = ('pretty', 'balance', 'is_active') @@ -44,11 +46,12 @@ class NoteClubAdmin(PolymorphicChildModelAdmin): """ Child for a club note, see NoteAdmin """ - inlines = (AliasInlines,) + inlines = (AliasInlines, ) # We can't change club after creation or the balance readonly_fields = ('club', 'balance') - search_fields = ('club',) + search_fields = ('club', ) + def has_add_permission(self, request): """ A club note should not be manually added @@ -67,7 +70,7 @@ class NoteSpecialAdmin(PolymorphicChildModelAdmin): """ Child for a special note, see NoteAdmin """ - readonly_fields = ('balance',) + readonly_fields = ('balance', ) @admin.register(NoteUser) @@ -75,7 +78,7 @@ class NoteUserAdmin(PolymorphicChildModelAdmin): """ Child for an user note, see NoteAdmin """ - inlines = (AliasInlines,) + inlines = (AliasInlines, ) # We can't change user after creation or the balance readonly_fields = ('user', 'balance') @@ -101,7 +104,10 @@ class TransactionAdmin(admin.ModelAdmin): list_display = ('created_at', 'poly_source', 'poly_destination', 'quantity', 'amount', 'transaction_type', 'valid') list_filter = ('transaction_type', 'valid') - autocomplete_fields = ('source', 'destination',) + autocomplete_fields = ( + 'source', + 'destination', + ) def poly_source(self, obj): """ @@ -136,8 +142,8 @@ class TransactionTemplateAdmin(admin.ModelAdmin): Admin customisation for TransactionTemplate """ list_display = ('name', 'poly_destination', 'amount', 'template_type') - list_filter = ('template_type',) - autocomplete_fields = ('destination',) + list_filter = ('template_type', ) + autocomplete_fields = ('destination', ) def poly_destination(self, obj): """ @@ -146,3 +152,12 @@ class TransactionTemplateAdmin(admin.ModelAdmin): return str(obj.destination) poly_destination.short_description = _('destination') + + +@admin.register(TransactionCategory) +class TransactionCategoryAdmin(admin.ModelAdmin): + """ + Admin customisation for TransactionTemplate + """ + list_display = ('name', ) + list_filter = ('name', ) diff --git a/apps/note/api/__init__.py b/apps/note/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..db0e35318ac4ca71b5902a22231ba11d098bd790 --- /dev/null +++ b/apps/note/api/serializers.py @@ -0,0 +1,103 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import serializers +from rest_polymorphic.serializers import PolymorphicSerializer + +from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias +from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction + + +class NoteSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Notes. + The djangorestframework plugin will analyse the model `Note` and parse all fields in the API. + """ + class Meta: + model = Note + fields = '__all__' + extra_kwargs = { + 'url': { + 'view_name': 'project-detail', + 'lookup_field': 'pk' + }, + } + + +class NoteClubSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Club's notes. + The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API. + """ + class Meta: + model = NoteClub + fields = '__all__' + + +class NoteSpecialSerializer(serializers.ModelSerializer): + """ + REST API Serializer for special notes. + The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API. + """ + class Meta: + model = NoteSpecial + fields = '__all__' + + +class NoteUserSerializer(serializers.ModelSerializer): + """ + REST API Serializer for User's notes. + The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API. + """ + class Meta: + model = NoteUser + fields = '__all__' + + +class AliasSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Aliases. + The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API. + """ + class Meta: + model = Alias + fields = '__all__' + + +class NotePolymorphicSerializer(PolymorphicSerializer): + model_serializer_mapping = { + Note: NoteSerializer, + NoteUser: NoteUserSerializer, + NoteClub: NoteClubSerializer, + NoteSpecial: NoteSpecialSerializer + } + + +class TransactionTemplateSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Transaction templates. + The djangorestframework plugin will analyse the model `TransactionTemplate` and parse all fields in the API. + """ + class Meta: + model = TransactionTemplate + fields = '__all__' + + +class TransactionSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Transactions. + The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API. + """ + class Meta: + model = Transaction + fields = '__all__' + + +class MembershipTransactionSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Membership transactions. + The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API. + """ + class Meta: + model = MembershipTransaction + fields = '__all__' diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..54218796211fb21173c9501b38f5cacff85f65dd --- /dev/null +++ b/apps/note/api/urls.py @@ -0,0 +1,17 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .views import NotePolymorphicViewSet, AliasViewSet, \ + TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet + + +def register_note_urls(router, path): + """ + Configure router for Note REST API. + """ + router.register(path + '/note', NotePolymorphicViewSet) + router.register(path + '/alias', AliasViewSet) + + router.register(path + '/transaction/transaction', TransactionViewSet) + router.register(path + '/transaction/template', TransactionTemplateViewSet) + router.register(path + '/transaction/membership', MembershipTransactionViewSet) diff --git a/apps/note/api/views.py b/apps/note/api/views.py new file mode 100644 index 0000000000000000000000000000000000000000..94b4a47a287eec3d481ed3f5dc9d48f8a6cbb535 --- /dev/null +++ b/apps/note/api/views.py @@ -0,0 +1,161 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.db.models import Q +from rest_framework import viewsets + +from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias +from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction +from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ + NoteUserSerializer, AliasSerializer, \ + TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer + + +class NoteViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer, + then render it on /api/note/note/ + """ + queryset = Note.objects.all() + serializer_class = NoteSerializer + + +class NoteClubViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer, + then render it on /api/note/club/ + """ + queryset = NoteClub.objects.all() + serializer_class = NoteClubSerializer + + +class NoteSpecialViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer, + then render it on /api/note/special/ + """ + queryset = NoteSpecial.objects.all() + serializer_class = NoteSpecialSerializer + + +class NoteUserViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer, + then render it on /api/note/user/ + """ + queryset = NoteUser.objects.all() + serializer_class = NoteUserSerializer + + +class NotePolymorphicViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer, + then render it on /api/note/note/ + """ + queryset = Note.objects.all() + serializer_class = NotePolymorphicSerializer + + def get_queryset(self): + """ + Parse query and apply filters. + :return: The filtered set of requested notes + """ + queryset = Note.objects.all() + + alias = self.request.query_params.get("alias", ".*") + queryset = queryset.filter( + Q(alias__name__regex=alias) + | Q(alias__normalized_name__regex=alias.lower())) + + note_type = self.request.query_params.get("type", None) + if note_type: + types = str(note_type).lower() + if "user" in types: + queryset = queryset.filter(polymorphic_ctype__model="noteuser") + elif "club" in types: + queryset = queryset.filter(polymorphic_ctype__model="noteclub") + elif "special" in types: + queryset = queryset.filter( + polymorphic_ctype__model="notespecial") + else: + queryset = queryset.none() + + return queryset + + +class AliasViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer, + then render it on /api/aliases/ + """ + queryset = Alias.objects.all() + serializer_class = AliasSerializer + + def get_queryset(self): + """ + Parse query and apply filters. + :return: The filtered set of requested aliases + """ + + queryset = Alias.objects.all() + + alias = self.request.query_params.get("alias", ".*") + queryset = queryset.filter( + Q(name__regex=alias) | Q(normalized_name__regex=alias.lower())) + + note_id = self.request.query_params.get("note", None) + if note_id: + queryset = queryset.filter(id=note_id) + + note_type = self.request.query_params.get("type", None) + if note_type: + types = str(note_type).lower() + if "user" in types: + queryset = queryset.filter( + note__polymorphic_ctype__model="noteuser") + elif "club" in types: + queryset = queryset.filter( + note__polymorphic_ctype__model="noteclub") + elif "special" in types: + queryset = queryset.filter( + note__polymorphic_ctype__model="notespecial") + else: + queryset = queryset.none() + + return queryset + + +class TransactionTemplateViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer, + then render it on /api/note/transaction/template/ + """ + queryset = TransactionTemplate.objects.all() + serializer_class = TransactionTemplateSerializer + + +class TransactionViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer, + then render it on /api/note/transaction/transaction/ + """ + queryset = Transaction.objects.all() + serializer_class = TransactionSerializer + + +class MembershipTransactionViewSet(viewsets.ModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `MembershipTransaction` objects, serialize it to JSON with the given serializer, + then render it on /api/note/transaction/membership/ + """ + queryset = MembershipTransaction.objects.all() + serializer_class = MembershipTransactionSerializer diff --git a/apps/note/apps.py b/apps/note/apps.py index c53f915a6b21a903990476766706249e64558335..4881e3b91b3676db9b123c6a9ce65830d7b905b6 100644 --- a/apps/note/apps.py +++ b/apps/note/apps.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.apps import AppConfig @@ -20,9 +19,9 @@ class NoteConfig(AppConfig): """ post_save.connect( signals.save_user_note, - sender=settings.AUTH_USER_MODEL + sender=settings.AUTH_USER_MODEL, ) post_save.connect( signals.save_club_note, - sender='member.Club' + sender='member.Club', ) diff --git a/apps/note/forms.py b/apps/note/forms.py index d74fa5b4db8572da1e29fdad542f4796b8b3acce..e4fd344c1672031f4bf57279de23c6d965c32ffc 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -1,9 +1,94 @@ -#!/usr/bin/env python +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later +from dal import autocomplete from django import forms -from .models import TransactionTemplate + +from .models import Transaction, TransactionTemplate + class TransactionTemplateForm(forms.ModelForm): class Meta: model = TransactionTemplate - fields ='__all__' + fields = '__all__' + + # Le champ de destination est remplacé par un champ d'auto-complétion. + # Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion + # et récupère les aliases valides + # Pour force le type d'une note, il faut rajouter le paramètre : + # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special} + widgets = { + 'destination': + autocomplete.ModelSelect2( + url='note:note_autocomplete', + attrs={ + 'data-placeholder': 'Note ...', + 'data-minimum-input-length': 1, + }, + ), + } + + +class TransactionForm(forms.ModelForm): + def save(self, commit=True): + self.instance.transaction_type = 'transfert' + + super().save(commit) + + class Meta: + model = Transaction + fields = ( + 'source', + 'destination', + 'reason', + 'amount', + ) + + # Voir ci-dessus + widgets = { + 'source': + autocomplete.ModelSelect2( + url='note:note_autocomplete', + attrs={ + 'data-placeholder': 'Note ...', + 'data-minimum-input-length': 1, + }, + ), + 'destination': + autocomplete.ModelSelect2( + url='note:note_autocomplete', + attrs={ + 'data-placeholder': 'Note ...', + 'data-minimum-input-length': 1, + }, + ), + } + + +class ConsoForm(forms.ModelForm): + def save(self, commit=True): + button: TransactionTemplate = TransactionTemplate.objects.filter( + name=self.data['button']).get() + self.instance.destination = button.destination + self.instance.amount = button.amount + self.instance.transaction_type = 'bouton' + self.instance.reason = button.name + super().save(commit) + + class Meta: + model = Transaction + fields = ('source', ) + + # Le champ d'utilisateur est remplacé par un champ d'auto-complétion. + # Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion + # et récupère les aliases de note valides + widgets = { + 'source': + autocomplete.ModelSelect2( + url='note:note_autocomplete', + attrs={ + 'data-placeholder': 'Note ...', + 'data-minimum-input-length': 1, + }, + ), + } diff --git a/apps/note/models/__init__.py b/apps/note/models/__init__.py index b00572ce4a83a36d03791895e0a9ca33fc2b6644..7e6cc310e823e28eea3f8d8e40b8e1d98b87ac7c 100644 --- a/apps/note/models/__init__.py +++ b/apps/note/models/__init__.py @@ -1,14 +1,13 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .transactions import MembershipTransaction, Transaction, \ - TransactionTemplate + TransactionCategory, TransactionTemplate __all__ = [ # Notes 'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', # Transactions - 'MembershipTransaction', 'Transaction', 'TransactionTemplate', + 'MembershipTransaction', 'Transaction', 'TransactionCategory', 'TransactionTemplate', ] diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index e3ab7931b8ee88cd2047c5c4d43ec0609bdb226e..3b616f0e754abaee90a917ae0288d0410ce1de77 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later import unicodedata @@ -10,7 +9,6 @@ from django.core.validators import RegexValidator from django.db import models from django.utils.translation import gettext_lazy as _ from polymorphic.models import PolymorphicModel - """ Defines each note types """ @@ -34,8 +32,7 @@ class Note(PolymorphicModel): default=True, help_text=_( 'Designates whether this note should be treated as active. ' - 'Unselect this instead of deleting notes.' - ), + 'Unselect this instead of deleting notes.'), ) display_image = models.ImageField( verbose_name=_('display image'), @@ -85,7 +82,8 @@ class Note(PolymorphicModel): """ Verify alias (simulate save) """ - aliases = Alias.objects.filter(name=str(self)) + aliases = Alias.objects.filter( + normalized_name=Alias.normalize(str(self))) if aliases.exists(): # Alias exists, so check if it is linked to this note if aliases.first().note != self: @@ -181,15 +179,15 @@ class Alias(models.Model): validators=[ RegexValidator( regex=settings.ALIAS_VALIDATOR_REGEX, - message=_('Invalid alias') + message=_('Invalid alias'), ) - ] if settings.ALIAS_VALIDATOR_REGEX else [] + ] if settings.ALIAS_VALIDATOR_REGEX else [], ) normalized_name = models.CharField( max_length=255, unique=True, default='', - editable=False + editable=False, ) note = models.ForeignKey( Note, @@ -209,11 +207,9 @@ class Alias(models.Model): Normalizes a string: removes most diacritics and does casefolding """ return ''.join( - char - for char in unicodedata.normalize('NFKD', string.casefold()) + char for char in unicodedata.normalize('NFKD', string.casefold()) if all(not unicodedata.category(char).startswith(cat) - for cat in {'M', 'P', 'Z', 'C'}) - ).casefold() + for cat in {'M', 'P', 'Z', 'C'})).casefold() def save(self, *args, **kwargs): """ @@ -229,7 +225,13 @@ class Alias(models.Model): raise ValidationError(_('Alias too long.')) try: if self != Alias.objects.get(normalized_name=normalized_name): - raise ValidationError(_('An alias with a similar name ' - 'already exists.')) + raise ValidationError( + _('An alias with a similar name ' + 'already exists.')) except Alias.DoesNotExist: pass + + def delete(self, using=None, keep_parents=False): + if self.name == str(self.note): + raise ValidationError(_("You can't delete your main alias.")) + return super().delete(using, keep_parents) diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 7a058607fb0b467f87d87f54fb65221e256ac8f8..4db2eda1aa41a4230db4714d6bbea7a583bf4b63 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.db import models @@ -7,16 +6,36 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.urls import reverse -from .notes import Note,NoteClub +from .notes import Note, NoteClub """ Defines transactions """ +class TransactionCategory(models.Model): + """ + Defined a recurrent transaction category + + Example: food, softs, ... + """ + name = models.CharField( + verbose_name=_("name"), + max_length=31, + unique=True, + ) + + class Meta: + verbose_name = _("transaction category") + verbose_name_plural = _("transaction categories") + + def __str__(self): + return str(self.name) + + class TransactionTemplate(models.Model): """ - Defined a reccurent transaction + Defined a recurrent transaction associated to selling something (a burger, a beer, ...) """ @@ -35,9 +54,11 @@ class TransactionTemplate(models.Model): verbose_name=_('amount'), help_text=_('in centimes'), ) - template_type = models.CharField( + template_type = models.ForeignKey( + TransactionCategory, + on_delete=models.PROTECT, verbose_name=_('type'), - max_length=31 + max_length=31, ) description = models.CharField( @@ -50,7 +71,7 @@ class TransactionTemplate(models.Model): verbose_name_plural = _("transaction templates") def get_absolute_url(self): - return reverse('note:template_update',args=(self.pk,)) + return reverse('note:template_update', args=(self.pk, )) class Transaction(models.Model): @@ -83,9 +104,7 @@ class Transaction(models.Model): verbose_name=_('quantity'), default=1, ) - amount = models.PositiveIntegerField( - verbose_name=_('amount'), - ) + amount = models.PositiveIntegerField(verbose_name=_('amount'), ) transaction_type = models.CharField( verbose_name=_('type'), max_length=31, @@ -127,7 +146,7 @@ class Transaction(models.Model): @property def total(self): - return self.amount*self.quantity + return self.amount * self.quantity class MembershipTransaction(Transaction): diff --git a/apps/note/signals.py b/apps/note/signals.py index 6e5d5c9e93312419ee338afc834a0bcc625a3f1d..ad376ee082e43230a7d2fd16be6f6975f982dd22 100644 --- a/apps/note/signals.py +++ b/apps/note/signals.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/apps/note/tables.py b/apps/note/tables.py index 4d4e9608c1236126b5cce3c3490bf882fd418f11..43a1ef7413566ce1c127131d7addf094910efbb0 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -1,20 +1,26 @@ -#!/usr/bin/env python +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + import django_tables2 as tables +from django.db.models import F + from .models.transactions import Transaction class HistoryTable(tables.Table): class Meta: - attrs = {'class':'table table-bordered table-condensed table-striped table-hover'} + attrs = { + 'class': + 'table table-condensed table-striped table-hover' + } model = Transaction - template_name = 'django_tables2/bootstrap.html' - sequence = ('...','total','valid') + template_name = 'django_tables2/bootstrap4.html' + sequence = ('...', 'total', 'valid') - total = tables.Column() #will use Transaction.total() !! + total = tables.Column() # will use Transaction.total() !! - def order_total(self, QuerySet, is_descending): + def order_total(self, queryset, is_descending): # needed for rendering - QuerySet = QuerySet.annotate( - total=F('amount') * F('quantity') - ).order_by(('-' if is_descending else '') + 'total') - return (QuerySet, True) + queryset = queryset.annotate(total=F('amount') * F('quantity')) \ + .order_by(('-' if is_descending else '') + 'total') + return (queryset, True) diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index ec0a0d5b3b9c4325ba1ab4988056606789ba335a..12530c6e22d4ea0eb86b46d018ffbcddb0c44f8c 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -1,11 +1,21 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + from django import template def pretty_money(value): - if value%100 == 0: - return "{:s}{:d} €".format("- " if value < 0 else "", abs(value) // 100) + if value % 100 == 0: + return "{:s}{:d} €".format( + "- " if value < 0 else "", + abs(value) // 100, + ) else: - return "{:s}{:d} € {:02d}".format("- " if value < 0 else "", abs(value) // 100, abs(value) % 100) + return "{:s}{:d} € {:02d}".format( + "- " if value < 0 else "", + abs(value) // 100, + abs(value) % 100, + ) register = template.Library() diff --git a/apps/note/urls.py b/apps/note/urls.py index 5e423d465b6339124063fe1f59e28e93f96d2f4a..fea911f6ac5ebdeaa871240fe56c764d024db675 100644 --- a/apps/note/urls.py +++ b/apps/note/urls.py @@ -1,15 +1,19 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.urls import path from . import views +from .models import Note app_name = 'note' urlpatterns = [ path('transfer/', views.TransactionCreate.as_view(), name='transfer'), - path('buttons/create/',views.TransactionTemplateCreateView.as_view(),name='template_create'), - path('buttons/update/<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'), ] diff --git a/apps/note/views.py b/apps/note/views.py index 08f4f63093a89226a6d3548b9ee7e9399449dacc..167ef4f0d4c4c8a45c251e7e60af2ebb622b98b1 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -1,13 +1,16 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from dal import autocomplete from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q +from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, ListView, DetailView, UpdateView +from django.views.generic import CreateView, ListView, UpdateView + +from .models import Transaction, TransactionTemplate, Alias +from .forms import TransactionForm, TransactionTemplateForm, ConsoForm -from .models import Transaction,TransactionTemplate -from .forms import TransactionTemplateForm class TransactionCreate(LoginRequiredMixin, CreateView): """ @@ -16,7 +19,7 @@ class TransactionCreate(LoginRequiredMixin, CreateView): TODO: If user have sufficient rights, they can transfer from an other note """ model = Transaction - fields = ('destination', 'amount', 'reason') + form_class = TransactionForm def get_context_data(self, **kwargs): """ @@ -25,24 +28,127 @@ class TransactionCreate(LoginRequiredMixin, CreateView): context = super().get_context_data(**kwargs) context['title'] = _('Transfer money from your account ' 'to one or others') + + context['no_cache'] = True + return context -class TransactionTemplateCreateView(LoginRequiredMixin,CreateView): + def get_form(self, form_class=None): + """ + If the user has no right to transfer funds, then it won't have the choice of the source of the transfer. + """ + form = super().get_form(form_class) + + if False: # TODO: fix it with "if %user has no right to transfer funds" + del form.fields['source'] + + return form + + def form_valid(self, form): + """ + If the user has no right to transfer funds, then it will be the source of the transfer by default. + """ + if False: # TODO: fix it with "if %user has no right to transfer funds" + form.instance.source = self.request.user.note + + return super().form_valid(form) + + +class NoteAutocomplete(autocomplete.Select2QuerySetView): + """ + Auto complete note by aliases + """ + def get_queryset(self): + """ + Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion. + Cette fonction récupère la requête, et renvoie la liste filtrée des aliases. + """ + # Un utilisateur non connecté n'a accès à aucune information + if not self.request.user.is_authenticated: + return Alias.objects.none() + + qs = Alias.objects.all() + + # self.q est le paramètre de la recherche + if self.q: + qs = qs.filter(Q(name__regex=self.q) | Q(normalized_name__regex=Alias.normalize(self.q)))\ + .order_by('normalized_name').distinct() + + # Filtrage par type de note (user, club, special) + note_type = self.forwarded.get("note_type", None) + if note_type: + types = str(note_type).lower() + if "user" in types: + qs = qs.filter(note__polymorphic_ctype__model="noteuser") + elif "club" in types: + qs = qs.filter(note__polymorphic_ctype__model="noteclub") + elif "special" in types: + qs = qs.filter(note__polymorphic_ctype__model="notespecial") + else: + qs = qs.none() + + return qs + + def get_result_label(self, result): + # Gère l'affichage de l'alias dans la recherche + res = result.name + note_name = str(result.note) + if res != note_name: + res += " (aka. " + note_name + ")" + return res + + def get_result_value(self, result): + # Le résultat renvoyé doit être l'identifiant de la note, et non de l'alias + return str(result.note.pk) + + +class TransactionTemplateCreateView(LoginRequiredMixin, CreateView): """ Create TransactionTemplate """ model = TransactionTemplate form_class = TransactionTemplateForm -class TransactionTemplateListView(LoginRequiredMixin,ListView): + +class TransactionTemplateListView(LoginRequiredMixin, ListView): """ List TransactionsTemplates """ model = TransactionTemplate form_class = TransactionTemplateForm -class TransactionTemplateUpdateView(LoginRequiredMixin,UpdateView): + +class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): """ """ model = TransactionTemplate - form_class=TransactionTemplateForm + form_class = TransactionTemplateForm + + +class ConsoView(LoginRequiredMixin, CreateView): + """ + Consume + """ + model = Transaction + template_name = "note/conso_form.html" + form_class = ConsoForm + + def get_context_data(self, **kwargs): + """ + Add some context variables in template such as page title + """ + context = super().get_context_data(**kwargs) + context['transaction_templates'] = TransactionTemplate.objects.all() \ + .order_by('template_type') + context['title'] = _("Consommations") + + # select2 compatibility + context['no_cache'] = True + + return context + + def get_success_url(self): + """ + When clicking a button, reload the same page + """ + return reverse('note:consos') diff --git a/entrypoint.sh b/entrypoint.sh index da32571a5e100b30349c044ba3fded737f3777ac..f05e962ae116b4831ea506704b828ab9c5988748 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,11 @@ #!/bin/bash +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + python manage.py compilemessages python manage.py makemigrations + +# Wait for database sleep 5 python manage.py migrate diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000000000000000000000000000000000000..3aadf83e4dca55978262aa6747f4aa56e3de05e8 --- /dev/null +++ b/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,517 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <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 "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 96da86e15f3170a57421012b625a2b5190e67d6f..bdf4fc8ff6512e2386875078be780cef87a506a2 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-08-14 15:14+0200\n" +"POT-Creation-Date: 2020-02-21 13:50+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <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:29 +msgid "transaction category" +msgstr "catégorie de transaction" -#: apps/note/models/transactions.py:30 apps/note/models/transactions.py:68 +#: 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/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/profile_detail.html:12 +#: templates/member/manage_auth_tokens.html:16 +msgid "Token" +msgstr "Jeton" + +#: templates/member/manage_auth_tokens.html:23 +msgid "Created" +msgstr "Créé le" + +#: templates/member/manage_auth_tokens.html:31 +msgid "Regenerate token" +msgstr "Regénérer le jeton" + +#: templates/member/profile_detail.html:11 msgid "first name" msgstr "" #: templates/member/profile_detail.html:14 -#, fuzzy -#| msgid "name" msgid "username" -msgstr "nom" +msgstr "nom d'utilisateur" -#: templates/member/profile_detail.html:22 +#: templates/member/profile_detail.html:17 #, fuzzy -#| msgid "account balance" -msgid "balance" -msgstr "solde du compte" +#| msgid "Change password" +msgid "password" +msgstr "Changer le mot de passe" -#: templates/member/profile_detail.html:26 +#: templates/member/profile_detail.html:20 msgid "Change password" +msgstr "Changer le mot de passe" + +#: templates/member/profile_detail.html:38 +msgid "Manage auth token" +msgstr "Gérer les jetons d'authentification" + +#: templates/member/profile_detail.html:54 +msgid "View my memberships" +msgstr "Voir mes adhésions" + +#: templates/member/profile_update.html:13 +msgid "Save Changes" +msgstr "Sauvegarder les changements" + +#: templates/member/signup.html:14 +msgid "Sign Up" msgstr "" #: templates/note/transaction_form.html:35 diff --git a/note_kfet/middlewares.py b/note_kfet/middlewares.py new file mode 100644 index 0000000000000000000000000000000000000000..73b87e363c32faf1d0fa836122fb2a2674347894 --- /dev/null +++ b/note_kfet/middlewares.py @@ -0,0 +1,38 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.http import HttpResponseRedirect + +from urllib.parse import urlencode, parse_qs, urlsplit, urlunsplit + + +class TurbolinksMiddleware(object): + """ + Send the `Turbolinks-Location` header in response to a visit that was redirected, + and Turbolinks will replace the browser's topmost history entry. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + + is_turbolinks = request.META.get('HTTP_TURBOLINKS_REFERRER') + is_response_redirect = response.has_header('Location') + + if is_turbolinks: + if is_response_redirect: + location = response['Location'] + prev_location = request.session.pop('_turbolinks_redirect_to', None) + if prev_location is not None: + # relative subsequent redirect + if location.startswith('.'): + location = prev_location.split('?')[0] + location + request.session['_turbolinks_redirect_to'] = location + else: + if request.session.get('_turbolinks_redirect_to'): + location = request.session.pop('_turbolinks_redirect_to') + response['Turbolinks-Location'] = location + return response + diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 234e70b9dc2d805f6c975155cb61a5bc74f2020a..68a40b887c39d6dde8bd55514cbea624048772e3 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -30,12 +30,17 @@ read_env() app_stage = os.environ.get('DJANGO_APP_STAGE', 'dev') if app_stage == 'prod': from .production import * - DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD','CHANGE_ME_IN_ENV_SETTINGS'); - SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY','CHANGE_ME_IN_ENV_SETTINGS'); - ALLOWED_HOSTS.append(os.environ.get('ALLOWED_HOSTS','localhost')); + DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD','CHANGE_ME_IN_ENV_SETTINGS') + SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY','CHANGE_ME_IN_ENV_SETTINGS') + ALLOWED_HOSTS.append(os.environ.get('ALLOWED_HOSTS','localhost')) else: from .development import * +try: + from .secrets import * +except ImportError: + pass + # env variables set at the of in /env/bin/activate # don't forget to unset in deactivate ! diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index e583d8a6b0edd7e4105f502d0c5442a857b1e46f..9019b4e07c39a3a0588e91bec7c7b580a4d2d4ba 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later import os @@ -50,11 +49,18 @@ INSTALLED_APPS = [ 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', + # API + 'rest_framework', + 'rest_framework.authtoken', + # Autocomplete + 'dal', + 'dal_select2', # Note apps 'activity', 'member', 'note', + 'api', ] LOGIN_REDIRECT_URL = '/note/transfer/' @@ -69,6 +75,7 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', 'django.contrib.sites.middleware.CurrentSiteMiddleware', + 'note_kfet.middlewares.TurbolinksMiddleware', ] ROOT_URLCONF = 'note_kfet.urls' @@ -117,6 +124,18 @@ AUTHENTICATION_BACKENDS = ( 'guardian.backends.ObjectPermissionBackend', ) +REST_FRAMEWORK = { + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': [ + # TODO Maybe replace it with our custom permissions system + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + ], + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.TokenAuthentication', + ] +} + ANONYMOUS_USER_NAME = None # Disable guardian anonymous user GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type' @@ -127,6 +146,7 @@ GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_c LANGUAGE_CODE = 'en' LANGUAGES = [ + ('de', _('German')), ('en', _('English')), ('fr', _('French')), ] diff --git a/note_kfet/settings/development.py b/note_kfet/settings/development.py index f6d48776f5a963009fafd26180bb353c8f0befe5..60055ee21d8ff5c43ce09b64a30ecb3a1fd7f374 100644 --- a/note_kfet/settings/development.py +++ b/note_kfet/settings/development.py @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + ######################## # Development Settings # ######################## diff --git a/note_kfet/settings/production.py b/note_kfet/settings/production.py index 02c46ed21af9d0b02c49acb65f029f132c439a71..296c17a4948f1e48ddcca3eb3ffae39ebdaef2ec 100644 --- a/note_kfet/settings/production.py +++ b/note_kfet/settings/production.py @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + ######################## # Production Settings # ######################## diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 88bb6bb9c7b3b73b8528f0bd5162f1f942988205..303e229aaed0ea291620383ce71e7c27fecbf410 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin @@ -19,4 +18,7 @@ urlpatterns = [ path('accounts/', include('django.contrib.auth.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), + + # Include Django REST API + path('api/', include('api.urls')), ] diff --git a/note_kfet/wsgi.py b/note_kfet/wsgi.py index 94a8e054fa4ec9bb4d116e5e6fd380065d6a938e..b89430ec723d2522deee1c0f3c93572d4120c5a8 100644 --- a/note_kfet/wsgi.py +++ b/note_kfet/wsgi.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2016-2019 by BDE # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/requirements.txt b/requirements.txt index 39b32fdf87288b75dfd9883501f9a1efe73fbae3..21c24808b0f562f2b0f71ea19a1ed1d2193b7b89 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,20 @@ certifi==2019.6.16 chardet==3.0.4 defusedxml==0.6.0 -Django==2.2.3 +Django~=2.2 django-allauth==0.39.1 +django-autocomplete-light==3.5.1 django-crispy-forms==1.7.2 django-extensions==2.1.9 django-filter==2.2.0 django-guardian==2.1.0 django-polymorphic==2.0.3 +djangorestframework==3.9.0 +django-rest-polymorphic==0.1.8 django-reversion==3.0.3 django-tables2==2.1.0 docutils==0.14 +psycopg2==2.8.4 idna==2.8 oauthlib==3.1.0 Pillow==6.1.0 diff --git a/static/favicon/android-chrome-192x192.png b/static/favicon/android-chrome-192x192.png index 5b31f298276637f9997dccc8dad10be846c5d136..38b7efeee89060046077d5b10dd88b9a5df3565f 100644 Binary files a/static/favicon/android-chrome-192x192.png and b/static/favicon/android-chrome-192x192.png differ diff --git a/static/favicon/android-chrome-512x512.png b/static/favicon/android-chrome-512x512.png index bb9e4daaed45893728a5cef590d245a38a417138..321f2b5a05cfe5c06db23c0d33201b59c365a0df 100644 Binary files a/static/favicon/android-chrome-512x512.png and b/static/favicon/android-chrome-512x512.png differ diff --git a/static/favicon/apple-touch-icon.png b/static/favicon/apple-touch-icon.png index c0b462bde2ab8f89522ebcc9c975878f39a26acf..8f6be04d0145d5d52577564df1e90cf42650e953 100644 Binary files a/static/favicon/apple-touch-icon.png and b/static/favicon/apple-touch-icon.png differ diff --git a/static/favicon/browserconfig.xml b/static/favicon/browserconfig.xml index 49604f0ed5d0f3eccff6bc07dc504a3e7f206f8b..c8771dff66fb5a1d6a1f8abc5058da561a89bed6 100644 --- a/static/favicon/browserconfig.xml +++ b/static/favicon/browserconfig.xml @@ -3,7 +3,7 @@ <msapplication> <tile> <square150x150logo src="/static/favicon/mstile-150x150.png"/> - <TileColor>#da532c</TileColor> + <TileColor>#00a300</TileColor> </tile> </msapplication> </browserconfig> diff --git a/static/favicon/favicon-16x16.png b/static/favicon/favicon-16x16.png index 5db7ed945375943b1950cd0878c568b7f8d60d09..14ff80a4050e9edc31a7120246d5ccae26b8b827 100644 Binary files a/static/favicon/favicon-16x16.png and b/static/favicon/favicon-16x16.png differ diff --git a/static/favicon/favicon-32x32.png b/static/favicon/favicon-32x32.png index a3d9263f297fd183b2a402677135c0ddd85cfd60..cb0827345bd0ff74a7c4c24e5ddcc61cc6ebe397 100644 Binary files a/static/favicon/favicon-32x32.png and b/static/favicon/favicon-32x32.png differ diff --git a/static/favicon/favicon.ico b/static/favicon/favicon.ico index 53393cda7e0850a26d561567294181c849f5021e..0bf544e051b9061348db04aa2b9c1b9655780860 100644 Binary files a/static/favicon/favicon.ico and b/static/favicon/favicon.ico differ diff --git a/static/favicon/mstile-150x150.png b/static/favicon/mstile-150x150.png index 378274c99391b5f2d3524fe3f6e33c0eed5286a2..0c44361b277e35f3759f20054fd4e000861bf0ee 100644 Binary files a/static/favicon/mstile-150x150.png and b/static/favicon/mstile-150x150.png differ diff --git a/static/favicon/safari-pinned-tab.svg b/static/favicon/safari-pinned-tab.svg index a8425e8a0f6af47044afaebbd730ff595ac30d68..cb5555d64d6c99079c82f5e380e9b9d42f467313 100644 --- a/static/favicon/safari-pinned-tab.svg +++ b/static/favicon/safari-pinned-tab.svg @@ -1 +1 @@ -<svg version="1" xmlns="http://www.w3.org/2000/svg" width="1678.667" height="1678.667" viewBox="0 0 1259.000000 1259.000000"><path d="M652.1 9.6C618 101 575.3 192.4 528 275c-19.3 33.7-21.4 37.1-22.1 35.8-.3-.7-2.6-9.8-4.9-20.1-2.4-10.3-4.7-18.2-5.1-17.5-.9 1.6-5.9 22.3-14.4 59.3-3.7 15.9-9.4 38.3-12.6 49.7-8.4 29.7-10.9 42.4-11.6 60.3-.9 24.1 2.5 37.8 11.9 48.1 2.8 3.1 3.6 4.5 2.3 4-20.1-7.5-38.8-12.5-58-15.3-18-2.6-51.4-2.3-70.4.6-56.9 8.7-106.8 33.5-144.8 72.2-21 21.3-31.3 40.5-31.3 58.3.1 9.2 1.3 11.9 13.8 28.4 5.6 7.3 10.6 14.9 11.2 16.8 1.1 3.2.8 4.1-3 11.7-10.2 19.9-12.8 23.7-45.8 68.4-7.8 10.5-14.2 19.6-14.2 20.1 0 2.4 21.5 4.3 35.4 3.1 13.4-1.1 23.9-3.8 35.8-8.9 5.8-2.5 8.8-3.3 8.4-2.4-.3.8-5.9 15.4-12.6 32.4-6.6 17.1-12 31.5-12 32 0 1.5 3 1.2 12.4-1 18.2-4.2 40.2-12.3 57.9-21.2 4.3-2.1 8.2-3.8 8.6-3.6.4.2-4.5 23.7-11 52.3-6.5 28.6-11.9 52.1-11.9 52.3 0 .6 5-2.5 11.5-7.2 31.5-22.6 60.2-53.4 79.1-84.8 11-18.3 15.9-32.8 17.1-50.9 1.2-18.6-1.2-22.5-22.7-37.3-7.4-5.1-13.6-10-13.8-10.9-.2-.9 1.9-4.9 4.6-9 23.5-34.7 60.1-66.6 99.2-86.2 29.9-15 65.4-25.1 92.7-26.2 11.1-.5 12.5-.4 16.7 1.6 6 3 9 6.7 14.2 17.6 9.1 19.2 16.8 47.5 19.3 70.7 1.3 11.2 1.4 42 .2 49.1-1.5 9.3-4.3 8.9-17.3-2.7-7.5-6.7-10.2-9.9-13.2-15.5-3.7-7.1-5.8-9.1-9.5-9.1-3.4 0-4.2 3.1-2 7.9 1 2.4 1.9 5.8 1.9 7.7 0 3.3 2.8 7.4 5 7.4.6 0 1 1.3 1 2.9 0 1.6 1.3 5.7 2.9 9.1 6.8 14.3 4.2 26.4-7 32-4.6 2.4-11.6 2.6-14.7.5-1.2-.9-3.6-3.6-5.3-6.1-1.7-2.5-5.5-6.1-8.5-8-3-1.9-6.1-4.5-6.9-5.7-.8-1.3-2.2-6.3-3-11.2-.9-4.9-2.6-10.7-3.8-12.9-2.5-4.6-7.8-8.6-11.3-8.6-4.5 0-10.2-6.7-15.9-18.5-2.9-6-6.2-13.1-7.3-15.8-2.4-5.3-5.6-6.4-7.1-2.4-1.1 2.9.2 15.3 2.3 22.2 1.3 4.1 2.2 5.4 5.3 7 4.4 2.3 6.2 6.3 6.2 13.8.1 6.3 2.3 10.2 7.1 12.7 4.9 2.5 5.6 3.8 6.4 12.8l.6 7.3-3 1.7c-1.6.9-3.2 2.5-3.5 3.4-.7 1.9 2.2 10.5 8.9 26.5 6.2 14.7 8.2 24.3 6.6 31.7-1.4 7-4.8 11.5-14.8 20-16.3 13.8-20.2 24.5-18.2 51.3 1.4 19.2 1.2 36-.5 42.5-.8 3.1-3.8 11.4-6.7 18.5-4.6 11.4-5.2 13.6-5.2 20.3-.1 8.8 1.4 12.2 8.1 19.1l4.4 4.6-1 9.6c-2.6 23.5.1 86.1 5.4 123.3 3.2 21.8 4.8 29.8 8.6 43.3 3.7 12.8 15.1 45.6 17.2 49.8 2.1 4 7.9 9 19.2 16.4 23 15.2 51 25.1 86.5 30.6 16.2 2.4 54.7 2.4 71.5 0 23.7-3.5 45.1-9.4 66.2-18.2 26.5-11.1 33.9-20.1 45.1-54.1 11.4-34.3 20.8-78.8 24.7-116.9 3.7-35.8 3.9-81 .5-115.5-2.3-23.5-8.3-60.2-12.1-74.4-.6-2.2-1.2-9-1.3-15.1-.3-13-1.8-17.2-9-25.2-6.3-6.9-8.8-13.1-9.4-22.3-.9-15.4 4.7-28.5 22.3-52 14.2-19 18.5-25.9 18.5-29.7 0-6-7.4-8.4-12.5-4-1.4 1.2-4.5 5.1-6.8 8.6-3.7 5.6-4.8 6.6-8.9 7.8-3.4 1-5.8 2.6-9.4 6.6-7 7.8-11.7 14.2-17.1 23.7-2.6 4.7-5.3 8.5-6 8.5-.6 0-2.7-2.1-4.6-4.6-3.3-4.3-5.5-5.7-17.7-11.3-2.4-1.2-2.5-1.5-2.1-8.9.5-11.2-2.4-14.6-13.3-16.1-9.7-1.2-12.6-5.5-12.6-18.6 0-9.4-1.3-13.1-6.9-19.8-5-6-8.1-12.6-8.1-17.7 0-7.4 2.9-24.5 5.1-29.4 1.6-3.9 1.8-5.3.9-7-1.3-2.5-3.1-2.6-6.9-.7-3.5 1.8-4.8 4.9-6.2 13.9-1.6 11.1-3.8 18.7-7 24.8-3.1 5.7-12.3 15.8-13.5 14.6-1.3-1.3-4.4-16.5-5.5-27.2-1.1-10.4-.6-33.2.7-38.2l.6-2.3 2.5 2c3.9 3 8.8 2.6 12.4-1l3-3 3 3.5c3.1 3.5 10.4 6.5 15.8 6.5 4.4 0 9.9-2 13.1-4.6l3-2.6 1.1 2.9c2 5.4 7.7 9.4 15.1 10.8 3.1.6 4.7.2 9.5-2.6 3.1-1.8 7.4-4.8 9.3-6.5l3.6-3.3 6.5 1.5c3.5.8 10.7 2.8 15.9 4.4 71.8 22.1 151.5 19.6 222.5-7 7.4-2.8 17.1-6.8 21.6-9 4.6-2.2 9.1-4 10-4 1 0 5.8 5.2 11.7 12.7 5.5 7.1 11.5 14.2 13.3 16 8.1 7.6 18.7 6.5 39.5-4 11.2-5.6 30.7-17.3 38.1-22.9 6.4-4.9 23.8-24.7 22.6-25.8-.4-.4-10-1.9-21.3-3.4-23.8-3.1-25-3.3-25-4.6 0-.5 5.3-10.5 11.9-22.3 16.3-29.2 16.5-29.6 14.3-29.1-.9.2-12.3 2.2-25.2 4.4-12.9 2.2-24.5 4.2-25.7 4.5-2 .5-1.6-.7 3.7-11.8 6.1-12.7 10.4-23.7 14.5-36.7 3.4-10.8 5.7-20.1 5.1-20.7-.3-.3-10.8 6.7-23.3 15.4-26.5 18.7-35.1 24.3-36.9 24.3-.7 0-3.2-2.8-5.6-6.2-5.6-8-8.5-10-15.1-10.6-11.5-.9-20.1 3.2-56.6 27-55.6 36.5-94.3 55-135.6 64.8-18.6 4.5-51.8 10-64.1 10.7l-11.2.6-1.6-3.1c-.9-1.8-2.9-3.8-4.4-4.5-3.5-1.8-20.1-2.9-22.6-1.5-1.5.8-3 .2-7.8-3.2-7.9-5.6-15.5-9-20.1-9-2.1 0-6.3.9-9.4 2-6.6 2.3-10.2 2.5-10.9.7-.4-1.1 5.7-19.8 10.5-32l1.1-2.8 7.8 9.3c4.2 5.1 8.6 9.4 9.6 9.6 1.8.4 1.9-.1 1.3-6.4-.4-3.8-1-8-1.3-9.3-.6-2.2-.2-2 4.9 1.8 8.2 6.2 20.7 10.4 20.7 7 0-.6-1.6-3.7-3.5-6.9s-3.5-6-3.5-6.4c0-.3 2.7 1.2 6 3.5 5.4 3.6 19.6 9.6 20.6 8.7.2-.2-2.8-5.1-6.6-10.8-3.8-5.7-6.8-10.6-6.6-10.8.2-.2 2.6 1.2 5.3 3.1 7.3 5.2 16.6 9.6 25.1 11.8 4.1 1 7.6 1.8 7.8 1.6.1-.1-5.5-7.2-12.6-15.7s-12.9-15.8-12.9-16.2c-.1-.4 3.7 2.2 8.3 5.6 4.7 3.5 12.1 8.2 16.5 10.5 7.7 4 19.1 8.5 19.7 7.8.2-.2-4-5.7-9.3-12.3-5.3-6.5-11.2-13.9-13.2-16.4l-3.6-4.4 4 2.3c13.5 8 21.8 12.7 27.9 15.8 9.3 4.7 24.6 11.3 24.6 10.6 0-1.1-19.1-23.1-30.2-34.7-8.6-9.1-10.3-11.3-7.2-9.4 13.3 8.4 40 19.3 52.4 21.3l4.4.7-5.1-5.8c-2.8-3.3-8.6-9.3-12.8-13.4-4.2-4.1-8.5-8.7-9.6-10.2l-2-2.8 6.6 3c13 5.8 35.6 11.7 41.6 10.8 2-.2-1.6-3.3-18.6-16.2-22.6-17.1-24.1-18.4-21-17.1 9 3.8 25.7 8.7 33.5 10 12.4 2 36.1 2.1 34 .2-.8-.8-10.8-7.1-22.3-14.2-11.4-7.1-20.6-12.9-20.5-13.1.2-.1 3.7.5 7.8 1.4 5.4 1.1 13.1 1.6 27.5 1.6 17.1 0 21.5-.3 30.4-2.2 19.7-4.3 20.3-3.7-11-12.7l-27.4-8 12.5-.7c24-1.4 46.8-5.7 63.4-12.1 6.4-2.4 7.6-3.2 6.1-3.8-1.1-.5-16.8-3.8-34.9-7.4-18.1-3.5-33.1-6.7-33.4-7-.3-.3 1.1-.5 3.1-.5 6.3 0 31.8-4.8 45.2-8.5 16.4-4.5 32.4-10.9 48.9-19.4 11.9-6.1 12.7-6.6 9.5-6.9-3-.3-37 1.9-51.4 3.3-4.6.5-4.1.2 5.5-3.3 29.4-10.9 53.1-23.6 71.6-38.4 3.5-2.8 6.2-5.3 5.9-5.6-.4-.4-13 2.8-41.6 10.4-7 1.9-13.1 3.4-13.5 3.4-.5 0 5.4-3.1 13.1-6.9 31.6-15.5 55.9-35.5 76.5-62.9 4.4-5.9 8-11.1 8-11.5 0-.5-1.5.2-3.2 1.5-11.8 8.8-51.5 29.8-56.4 29.8-.3 0 3.6-3 8.5-6.8 34-25.6 61-57.9 80.2-96.2 9.5-18.8 9.5-18.8-6.8-2.9-17.2 16.8-30.9 27.9-48.3 39.4-11.6 7.7-32.6 19.5-34.5 19.5-.5 0 3.1-2.7 8.1-6.1 29.4-20.1 57-48.2 77.8-79.3 10.3-15.4 29.7-55 29.6-60.5-.1-1.5-.2-1.5-1.3 0-4.8 6.3-28.2 31-37.9 39.9-51.4 47.3-107.1 83.6-198.8 129.5-28.8 14.4-70.4 34.5-71.4 34.5-.2 0 .7-3.5 2.1-7.7 2.2-7 4.4-24.3 3.1-24.3-.3 0-10.4 12.6-22.4 28.1l-21.8 28.1-2.7-11c-4.2-17.2-14.4-35.2-25.8-45.5-3.5-3.2-4.5-3.5-12.7-4.2-12.2-1-22.2-3.7-33.1-9-5.1-2.4-9.3-4.2-9.3-3.9 0 .3 2.5 4.8 5.6 10 3.1 5.3 5.4 9.7 5.2 9.9-.2.2-7-1.3-15.1-3.5-8.1-2.1-14.7-3.4-14.7-2.9 0 2.5 3.2 8.2 6.9 12.2l4.1 4.4-11.2.5c-10 .4-12.3.8-20 4-4.9 1.9-8.8 3.3-8.8 3.1 0-.2 2.2-4.6 5-9.6 10.3-19.3 19.9-46.8 24-69.1 1.2-6.1 2.2-11.9 2.4-12.9.2-.9.1-1.7-.3-1.7s-6.5 6.7-13.5 15c-7.1 8.2-13 15-13.1 15-.1 0 2.8-6 6.6-13.3 11.9-23.1 20.3-48.2 24.4-72.7 1.6-9.2 3.4-49 2.2-49-.3 0-4.5 6.9-9.4 15.4-4.8 8.5-8.9 15.3-9.1 15.2-.1-.2 1.3-5.7 3.2-12.2 8.2-28.4 12.2-59.4 11.3-85.9-.3-8.8-1-17.3-1.5-18.9-.8-2.8-1-2.4-4.1 6zm4.5 110.1c-1.3 20.7-4.5 38.6-9.6 54.5-6.3 19.8-30.4 63.9-43.1 79.1-3 3.5-21.5 24.5-41.3 46.6-33.7 37.8-35.9 40.1-37.6 38.6-1.7-1.5-2-1.3-5.4 3.1-4.1 5.4-4.6 4.2-2-4.7 2.5-8.2 4.3-11.3 17.3-28.6 21.3-28.4 49.9-72 70.3-106.8 5.7-9.9 19.6-34.8 30.8-55.3 11.2-20.4 20.6-37.2 20.8-37.2.3 0 .2 4.8-.2 10.7zm414.7 69c-1.5 3.9-9.9 18.3-15.6 26.8-21.5 32.1-53.1 61.1-82.4 75.9-21.7 10.8-65.9 24.9-104.3 33.2-15.6 3.4-43.8 8.4-47 8.4-1.2 0-1.5-1.4-1.5-7.2l.1-7.2 17.9-5.6c32.9-10.3 52.6-18.6 93-39 17.1-8.7 39.8-19.5 50.5-24 34.5-14.5 55-28.2 80.5-53.9 4.9-5 9.1-9.1 9.2-9.1.1 0-.1.8-.4 1.7zM700 266.1c17.4 5.8 22.1 8.6 27.6 16.1l3.9 5.3.5-4 .6-4 3.2 5c4.7 7.2 9.8 21.5 7.6 21.5-1.9-.1-4.9-3.2-8.8-9.1-5.2-8-7.8-9.8-16.2-11.5-4.7-1-8.8-2.8-12.9-5.4-5.1-3.4-6.1-3.7-6.4-2.3-.2 1.1 1.5 3.4 4.8 6.3 4.9 4.3 5.1 4.7 5.1 9.7 0 6.3 1.4 7.6 3.4 2.9 1.2-3 4.1-5.6 6-5.6.4 0-.5 1.7-1.8 3.7-3.2 4.9-4 8.2-2.7 11.6 2.2 5.7 8.5 6 14.6.6 2.2-2 4.1-2.9 5.2-2.5 1.4.6 1.3 1-.7 3.1-3.3 3.5-11.7 7.5-15.7 7.5-1.9 0-5.6-1.1-8.3-2.5-5.8-2.9-6.8-3-9.2-.8-3.9 3.6-1.6 7.7 9.6 16.7l4.8 3.9-6.2 6.1c-5.9 5.8-9.8 11.8-12.5 19.6l-1.3 3.5-1.1-2.9c-.9-2.2-1.4-2.6-2.4-1.7-2.4 1.9-9.8 17.9-13.1 28.1-1.8 5.5-3.6 9.7-3.9 9.3-1.3-1.2-.7-22.1.8-29.9.8-4.3 1.3-7.9 1.1-8.1-.7-.7-5.5 8.8-9 17.6-3.4 8.3-9 26.2-11.7 36.8-2.3 9.5-3.2 6.1-2.5-9.5.7-16 2.7-31.3 5.5-43.5 2.2-9.5 1.9-9.3-8.8 5.2-8.7 11.9-11 15.3-19.4 29.6-6.7 11.3-6.7 11-2-8.5 3.4-14.4 9.3-29.2 15.2-38.1 3.3-5 4.6-7.7 3.9-8.4-1.4-1.5-22.8 21.1-31.3 33.1-7.9 11.1-15.6 19.5-16.2 17.7-1.2-3.4 13.7-29.4 22.6-39.7 4-4.6 4.2-5.1 2.5-6.3-1.7-1.2-1.6-1.6 1.9-5.1 4.2-4.3 4.5-5.2 1.7-5.2-1.1 0-3.1 1.7-4.9 4.3-5.5 8.1-16.6 16.3-21.1 15.5-3.1-.5 30.6-37.8 40.9-45.1 4.4-3.3 8.1-6.3 8.1-6.7 0-1.7-2-2.1-5.5-1.1-3.2.9-3.5.8-3.5-1.2 0-2.9 3.4-8.3 6.1-9.7 1.2-.7 7.4-1.6 13.8-2.2 11.8-.9 16.1-1.9 16.1-3.8 0-.5-3.1-2.6-7-4.5-6.8-3.4-8.8-5.5-5.2-5.5.9 0 7.5.7 14.6 1.5 7.1.9 13.1 1.3 13.4 1.1.2-.3-2.4-3.5-5.8-7.1-3.5-3.6-5.7-6.5-4.9-6.5.8 0 8.4 2.3 16.9 5.1zm-60.1 23.6c-5.3 14.1-18.9 33.7-37.2 54-7 7.7-10.7 11-15.2 13.3-6.5 3.3-62.5 24.3-63.2 23.7-.2-.2.1-1.7.7-3.2 1.1-3 .5-6.5-1.2-6.5-.6 0-2.6 1-4.5 2.1-4.2 2.6-5.3 1.8-2.7-1.9 2.3-3.2 9.1-7.6 26.4-17.4 21.7-12.2 43.3-26.7 70.3-47 14.4-10.9 26.5-19.8 26.9-19.8.4 0 .3 1.2-.3 2.7zm88.1 9.8c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5.7-1.5 1.5-1.5 1.5.7 1.5 1.5zm251.6 41c-5.6 4.9-30.1 16.8-44.1 21.4-21.4 6.9-40.5 9.8-71 10.8-15.8.5-55.1-1-56.3-2.1-.1-.2.2-1.5.7-2.9.6-1.6.7-3.8.1-5.7-1.5-5.3 3-6.5 31.5-8.4 91.4-6.2 112.1-8.5 135-15 7.6-2.2 8.4-1.8 4.1 1.9zm-384.1 54.4c-2.7 2.6-27 17.7-34 21.1-11.6 5.8-18.7 7.3-31.9 6.8-13.6-.4-14.8-1-10.9-5l2.8-3.1-5.7.7c-3.2.3-5.8.2-5.8-.3 0-2 4.3-6 8-7.5 2.3-.9 8.7-2.1 14.3-2.7 12.2-1.2 21.7-2.8 43.7-7.3 17.7-3.7 21-4.1 19.5-2.7zm201 1.5c3.3.9 11.9 2.3 19 3.1 23.3 2.6 28.3 3.5 50.2 9.1 19.5 4.9 40.2 9.1 53.8 10.9 6.7.9 4.7 1.9-10.6 4.9-26.3 5.3-53.4 6.4-72.4 3.2-8.6-1.5-48.4-13.9-53.8-16.8-1.7-.9-1.7-1.1.3-4.2 1.1-1.8 2-4.7 2-6.4 0-1.7.3-3.8.6-4.7.7-1.8 1.1-1.8 10.9.9zm18.2 57.3c48.3 27.7 48.4 27.8 43.9 28.1-5.5.4-18.3-1.9-30.3-5.4-19.4-5.6-36.7-14.5-66.2-33.9-9.6-6.3-10.3-7-8.5-8.2 1-.7 3.9-2.9 6.4-4.7 2.5-1.9 4.9-3.5 5.3-3.5.5-.1 22.7 12.4 49.4 27.6zM519.3 448c14.5 4.2 32.4 7.7 47.9 9.6 7.3.8 13.5 1.7 13.8 2 .9 1-5 5.1-8.9 6.3-6.8 2-27.2 5.1-33.7 5.1-8.4 0-15.6-1.6-23.2-5.2-6.5-3-8.3-5.1-6.3-7.1 2.5-2.5.9-3-7.4-2.4-4.7.3-8.5.3-8.5-.1 0-1.6 11.7-11.1 13.7-11.2 1.3 0 6.9 1.3 12.6 3zm233 18.4c10.6 7.8 24.2 18.7 30.2 24.1 6.1 5.4 16.1 13.9 22.2 18.8 6.2 5 11.1 9.2 10.9 9.5-.2.2-5.2-1.3-11.1-3.2-12.5-4.2-26.5-11.2-39.2-19.6-11.5-7.5-44.7-33.7-45.1-35.6-.2-.7 1.6-3.6 4-6.4 4.1-4.8 4.3-4.9 6.6-3.4 1.3.9 11 8 21.5 15.8zM722.1 498c37.9 39.7 39.4 41.3 38.1 41.8-1.9.6-13.4-5.2-22-11-8.3-5.7-30.6-26.9-43.5-41.2l-8-8.9 3.9-1.2c2.2-.6 4.9-1.7 5.9-2.3 1.1-.6 2.3-1 2.6-.9.3.1 10.7 10.8 23 23.7zM392 482c40.6 3.5 75.2 13.7 111.5 32.9 14.4 7.6 21 11.8 57.5 35.8 52.9 34.7 72.5 45 103.7 54.3 6.5 1.9 7.4 2.6 9 5.8 2.2 4.5 5 5.1 10 2.2l3.6-2.1-.7 5.1c-.7 5.6.3 6 2.4 1 1.3-3.1 2.4-3.7 3.5-2 .9 1.4 4.8 1.2 6.4-.3 1.1-1 1.3-1 .7 0-1 1.9.5 1.6 1.9-.3 1-1.4 1.3-1.3 2.8 1 2.2 3.3 7.1 3.6 8.7.5.7-1.1 1.4-1.8 1.7-1.6.2.3-.3 1.9-1.2 3.7-2.4 4.7-1.2 4.7 2.5 0 2.3-2.9 3.6-3.8 4.3-3.1 1.5 1.5 5 1.4 6.5-.1.9-.9 1.2-.8 1.2.5 0 3.1-4.6 6.7-8.5 6.7-2.3 0-3.8.5-4.2 1.6-1.2 3.3-2.8 2.9 58.4 11.9 28.8 4.3 38.1 5.2 26.1 2.7-23.5-5-41.5-8.4-53.8-10.1-21.5-3.1-21.7-3.1-19.8-4.2.8-.5 10-1.8 20.4-2.9 10.4-1.1 22.3-2.7 26.4-3.5 8.6-1.8 24.4-6.4 23.8-7.1-.3-.2-5.8.7-12.4 2.1-15.7 3.2-30.6 5.3-43 6.1l-10.1.7.5-3.2c.6-4.2 2.2-4.8 20.7-7.2 72.8-9.6 140.7-35.3 200-75.7 7.7-5.3 17.2-11.8 21.2-14.4 13.9-9.4 25.8-12.5 30.8-7.8 3.5 3.3 22.8 38.7 41.5 76 8.3 16.7 8.3 15.6.2 15.2-6.6-.4-6.8-.3-22.2 7.3-46.8 23.2-87.4 36.1-139 44.2-15.9 2.4-55.2 2.4-73 0-26.7-3.7-82.5-16.3-84.5-19-.8-1.1-1.6-1.3-2.6-.6-2 1.3-6.7 1.1-9.2-.3-1.2-.7-7.5-1.5-14.2-1.8l-12-.5-.5-3.3c-.5-3.6-2.7-4.1-2.2-.5.4 3.2-1.6 3.8-4.3 1.3-1.8-1.7-4.3-2.4-10.3-3.1-9.6-1.1-24.5-4.8-35.5-9-4.5-1.7-22.4-10-39.7-18.4-43.2-21-57.4-25.5-80.9-25.5-30.9 0-75.7 13.2-109.5 32.1-48.5 27.3-88.8 68-113.4 114.6-3.7 7.1-7.4 12.9-8.3 13.1-1.9.3-21.2-13.6-36.1-25.9-23.5-19.6-44-41.3-63.8-67.6-13.5-18.1-16.1-22.7-16.2-29.3-.1-13 21.6-45.2 45-67 48-44.5 112.6-66.6 178.2-61zm119.5 7c10.9 6.3 30.1 15.1 40.4 18.5 6.1 2 5.7 2.5-3.4 4.8-9.2 2.3-22.3 3.1-26.6 1.5-5.3-1.9-28.9-17.1-28.9-18.6 0-.7 2-2.1 4.5-3.2 3.2-1.4 4.5-2.5 4.5-4 0-1.8-.6-2-7.5-2-7.8 0-9.5-1.1-4.7-2.9 5.3-2 10.2-.7 21.7 5.9zm557.9 14.6c-2.5 7.1-8.2 21-12.5 30.8-4.3 9.9-7.9 18.8-7.9 19.9 0 1.8.5 1.9 6.8 1.4 7.1-.7 26.7-4.5 37.3-7.2 3.3-.9 6.5-1.5 7-1.3.5.2-4.7 10.1-11.6 22-7 12.1-12.5 22.8-12.5 24.2 0 1.8.9 3 2.8 4 2.9 1.5 24.1 5.2 35.5 6.2l6.9.6-2.4 2.5c-4.8 5.2-20.7 17.9-30 24-15 9.9-40 21.3-46.5 21.3-3.6 0-3.2-1 9.5-24 2.3-4.2 4.8-10.3 5.7-13.8 2.7-10.5 1.7-13.5-17-50.8-12-23.8-16.6-33.9-16.3-35.9.2-2.3 1.9-3.6 10.8-8.7 13-7.5 20.2-12.6 30.5-21.6 4.4-3.8 8.1-6.8 8.3-6.7.2.1-1.8 6.1-4.4 13.1zm-388.4 1c0 .8 3.2 4.9 7.2 9.2 6.6 7.1 29.3 36.5 31.8 41.3 1.1 2 1.1 2-2.2.3-10.4-5.2-33.4-25.8-36.7-32.7-2-4.3-2.5-7.7-2.2-16 0-2.9.5-4.6 1.1-4.2.6.3 1 1.3 1 2.1zm-6.7 41.6c1.4 1.8 3.3 4.9 4.2 6.8 1.9 3.7 4.7 13.2 4 13.8-.4.4-9.5-12.9-11.8-17.5-1.6-3-1.4-6.3.4-6.3.4 0 1.9 1.5 3.2 3.2zm374.2 62c2.4 1.9 2.7 2.6 2.3 6.9-.6 6-7.4 19.9-16.8 33.9-.8 1.2-1.8.6-5.5-3.5-5.8-6.3-18.9-23.4-18.4-23.9.2-.2 3.9-1.8 8.2-3.5 4.3-1.7 11.7-5.1 16.5-7.6 4.8-2.4 9.2-4.5 9.8-4.5.6 0 2.4 1 3.9 2.2zm-328 15.8c.3.5-.1 1-1 1s-1.3-.5-1-1c.3-.6.8-1 1-1 .2 0 .7.4 1 1zm0 14c-.3.5-1.3 1-2.1 1s-1.4-.5-1.4-1c0-.6.9-1 2.1-1 1.1 0 1.7.4 1.4 1zM668 656.7c-1.1 2.1-2 5.6-2 7.8-.1 2.2-.7 6.9-1.5 10.5-1.8 8-1.8 16.4-.2 21 .7 2 3.4 6.1 6 9.3 5.3 6.6 6.6 10.4 7.6 23.6 1.2 15.5 4.3 19.2 16.2 19.7 3.8.1 7.6.8 8.4 1.4 1.1.9 1.4 3.4 1.2 10-.2 10.2 1.1 12.6 7.5 13.6 4.9.8 8.3 3.5 10.9 8.7 1.2 2.3 3 5.2 4.2 6.4l2 2.2-5.4 5.6c-6.4 6.7-12 10.5-15.3 10.5-1.3 0-4.3-1.6-6.6-3.5s-5-3.5-6-3.5c-2.7 0-7.8 4.7-10.1 9.2-2.5 4.8-2.7 4.7-10.7-7-10.5-15.3-20.2-36.5-28.1-61.4-9.8-30.7-9.7-30.1-4.8-32.1 5.7-2.4 15.2-15.7 18.6-26.1 1.2-3.4 2.7-10.2 3.4-15.3 1.3-8.6 3.5-14.3 5.7-14.3.6 0 .1 1.7-1 3.7zm-443.9 33.4c12.9 12.8 26.2 24.2 41.4 35.6 13.8 10.4 16.6 11.7 24.5 11.8 5.9 0 7.3-.4 15.5-4.9 19-10.5 23.2-11.8 29.7-9.6 14.3 5 12.4 27.8-5.2 63.5-7.5 15.1-22.9 39.4-32.7 51.5-6.8 8.4-21.3 22.6-31.8 31.3-7 5.8-19 12.5-20.2 11.4-.3-.4 1.7-9.1 4.5-19.4 9-32.9 19.2-75.4 19.2-79.9 0-1.7-.6-2.4-1.9-2.4-1 0-8.8 3.3-17.2 7.4-8.5 4.1-19.8 9.1-25.3 11.1-10.5 3.9-34.7 10.8-35.3 10.1-.3-.2 2.6-7.3 6.3-15.8 3.7-8.4 9.8-23.4 13.5-33.3 6.7-17.7 6.7-18 4.4-18.3-1.2-.2-7.1 1.7-12.9 4.2-19.3 8.3-35.9 11.3-55.6 10.2-6.3-.4-11.6-.9-11.8-1.1-.2-.2 3.1-4.4 7.4-9.2 21.9-24.4 39.3-49.6 51.6-74.6l4.3-8.9 7.9 8.9c4.4 4.8 13.2 14 19.7 20.4zm233.8 15.2c2.4 2.1 5.1 3.7 6.6 3.7 7.3 0 11.5 6.4 13.9 21.1 1.6 10.1 3.3 13.2 8.6 16.4 1.9 1.1 6 4.8 9.3 8.2 6 6.3 8.5 7.6 16.7 8.8 8.8 1.4 12 4.3 17.4 15.7 5.6 11.8 12 15.3 19.5 10.7 4.7-2.9 7.3-1.5 11 5.9 4.9 9.7 9.3 12.7 44 29.9l31.6 15.7 9.5.1c8.4 0 10.2-.3 15.5-2.8 7.6-3.5 15.1-10.8 22.5-21.8 6.9-10.2 10.6-12.5 14.3-9.1 3.1 2.9 7.5 4.4 11.1 3.7 9.2-1.9 23.8-17.4 38.2-40.7 8.7-14.1 13.5-19.3 18.8-20.3 6.3-1.3 6.8-1.7 10-8.8 4.7-10.2 9-13.3 11.9-8.6 1.7 2.8-.9 7.2-9.5 16.1-10.7 11.1-19.7 23.2-24.6 33.1-6.5 13-7.7 17.8-7.7 29.7 0 9.3.3 11.2 2.6 16 1.4 3 5.3 8.6 8.7 12.3 10.9 12.3 11.6 22.2 4 53-4 16-4.3 18.1-4.2 30.7 0 12.5.4 15.2 5.3 37.5 5.5 25.5 7.2 38.6 5.2 42.3-2.8 5.2-9.7 3.2-14.3-4.2-6.9-11-7.5-21-2.9-51.3 4.3-28.3 4-37.8-1.7-48.8-2.6-5.1-11-13-16.5-15.5-9.9-4.5-25.9-1.6-34.5 6.3-8.2 7.6-10 13.4-11.2 36.2-1.1 19.8-2.6 26.5-12.4 54.8-10.4 30-12 37.5-12.1 54.7 0 19 1.7 24.6 12 40.1 8 12 9 15.2 6 20.9-1.8 3.6-9 6.5-13.7 5.6-7.9-1.5-14.6-9-16.8-18.9-1.8-8.4-2.3-26.4-1.1-38.2.7-6.1 3.2-22 5.7-35.5 7.7-41.6 9.3-57.1 7.5-71.7-2-16.1-5.9-25.6-12.5-30.1-4.3-2.9-10.3-3.9-31.3-5.2-38.7-2.2-72-8.5-112.1-20.9-8.6-2.7-18-5.2-20.8-5.6-10.9-1.4-18.8 3.6-24.2 15.3-7.3 15.8-9 32-5.2 51.4 2.5 12.7 2.5 19.1.1 24.3-2.8 6.2-7.3 9.5-12.8 9.5-3.9 0-5-.5-7.8-3.5-4-4.1-5.6-10.5-4.4-16.9.4-2.5 3.3-9.4 6.3-15.4 8.5-17 9-19.6 8-39.7-1.4-30.5-1.1-42.1 1.3-50 2.9-9.2 7.2-15.8 13-19.8 13-9 17.2-15.6 18-28.9.7-11.2-1.2-18.3-9.3-34.4-7.4-14.8-7.8-17.3-2.5-18.6 3-.8 3.2-1.2 2.6-3.6-.3-1.5-.9-6.3-1.3-10.7-.8-9.3-2.7-13-7.2-14-4.7-1-6.6-4.4-7.3-12.9-.7-9-1.4-10.5-6.4-13.1-4.9-2.6-6.4-5.9-7-15.8l-.4-8.2 7.4 15c5.5 11.2 8.5 16 11.6 18.8zm65.5-5.3c2.2 6 9.4 14.3 19.4 22.2 5.5 4.4 7.2 5.3 10.9 5.3 2.3.1 4.5.3 4.7.6.6.5-1.9 10.9-3.6 15.1-1.2 3.2-2 3.4-5.3 1.2-4.4-2.8-6.7-1.6-9.3 5.1-5.3 13.5-7.4 17.5-9.4 17.5-1 0-2.3-1.1-3-2.4-.9-2.1-.6-3.2 2-7.3 3.8-6 5.2-11.1 5.2-18.9 0-5-.6-7.4-3.6-13.3-2.6-5.2-3.5-8.1-3.2-10.8.3-3.4 0-3.9-3.2-5.4-3.1-1.5-3.6-2.2-4.1-6.5-.3-2.7-.9-5.7-1.4-6.8-.6-1.4-.5-1.8.6-1.4.8.2 2.3 2.8 3.3 5.8zm-205.3 9.7l9.9 7.8-4.9.6c-4.9.7-8.5 2.3-18.7 8.8-5.9 3.7-10.6 4.5-11.2 1.9-.2-1 .7-4.2 2.1-7 2.6-5.7 11.3-19.8 12.3-19.8.3 0 5 3.5 10.5 7.7zm233.2 44.8c-.3 4.1-12.1 27.7-13.6 27.2-.6-.2-1.8-2-2.6-4-1.3-3.6-1.3-4 3.3-13.6 2.5-5.4 4.6-10.1 4.6-10.4 0-.3.5-1.8 1-3.2l1-2.7 3.3 2c2.5 1.5 3.2 2.5 3 4.7zm49.7 11.2c0 6.5 3.5 36.8 5 44.4 1.2 5.6 1.8 10.4 1.5 10.7-.6.7-10.2-3.7-11.5-5.4-.7-.7-.5-3.6.6-8.5.9-4.2 1.8-14.8 2.1-24.7.3-9.4 1-17.2 1.4-17.2.5 0 .9.3.9.7zM571.8 787c-.1 7.7-.7 14-1.2 14-.9 0-2.3-1.5-5.2-5.7-1.4-2-1.3-2.9 2.2-12.2 2-5.6 3.9-10.1 4.1-10.1.2 0 .3 6.3.1 14zm65.3 11.7c3.6 10.4 9.5 21.5 15.9 29.8 2.2 2.8 4 5.6 4 6.2 0 1.4-12 3.7-13.1 2.6-.7-.7-11.9-45.3-11.9-47.6 0-.4.4-.7.9-.7s2.4 4.3 4.2 9.7zM476 888.5c1.5 1.8.6 4.6-2.5 7.5-4.6 4.3-8 .2-4.5-5.5 2.3-3.7 4.9-4.5 7-2zm116.4 30.7c1.4 2.4-1.9 7-4.7 6.6-3-.4-3.4-4.1-.7-6.8 2.3-2.3 3.8-2.3 5.4.2zm38 1.4c4 1.6.3 8.4-4.6 8.4-2.4 0-2.8-.4-2.8-2.9 0-2 .8-3.5 2.2-4.5 2.5-1.8 3-1.9 5.2-1zm142.9 53.5c.7 2.6-.8 7.2-1.6 4.9-1.6-4.5-1.7-7-.3-7 .7 0 1.6.9 1.9 2.1zm-187.9 32.5c3.5 1.3.6 7.4-3.5 7.4-1.5 0-1.9-.7-1.9-3.3 0-4.6 1.4-5.6 5.4-4.1zm44.1 5.8c.8 2.4.6 3.1-1.4 4.7-2.9 2.4-4.3 2-5.5-1.4-.8-2.1-.6-3.1.9-4.7 2.4-2.7 4.7-2.2 6 1.4zm-165.7 110.9c.2 1.8-.2 2.9-1.3 3.4-2 .7-3.5-1.2-3.5-4.5 0-1.9.5-2.3 2.3-2 1.6.2 2.3 1.1 2.5 3.1zm290.6 12.9c-1.3 2.3-3.7 4.8-4.6 4.8-1.2 0-1-3.4.4-4.8 1.6-1.6 5.1-1.6 4.2 0zM585 1154.5c1.1 1.3 1 2.1-.7 4.3-2.8 3.7-5.1 4-5.9.6-.6-2.1-.2-3.1 1.6-4.6 2.7-2.2 3.4-2.2 5-.3zm40.6 5c.8 2.1-2.2 6.5-4.4 6.5s-4.2-1.9-4.2-4c0-.9.5-2.1 1.2-2.8 1.7-1.7 6.7-1.5 7.4.3zm-143.6 42c0 2.8-2.5 4.4-4.2 2.7-1.5-1.5.1-5.2 2.3-5.2 1.4 0 1.9.7 1.9 2.5zm256.8 9.7c.3 2.8-2 5.3-4.3 4.4-2-.7-1.9-3.5.2-5.8 2.1-2.4 3.7-1.8 4.1 1.4zM584 1239.5c0 3.2-2.5 5.1-4.5 3.5-2-1.7-1.9-4.7.2-5.8 2.4-1.4 4.3-.4 4.3 2.3zm38.4-.6c.8 1.3-.3 3.8-2 4.5-1.9.7-4.4-.3-4.4-1.8 0-2.8 5.1-4.9 6.4-2.7z"/><path d="M384 507.4c-5.5 1-6.5 1.5-6.4 3.1.1 1.1 2.2 13.5 4.7 27.5s4.7 27.1 4.9 29c.3 3.4.4 3.5 4.3 3.4 7.8-.1 11.5-1.3 11.5-3.6 0-1.8-.4-2-3.5-1.5-2.7.4-3.7.2-4.4-1.1-1.3-2.5-9.4-50-8.8-51.6.3-.7 2-1.6 3.7-1.9 2.2-.5 3.1-1.2 2.8-2.4-.4-2.1-1.1-2.2-8.8-.9zM407.8 512.7c-4.3.2-7.8.8-7.8 1.3 0 .6 1.1 1 2.5 1s2.8.6 3 1.2c.2.7.9 8.9 1.5 18.2.9 15.3.9 17.1-.6 18.8-1 1-1.5 2-1.3 2.2.6.4 27.9-1.2 29.6-1.8.8-.4 1.3-2.3 1.3-5.6 0-5.9-1.3-6.5-2.6-1.3-1.1 4.4-2.7 5.1-12.5 5.2l-6.7.1-.5-4.3c-.4-2.3-.9-10.6-1.3-18.3l-.6-14.2 3.1-.7c1.7-.4 2.9-1.1 2.5-1.6-.3-.5-.8-.8-1.2-.7-.4.1-4.2.4-8.4.5zM447.2 519.2c.2 1.5 1 2.3 2.3 2.3 1.3 0 2.1-.8 2.3-2.3.3-1.8-.1-2.2-2.3-2.2-2.2 0-2.6.4-2.3 2.2zM446 527.5l-4.5 1.2 2.9 1.2c2.9 1.2 2.9 1.2 2.2 7.9-.3 3.7-.6 7.9-.6 9.4 0 2.1-.5 2.8-2 2.8-1.1 0-2 .4-2 1s2.9 1 6.8.9c5.8 0 6.4-.2 4.3-1.2-2.3-1.1-2.4-1.3-1.9-12.9.3-6.5.2-11.8-.1-11.7-.3.1-2.6.7-5.1 1.4zM469.6 531.1c-4.7 3.7-3.3 8 3.6 11.5 2.7 1.4 5.1 3.1 5.5 4 .8 2.2-1.6 4.4-4.9 4.4-4.3 0-6.8-1.7-6.8-4.5 0-1.4-.4-2.5-1-2.5-.5 0-1 .6-1 1.2 0 .7-.3 2.3-.6 3.4-.5 1.7.1 2.4 2.7 3.3 6.8 2.3 14.3.3 15.5-4.3.9-3.6-.9-5.9-6.6-8.6-4.4-2.1-5.1-2.7-4.8-4.9.2-2 1-2.7 3-2.9 3.6-.4 6.8 1.4 6.8 3.8 0 2.9 1.7 2.4 2.3-.6.4-2.2 0-2.8-2.9-4-4.6-1.9-7.7-1.8-10.8.7z"/><path d="M516 529.9c0 .6.6 1.1 1.4 1.1.8 0 1.6.5 1.8 1.1.2.7-.9 7.7-2.4 15.8-1.5 8-2.7 14.7-2.8 14.8 0 .2-.9.3-2 .3s-2 .4-2 1c0 1.2 6.9 2.5 8 1.4.5-.5 1.8-5.8 2.9-11.8 1.1-6 2.2-11.1 2.5-11.3 1-1.1 28.5 15.3 50.3 29.9 15.2 10.2 20.4 14.3 15.7 12.4-29.2-12-49.8-16.5-78.9-17.3-20.1-.5-33.9.7-53.5 4.7-9.9 2.1-22.4 5.9-21.7 6.6.2.3 4.4-.5 9.2-1.6 15.5-3.6 38.6-7 52-7.6 32.5-1.6 63.6 6.5 123.9 32.3 30.9 13.2 36.8 15.3 43.7 15.3 9.8 0 5.3-2.8-17.2-10.5-26.1-9-37.5-15.1-73.1-38.6-12.8-8.5-27-16.9-35.5-21.1l-14.3-7v-4.3c0-4.8-.1-5-4.7-6-2.1-.4-3.3-.3-3.3.4z"/><path d="M498 532.6c-.6 1.4-1 3-1 3.5s-1 .9-2.2.9c-2 .1-2.1.2-.6 1 1.6 1 1.6 1.4-.3 6.7-2.7 7.9-2.5 11.1.6 11.9 1.4.3 3.3.3 4.2-.1 1.6-.6 1.6-.8-.4-1.6-2.7-1-2.8-2.5-.6-9.6 1.4-4.6 1.9-5.2 3.7-4.7 3.3 1 3.8 1 4.3-.2.2-.6-1-1.4-2.7-1.7l-3-.7 1.5-3.6c1.9-4.6 1.9-4.4-.5-4.4-1.3 0-2.4 1-3 2.6zM994.3 543c-4.8 2.9-6.6 7.2-6 14 .5 6 5.1 17.3 9.4 22.8 5.1 6.8 13.3 7.8 19.2 2.3 5.5-5.2 5.1-14.3-1.3-27.7-5.8-12.2-13.4-16.3-21.3-11.4zm9.7 3.6c4.3 3.7 12 19.8 12.7 26.7.5 4.7.3 5.6-1.8 7.5-5.3 5.1-11.2.7-18-13.3-5.1-10.6-6.2-18.2-3-21.4 2.8-2.8 6.3-2.7 10.1.5zM349.7 545.5c-4.5 1.5-7 3.6-9.8 8.1-1.6 2.6-1.8 3.7-.9 5.4 1.6 3.1 3 2.4 3-1.5 0-3.8 2.3-6 7.7-7.1 2.5-.5 3.7 0 6.3 2.3l3.2 2.9-5.6 4.3c-6.1 4.7-9.6 9.5-9.6 13.2 0 3.6 3.6 8.8 6.5 9.5 5 1.2 8.7-.7 11.9-6.4 3.3-5.7 4.1-6.2 5.1-3.2.8 2.5 1.4 2.5 7-.4 4.5-2.2 4.9-4.5.6-3.1-1.6.5-2.8-1-7.1-9.4-7.5-14.3-11.1-17.2-18.3-14.6zm12.8 17.4c2 4.5 1.6 7.5-1.6 11.4-4 5.2-10.9 2.5-10.9-4.1 0-2.9.8-4.3 4.6-7.9 2.6-2.3 5-4.1 5.3-3.8.4.2 1.6 2.2 2.6 4.4zM963.3 560c-5 3-6.5 5.9-5.2 10.3 1.1 4 3.2 5.1 2.8 1.4-.4-3.5.9-6.3 3.7-7.5 3.7-1.7 6-1.5 8.9 1.1 4.4 3.7 4.3 8.3-.3 22.5-2.2 6.7-3.7 12.4-3.3 12.6.6.4 22-9 23.4-10.3.4-.4.1-1.5-.6-2.5-1.2-1.6-1.8-1.4-10.1 2.3-4.8 2.3-8.6 3.5-8.3 2.8 2.3-6.1 6.7-21.2 6.7-23.3 0-9.1-9.7-14.3-17.7-9.4zM309.2 570.1c-5.1 2.6-6.9 4.9-7.8 10.6-.8 5-1.5 5.3-4.1 1.8l-1.8-2.5-4.2 5.7c-4.5 6.1-4.8 8.3-.6 5.6 2.6-1.7 2.6-1.7 6.2 2.3 2 2.3 6.2 7.6 9.5 11.8l5.9 7.7-2.3 2.4c-1.2 1.3-1.9 2.8-1.6 3.4.5.8 10.8-6.4 16.5-11.5.1-.1 0-.7-.4-1.3-.4-.7-1.4-.5-3 .5-1.3.9-2.9 1.4-3.4 1.2-.5-.1-4.1-4.3-8-9.2-6.1-7.6-7.1-9.4-7.1-12.8 0-7.9 8.9-12.8 15.2-8.4 1 .7 5.2 5.6 9.4 10.9 4.1 5.4 8.1 9.7 8.7 9.7 1.6-.1 10.4-7.7 9.6-8.4-.3-.3-1.9 0-3.7.6-1.7.7-3.3 1-3.5.8-.2-.3-3.8-4.8-8.1-10.1-7.1-9-11.9-12.9-15.8-12.9-.7 0-3.2 1-5.6 2.1zM941.4 573.4c-4.3 1.9-6.4 5.3-6.4 10.3 0 4.8 3.2 14.1 6.7 19.5 4.5 6.7 11.9 7.4 16.9 1.5 2.4-2.8 2.6-3.7 2.1-8.8-.6-7.3-5.1-17.1-9.4-20.9-3.7-3.3-5.3-3.6-9.9-1.6zm6.7 3.7c3 3 8.8 16 9.5 21.1 1.1 8.3-5.5 11.1-10.8 4.7-3.2-3.8-8.8-17.5-8.8-21.5 0-5.8 6-8.4 10.1-4.3zM914.4 585.2c-4.6 1.2-7.3 4.6-6.7 8.5.7 4.2 2.1 4.2 2.5.1.6-6.3 9.7-6.6 11.8-.4 1.2 3.8.1 8-5 18.8-1.6 3.4-2.8 6.4-2.5 6.6.2.2 4.8-1.1 10.3-2.9 8.4-2.7 9.8-3.5 9.4-5.1-.6-2.3-.7-2.3-8.6.7-3.6 1.4-6.6 2.1-6.6 1.6 0-.4 1.6-4.2 3.5-8.4 4-8.6 4.4-13.3 1.5-16.9-2.3-3-5.2-3.8-9.6-2.6zM256.6 588.2c-1.3 1.9-1.4 2.5-.2 4.3 2 3.1 6.3 3.2 7.7.2 2.5-5.4-4.1-9.4-7.5-4.5zM218.7 610.7c-12 11.9-18.4 19-17.8 19.6.6.6 2.1 0 4.1-1.7 1.7-1.4 3.7-2.6 4.3-2.6 1.5 0 44.7 43.2 44.7 44.7 0 .6-1.1 2.5-2.5 4.1-2.5 3-3.2 5.2-1.7 5.2.4 0 5.7-4.8 11.6-10.6 7.4-7.3 10.5-10.9 9.8-11.6-.8-.8-2.1-.3-4.2 1.6-1.7 1.4-3.9 2.6-4.7 2.6-.9 0-6.2-4.5-11.7-10l-10.1-9.9 7.5-7.6c4.2-4.2 8.3-7.5 9.4-7.5 1.1 0 3 1 4.2 2.1 1.5 1.4 2.9 1.9 4 1.5 1.5-.6.2-2.3-6.9-9.4-5.9-5.9-9-8.3-9.7-7.6-.7.7-.2 2.1 1.5 4.3 2.2 3.1 2.3 3.6 1 6-1.2 2.4-4.3 5.5-11.8 12.1l-2.8 2.4-9.4-9.4c-5.2-5.2-9.5-10-9.5-10.6 0-1.9 15.7-16.3 18.5-17 2.1-.5 3.3 0 5.6 2.2 2.1 2 3.2 2.5 4 1.7.8-.8-.2-2.7-3.6-6.9-2.6-3.3-4.8-6-4.9-6.1-.1-.1-8.6 8.2-18.9 18.4zM267.2 601.7c-1.7 2.7-3.7 5.6-4.4 6.5-1.8 2.5-.3 3.3 2.6 1.4 1.4-.9 3-1.6 3.5-1.6 1.2 0 21.1 23.6 21.1 25 0 .6-.9 1.8-2 2.7-1.1 1-2 2.3-2 3 0 2.2 2.5 1.4 5.6-1.8 1.6-1.7 5.3-4.9 8.2-7.1 3.5-2.7 5-4.4 4.3-5.1-.7-.7-1.8-.4-3.5.7-1.4.9-3 1.6-3.5 1.6s-6.6-6.8-13.6-15c-6.9-8.3-12.7-15-12.9-15-.2 0-1.7 2.1-3.4 4.7zM857.5 602.2c-3.2 1.8-4.4 4-4.5 8 0 5 .8 6.6 4.1 8.4 3.5 1.8 7.4 1 9.4-1.9 1.5-2.1 1.5-2 1.5 1.1 0 4.3-2.2 8.4-6.2 11.6-4.2 3.3-2.4 3.4 2.4.1 4.2-2.8 7.8-9.6 7.8-14.6-.1-4.4-3.2-11.4-5.8-12.8-2.6-1.4-6.2-1.4-8.7.1zm7 2.3c2.9 2.9 3.3 7.9.8 10.7-2.2 2.4-3.9 2.3-6.8-.7-3.3-3.2-3.4-8.7-.3-10.9 3-2.1 3.5-2 6.3.9zM838.2 608.4c-2.4 1.2-4 2.5-3.6 2.9.4.4 1.8 0 3-.8 2.1-1.4 2.3-1.3 2.8 1.3.3 1.5.8 6.4 1.2 10.9.6 8 .5 8.3-1.7 9.3-1.9.8-1.4.9 2.5.5 2.7-.3 5.6-.5 6.5-.6.9-.1.5-.4-.9-.8-2.4-.7-2.5-1.2-3.7-12.5-.6-6.5-1.3-11.9-1.5-12.1-.1-.2-2.2.7-4.6 1.9zM892.6 609.4c-4.9 1.1-9.1 2.7-9.4 3.4-.3 1 .4 1.2 3.2.7 8.3-1.6 16.7-4.1 16.7-5 .1-1.5-1-1.4-10.5.9zM818 612.2c-1.6 1.7-2 3.5-2 8.7 0 10.7 4.5 15.6 11 12.1 5-2.7 4.8-18.8-.4-22.2-2.2-1.5-6.6-.8-8.6 1.4zm7.6 1.3c2.1 3.2 3.1 14.4 1.5 17.4-1.7 3-5.2 2.4-7-1.3-1.9-4.1-2-16.4-.1-17.6 2.4-1.6 3.8-1.2 5.6 1.5zM800.8 613.2c-1.8 1.5-3.1 4.8-1.9 4.8.5 0 1.1-.7 1.5-1.5.7-1.9 5-2 6.5-.1 1.8 2.2.3 6.2-4.5 11.5-2.4 2.7-4.4 5.2-4.4 5.5 0 .8 6.9.8 10.9 0 4.8-1 3.7-2.4-2-2.4h-5.1l4.1-5.1c4.7-5.9 5.5-11.1 2.2-12.9-2.6-1.4-5.4-1.3-7.3.2zM647.9 716.9c-1.6 1.6-2.9 3.6-2.9 4.5 0 2.7 2.1 2.8 4.5.2 3.9-4.2 10.1-3.2 12.9 2.2.8 1.6 2 6.5 2.6 11 1.9 14.1 5.4 21.3 11.8 25 2.2 1.2 4.8 2.2 5.9 2.2 1 0 2.9.7 4.2 1.6 2.7 1.9 4.6.7 3.6-2.4-.4-1.4-2.2-2.6-5.8-3.6-9.1-2.7-12.8-8.3-15.2-23.2-2.4-15-6.5-20.4-15-20.4-2.7 0-4.4.8-6.6 2.9zM699.5 777c-1 1.7 1.4 4.9 5.3 6.9 4.2 2.1 6.3 4.3 8.3 8.5 1.8 3.8 3.3 3.8 3.7.1.4-4.3-2.9-8.8-9.3-12.9-5.9-3.8-7-4.2-8-2.6zM477.9 767.2c-.9 2.1-.6 3.6 2 9.5 4.2 9.7 5.4 16.2 4.9 26.3-.3 4.7-.2 9.3.1 10.3.6 1.7.8 1.7 2.5-.2s1.8-3.2 1.3-13.3c-.6-9.9-2.3-20.7-4.3-27.1-.6-1.7-.3-1.9 2.2-1.3 5.5 1.4 9.6 4.9 19.1 16.3 13 15.7 17.6 18.5 33 20.2 10.6 1.1 14.9 3.3 25.8 13.2 4.9 4.4 10.6 8.9 12.7 10 2 1 7.9 2.5 13.1 3.3 5.2.8 11 2.2 12.8 3.1 2.9 1.5 9.8 6.9 17.7 13.9 4.3 3.8 2.4-.8-2.7-6.5-6.3-7.1-14.2-11.3-25-13.3-11.6-2.2-15.2-4.1-26.4-14-12.2-10.7-15.7-12.5-27.5-14.3-14.2-2.1-18.2-4.6-32.4-20.4-10-11-17.7-16.8-23.6-17.6-3.7-.5-4.3-.3-5.3 1.9zM723.6 810.9c-7.6 7.8-11.5 9.1-32.4 11.1-.2 0-4.9 4.7-10.5 10.4-9.2 9.3-11.2 10.8-20.1 15.1-8.9 4.2-9.7 4.8-8.3 6.2 1.4 1.4 2 1.4 5.9-.1 8.7-3.5 18.1-9.9 25.7-17.7l7.8-7.9 8.4-1.4c10.1-1.6 14.9-3.5 20.4-8.1 3.8-3 8.3-10 7.3-11.1-.2-.2-2.1 1.4-4.2 3.5z"/><path d="M539.3 822.6c-.9 2.3 8 10.8 15.7 15.1 3.6 2 7.7 4.7 9.2 6.1 3 2.8 4.3 2.5 3.3-.7-.6-2-5.4-5.5-14.5-10.4-3-1.6-6.3-4.5-8.2-7.2-3.4-4.8-4.5-5.4-5.5-2.9zM503.8 829.6c-.5.4-.8 3-.8 5.8 0 5.6 3.6 13.1 8.4 17.5 3.4 3.2 3.5.7.1-4-4.4-6.1-5.6-8.9-6.3-14.5-.4-3.1-1-5.2-1.4-4.8zM523 830.4c0 .8 3.9 4.2 8.8 7.4 4.8 3.3 11 8 13.8 10.6 5.3 4.7 6.4 5.3 6.4 3.6 0-2.2-8.5-10.1-17-15.9-10.4-7.2-12-7.9-12-5.7zM731.5 833.4c-7.3 2.3-10.7 4.7-21.9 15.5-16.7 16-24.5 20.2-41.9 22.4-8 1-8.8 1.3-8.5 3.2.4 2.9 5.1 3.1 15.1.6 14.5-3.6 23-8.6 36.6-21.5 15.7-14.8 22.5-18.1 32.4-15.3 2.1.6 2.9.4 3.4-.9 1.4-3.8-7.9-6.3-15.2-4z"/><path d="M486.7 836.1c-4.3 6 6.5 21.9 19.3 28.4 14.4 7.3 31.5 7.2 62-.2 18.3-4.4 22.9-4.2 33.9 1.5 9.4 4.9 18.2 8 26.1 9.1 6.8.9 7.8.1 2.3-1.8-2.1-.8-11.2-4.5-20.3-8.4-10.6-4.5-18.3-7.2-21.5-7.4-3.4-.3-10.2.7-21 3-29 6.4-40.7 7-54 2.6-13.8-4.6-23.4-14.9-23.5-25.2 0-4.1-1.2-4.7-3.3-1.6z"/><path d="M516.6 837.9c-.3.5 1.8 2.6 4.7 4.7 2.8 2.2 6.2 5.8 7.5 8.2 2.5 4.4 4.2 5.4 4.2 2.4 0-2.8-5-9.7-9.5-13.1-4.1-3.1-6-3.7-6.9-2.2zM394.6 625.6c-5.2 5.2 2.6 15.2 11 14.2 2.5-.3 2.9-.8 3.2-3.6.4-4-1.7-7.6-6.1-10.2-3.9-2.4-6-2.5-8.1-.4zm7.8 5.7c3 2.4 2.9 5.1-.2 4-2.5-.9-5.2-3.4-5.2-5 0-2 2.1-1.6 5.4 1zM403.8 642.1c-2.7 1.5-2.2 3.7 2.3 11.4 2.3 3.9 5.7 10.3 7.6 14.2 3.6 7.8 5.3 9.3 10.5 9.3 4.8 0 5.8-1.5 5.8-9 0-6.4-.1-6.6-4-10-3.1-2.7-4.4-4.7-5.3-8.5-1-4.2-1.8-5.3-4.7-6.7-3.9-2-9.5-2.3-12.2-.7zm10.6 5.8c.9 1 1.9 4.1 2.2 6.7.2 2.7.8 5 1.4 5.2.5.2 2.4 1.3 4.2 2.5 2.6 1.8 3.4 3.1 3.6 6 .2 2.7-.1 3.9-1.3 4.3-2.3.9-3.9-.9-8-9.4-2-4.2-5.1-9.1-6.7-10.9-1.9-1.9-2.8-3.8-2.5-4.7.9-2.2 5-2 7.1.3zM504.7 643.1c-1.7 2.4-1 6 2.3 11.6 1.1 1.9 2 4.4 2 5.6 0 3.3 2.8 6.7 5.4 6.7 7.3 0 7.6-14.4.4-22.5-3.6-4.1-7.8-4.7-10.1-1.4zm7.7 3.8c2 2.2 4.2 12 3.3 14.5-1 2.4-2.7 1-2.7-2.2 0-1.5-1.2-4.7-2.6-7.1-3.3-6-2-9.6 2-5.2zM390.1 648.6c-2.6 3.4-2.6 4.5.1 7.9 2.4 3 7.4 5.5 11.3 5.5 2.3 0 2.5-.3 2.5-4.6 0-3.9-.5-5.1-3.4-8-4.2-4.2-7.6-4.4-10.5-.8zm8.7 5.6c.4 3.5-2 3.8-4.2.6-1.8-2.6-1.2-4 1.7-3.6 1.6.2 2.3 1.1 2.5 3zM684.3 652.5c-2.8 2-5.7 9.7-4.9 12.9.9 3.6 4.2 5.2 7.3 3.5 3.3-1.7 8.3-9.6 8.3-13 0-4.6-6.2-6.5-10.7-3.4zm6.4 4.1c.1.5-1.1 2.5-2.8 4.4l-3.1 3.5.6-3.9c.5-3.8 2.1-5.9 4-5.3.6.2 1.1.7 1.3 1.3zM834.9 675.3c-1.9 1.2-4.3 3.9-5.3 5.8-2 3.6-2 3.6.3 6.7 2.1 3 2.1 3.3.6 6.2-.8 1.6-3.4 4.2-5.6 5.6-4.3 2.7-7.8 8.8-7.9 13.6 0 2.5.3 2.8 3.6 2.8 2.9 0 4.4-.8 7.3-3.8 2-2.2 5.1-4.5 6.9-5.2 4.1-1.6 8.2-6.4 8.2-9.6 0-1.4-1.2-3.7-2.6-5.1l-2.6-2.6 3.5-3.1c5.6-4.9 7.1-9.1 4.1-12-2.2-2.3-6.5-2-10.5.7zm7.1 3.2c0 2.2-4.9 6.5-7.2 6.5-3 0-1.8-3.1 2.1-5.7 4-2.7 5.1-2.9 5.1-.8zm-4.2 19.2c-.2 2-1.4 3.2-4.8 5-2.5 1.3-6 4-7.7 6.1-2.8 3.1-3.3 3.4-3.3 1.6 0-2.5 2.8-5.6 8-8.9 2.1-1.3 4.1-3.3 4.5-4.5 1-3.1 3.7-2.6 3.3.7zM509.3 675.2c-3.8 4.9-.1 16.8 4.4 14 3.1-2.1 4.6-11.5 2.1-14-1.6-1.6-5.3-1.5-6.5 0zm4.5 6c-.4 5.1-3.1 6.1-3.6 1.5-.4-3.7.4-5.7 2.3-5.7 1.3 0 1.5.9 1.3 4.2zM807.3 706.2c-3.9 4.4-10.3 15.5-10.3 18.1 0 6.1 9.8 4 14.5-3.1 3.6-5.4 4.1-9.9 1.6-14-2.4-3.8-3.1-3.9-5.8-1zm1.8 9.6c-2 3.9-6.7 8.9-7.6 8-.3-.2 1.3-3.3 3.5-6.7 4.3-6.6 7.3-7.6 4.1-1.3z"/></svg> \ No newline at end of file +<svg version="1" xmlns="http://www.w3.org/2000/svg" width="933.333" height="933.333" viewBox="0 0 700.000000 700.000000"><path d="M273.1 50.5c-1.6 1.4-3.7 4.7-4.7 7.3-1 2.6-2.3 5.6-3 6.7-.7 1.1-2.4 6.3-3.9 11.5s-3.8 12.1-5.1 15.2c-1.3 3.2-3.1 8.9-4 12.6-.8 3.7-3.5 12.8-5.9 20.2-2.4 7.4-5.1 18.3-5.9 24.1-2 13.5-2.1 29-.2 32.7 4.2 8 16.9 15 53.6 29.7 28.1 11.2 43.9 16.5 49.1 16.5 1.4 0 3.9.4 5.5 1 4.8 1.6 39.4 8.1 46.4 8.7 3.6.3 13.3 0 21.5-.6 8.3-.7 25.6-1.6 38.5-2.1 12.9-.6 26.9-1.7 31-2.5 4.1-.8 9-1.5 10.8-1.5 3.3 0 15.2-4.7 15.2-6.1 0-.4.8-1.5 1.7-2.5 2.5-2.5 3.2-11.3 3.2-41.9 0-32.1-1.2-58.2-2.7-61.1-1.5-2.7-6.5-4.8-9.7-4-5.4 1.4-5.5 2.4-4.8 50.7.7 50.8 1.2 47.9-7.8 47.9-2.7 0-6.6.7-8.6 1.5-4.6 1.9-25.8 3.3-63.3 4.1-27.4.6-30.4.5-42-1.5-6.9-1.2-15-2.8-18-3.6-3-.7-7.7-1.6-10.5-2-10.7-1.4-33.9-8.7-36.5-11.5-.3-.3-5.9-2.4-12.5-4.7-18.7-6.5-27.1-9.8-32.1-12.9-2.5-1.6-6.1-3.7-7.9-4.6-5.1-2.7-6-6.2-4.7-18.1 1.2-11 4.6-26.9 8.2-38.2 6.5-20.9 8.3-26 9.5-27.9.7-1.2 2.5-6.2 4-11.1 1.4-5 3.3-10.9 4.1-13.2l1.6-4.3 3.1 1.6c1.8.9 4.6 2.7 6.3 4 1.7 1.3 3.4 2.1 3.6 1.9.4-.4 9 4 21.3 10.8 1.7.9 8.2 3.7 14.5 6.1 6.3 2.4 12.4 4.7 13.5 5.1 1.1.5 4.9 1.6 8.5 2.5 3.6 1 12.2 3.4 19.2 5.3 7 2 16.9 4.1 22 4.6 5.1.6 14 2 19.8 3.1 16.1 3.2 38.2 4.3 64.5 3.5 21.8-.8 38.2-1.4 39.7-1.5.3 0 2.1-1.2 3.8-2.7 2.8-2.3 3.1-3.1 2.6-6.1-.3-1.9-1.3-4.5-2.2-5.9l-1.6-2.6-13.7.8c-7.5.4-28.8 1-47.4 1.3-27.4.6-34 .4-35.1-.6-1.8-1.8-6.9-2.9-22.6-4.7-7.3-.8-14-2-15-2.5-1-.6-3.4-1-5.4-1-4.7 0-23.6-5.8-41-12.6-13.3-5.3-43.3-20-46.7-23-.8-.8-2.4-1.4-3.5-1.4-1-.1-3-.7-4.4-1.5-4.1-2.4-10.4-1.9-13.9 1z"/><path d="M382.5 117.9c-4.8 1.2-7.5 3-8.5 5.7-2.7 7 1.2 24.6 6.1 27.7 2.7 1.7 6.7-.1 8.6-3.7 1-1.9 4.1-5.2 7-7.5 4-3.1 5.5-5.1 6.3-8.2 2.6-9.6-.7-14.1-10.7-14.5-3.2-.2-7.1.1-8.8.5z"/><path d="M314.3 135.1c-4.3 1.6-6.9 11.1-4.3 15.9 1 1.8 6.8 4 11.4 4.2 1.1.1 2.6.5 3.1.9 2.6 1.9 49.8 9.9 58.6 9.9 2.9 0 5 .7 7.1 2.5 3.9 3.3 15 6.7 22.3 6.8 5.9 0 14.5-3 14.5-5.2 0-.5.9-1.1 1.9-1.4 2.2-.5 8.7-9.1 9.6-12.6.7-2.7-3.5-8.7-7.4-10.7-2.3-1.2-15.3.5-18.8 2.5-1 .6-5.4 2.5-9.8 4.3-7.8 3.2-8.3 3.3-22.5 3.2-22-.1-41.9-5.3-51-13.1-7.8-6.6-11.4-8.4-14.7-7.2zM375.2 172.9c-.7.4-2.5 3.2-3.8 6.2-2.8 6-2.6 8 1.2 14.6 2 3.4 3.4 4.5 8.1 6.3 4.8 1.8 6.1 1.9 8 .9 3.9-2.1 6.6-7.5 6-11.6-1.5-9.3-14.1-19.9-19.5-16.4zM94.5 186.2c-4.1 1.4-7.4 4-19.5 15.3-11.4 10.5-26.9 23.3-34 28.2-7.5 5-11.2 6.6-17.3 7.4-7.2.9-10.7 3.7-10.7 8.6 0 3.6 1.2 6.6 3.7 9.5.8.9 4.1 8.3 7.4 16.3 5.8 14.6 12.7 28.5 19.9 40 2 3.3 5.2 8.7 7.1 12 4.2 7.4 46.3 50 55.6 56.3 3.4 2.3 6.4 5 6.8 6.1.3 1 1.3 2.1 2.3 2.3.9.3 8.1 5.1 16 10.7 7.8 5.6 17 11.8 20.5 13.8 3.4 2 8 4.7 10.2 5.9 6 3.6 30.3 14.4 32.2 14.4.5 0 2.7.9 4.9 1.9 6.3 3 16.5 5.3 20.6 4.7 3-.5 4.2-.2 5.8 1.4 3 3 8.5 2.7 11.7-.8 1.4-1.5 5.7-8.6 9.5-15.7 9.6-17.9 25.4-46.7 29.9-54.5 5.1-9 16.3-31.5 19.2-38.8 1.4-3.3 3.7-7.6 5.1-9.4 1.3-1.8 3-5.6 3.6-8.3.6-2.8 1.6-6.4 2.2-8.1.9-2.4.7-3.7-.6-6.6-1.4-2.9-3.1-4.2-9.4-7.1-12-5.6-17-7.3-27.7-9.7-9.7-2-17.5-4-30-7.4-5.5-1.6-15.2-5.4-20-7.9-3.9-2.1-7.1-3.6-12.6-6.2-3.1-1.4-7-3.5-8.5-4.8-1.6-1.2-5.2-3.1-8.1-4.3-9.3-3.8-32.9-18.2-40.9-25.1-2.8-2.3-5.6-4.3-6.1-4.3-.6 0-2.8-1.9-4.9-4.3-2.1-2.3-3.9-4.1-4.1-4-.1.1-1.6-1.1-3.4-2.8-1.7-1.6-3.5-2.9-3.9-2.9-.5 0-1.2-.9-1.5-2-.3-1.1-1.1-2-1.6-2-1.2 0-15.3-11.4-17.6-14.3-1.7-2.1-6.5-4.8-8.1-4.6-.4 0-2 .5-3.7 1.1zm7 20.8c.4.6 2.3 2.1 4.3 3.4 2.1 1.3 8.7 7 14.7 12.5 6.1 5.6 12.8 11.4 15 12.9 2.2 1.4 5.9 4.2 8.2 6.2 7.2 6.1 9.8 7.8 18.9 12.6 4.9 2.6 10 5.5 11.4 6.4 4.2 2.9 44 22.7 51 25.3 9.9 3.8 29.7 9.5 37.3 10.7 7.5 1.2 23.5 6.5 25.5 8.4 1.1 1.1.8 2.8-2 9.7-5.9 15.1-30.3 63-34.5 68.1-.8.9-1.6 2.2-1.7 3-.3 1.6-3.3 6.8-6.3 11.2-1.2 1.7-4 6.8-6.2 11.4-2.2 4.5-4.9 9.5-6 11-1.1 1.6-2.3 4-2.6 5.5-.8 3-2.6 3.5-4.5 1.2-.7-.8-1.9-1.5-2.6-1.5-.8 0-2.2-.4-3.2-.9-.9-.5-5.7-2.1-10.7-3.6-8.7-2.7-17.4-6.5-35.5-15.4-13.4-6.7-17.3-9.1-27.7-17.1-5.4-4.1-10-7.5-10.3-7.5-2.8-.1-26.3-19.9-43.9-37-14.3-13.9-23-24.3-27.2-32.7-1.6-3.2-3.6-6.1-4.3-6.4-.7-.3-2.2-2.4-3.2-4.7-1-2.3-3.1-6.1-4.7-8.5-4-6-14.7-30.2-14.7-33.3 0-1.8.9-3.2 2.5-4.3 1.4-.9 2.3-2.1 2-2.6-.3-.5 1.6-2.3 4.2-4.1 8-5.5 28-21.6 38.2-30.8 12.3-11.2 13.9-12.3 16.2-11 1 .5 2.1 1.4 2.4 1.9z"/><path d="M134.6 275.6c-.9.8-1.9 2.7-2.2 4.2-.3 1.5-.8 3.3-1 4-.2.7.5 3.1 1.6 5.2 1.1 2.2 2 4.4 2 5 0 .5 1.9 3.8 4.3 7.4 2.3 3.5 5.9 9 8 12.2 3.9 6.2 7.7 11.5 16.8 23.5 4.8 6.3 6.8 8 12.9 11.2 6.1 3.1 8.2 3.7 13.5 3.7 4.4 0 6.6-.5 7.5-1.5 1.5-1.8 1.8-9.3.7-15.3-.9-4.1-.8-4.2 1.7-4.2 4.1 0 9.3-2.5 11.2-5.4 2.1-3.3 1.5-11.4-1.4-17.8-3.1-6.9-8.2-11.2-19.9-16.8-9.1-4.4-12.4-5.4-20.8-6.6-11.9-1.7-25.2-5.6-28.5-8.4-2.8-2.4-4.3-2.5-6.4-.4zM572.3 225.7c-1.8 2.1-7 8-11.5 13.1-8.5 9.7-15.8 15.6-24.2 19.4-6.9 3.2-22.4 11.1-39.6 20.3-23.9 12.8-29.1 15.5-30.5 15.5-.7 0-3.5 1.1-6.2 2.4-5.6 2.8-39.6 12.9-52.8 15.7-4.9 1-12.2 1.9-16.1 1.9-5.8 0-7.7.4-9.8 2.1-2.1 1.6-2.4 2.4-1.5 3 1 .6.9 1.2-.5 2.8-2 2.2-2 2.4-4.5 44.1-1.5 23.3-3 37.9-6.1 58.5-.6 3.8-2.6 13.3-4.6 21-2.6 10.4-4.1 14.4-5.7 15.7-1.7 1.3-1.9 1.9-.9 3 .9 1.1.9 1.7 0 2.6-1.9 1.9 5.7 8.6 8.5 7.7 6.2-1.9 9.7-2.5 14.7-2.5 5.5 0 12.8-1.8 18.5-4.4 1.7-.8 7.5-2.1 13-2.9 27.4-4.3 52-16.4 126.1-62.1 5.5-3.4 12.8-9.4 23.8-19.6 3.9-3.6 12.4-11.2 18.8-16.8 6.4-5.6 14.9-13.8 18.9-18.1 6-6.7 7.8-8.1 11.2-8.6 2.2-.4 5.1-1.8 6.5-3.2 2.2-2.2 2.3-3 1.8-7.7-.6-4.1-.4-5.5.8-6.6 1.8-1.4 2.1-5.3.5-8.1-.6-1.1-3-2.9-5.5-3.9-3.4-1.5-4.4-2.5-4.4-4.2-.1-7.1-2.2-21.5-3.5-23.5-.9-1.3-1.1-2.3-.5-2.3.5 0 1-.8 1-1.8 0-2.2-6.5-15.8-13-27.2-2.7-4.7-5.6-11.2-6.6-14.5-.9-3.3-3-7.9-4.7-10.3-3.9-5.5-6.8-5.6-11.4-.5zm5.8 28.3c.5.3 3 4.9 5.7 10.1 5 9.8 6.4 15.1 9.7 35.9.8 5.2 2.5 12.3 3.7 15.7l2.1 6.1-3.4 5.4c-4.2 6.5-14.5 19-16.7 20.2-2 1.1-5.2 3.9-22.9 19.8-20.1 18.3-30.5 25.8-52.3 38.1-6.3 3.5-15.9 9-21.2 12.1-5.3 3.1-9.9 5.6-10.3 5.6-.4 0-2.5 1.3-4.8 2.8-6.6 4.5-25.8 14-30.4 15.2-2.4.6-4.3 1.4-4.3 1.9 0 .4-1.4.8-3 .9-1.7 0-7 1.2-11.8 2.5-4.8 1.3-11.2 3-14.2 3.7-3 .7-7.1 1.9-9 2.7-1.9.8-6.4 1.6-9.8 1.8-5.7.4-6.3.2-5.7-1.3 1.5-3.6 6.4-24.6 7-30 .4-3.1 1.3-10 2.1-15.2.8-5.2 1.4-13.2 1.4-17.7s.6-13.3 1.4-19.5c1.2-9.5 2.9-30.8 3.1-38.3 0-1.8 1.1-2.2 9-3.3 11.1-1.5 23.8-4.7 42-10.5 14.9-4.8 35.3-12.5 37.6-14.3.8-.6 3-1.7 4.9-2.4 1.9-.8 5.9-2.9 8.8-4.7 2.9-1.8 5.5-3.3 5.9-3.3.3 0 1.4-.6 2.2-1.4 2.5-2.2 25.9-14.2 32.3-16.6 6.4-2.4 16-8.5 26.1-16.9 3.7-3.1 8-6.4 9.5-7.3 2.5-1.7 2.7-1.7 3.6 0 .5.9 1.3 1.9 1.7 2.2z"/><path d="M461.2 337.1c-3.8 6.9-14.6 23.7-17.8 27.6-3.9 4.8-7.3 11.8-7.4 15 0 2.9 2 3.8 10.2 5.1 3.7.6 8.8 2 11.4 3.1 2.5 1.2 5.3 2.1 6 2.1.7 0 2.9.7 4.8 1.7 4.3 2 6 1 8.6-5.2 2.8-6.5 11.1-14.7 23.5-23.1 8.4-5.7 11.7-8.6 12.1-10.4.3-1.4 1-3.5 1.6-4.7.9-1.8.8-2.7-.7-4.3-1.6-1.8-3.1-2-13.4-2-12.7 0-20.1-1.4-28.1-5.5-6.6-3.3-8.7-3.2-10.8.6zM678.2 500.6c-2.1 1.4-6.9 1.8-34.5 2.5-20.8.6-35.7.6-42-.1-5.5-.6-38.1-1-74.2-.9-61.8 0-64.3.1-64.8 1.9-.4 1.5-1.2 1.7-4.4 1.3-5.1-.7-7.9 1.7-16.3 13.4-3.4 4.9-9.7 13.2-13.9 18.5l-7.6 9.7-97.5.3c-73.3.2-98.2.6-100.2 1.5-3.4 1.5-5.8 5.2-7.4 11.4-.7 2.7-2.3 6.3-3.6 8-4.1 5.3-15.1 18.4-22.3 26.5-12.1 13.6-20.8 26.8-24.9 37.6-2 5.3-3.6 10.7-3.6 11.9 0 2.9 6 8.4 10.7 9.9 2.1.7 4.5 1.8 5.3 2.5.8.7 3.3 1.5 5.5 1.8 2.2.2 10.5 1.6 18.5 3 39.3 7.1 67.1 8.7 101 5.9 8.5-.6 37.3-1.7 64-2.2 50.7-1 71.8-2.2 83.4-4.5 8.7-1.8 11.6-3.7 11.6-7.4 0-1.6.9-4 2-5.3 1.9-2.5 5.8-12 8.7-21.3.8-2.8 2.1-6.5 2.9-8.2l1.4-3.3h12.2c6.8 0 16.1-.4 20.8-1 4.7-.5 23.4-1.4 41.5-2 18.2-.6 35.7-1.5 39-2 3.3-.5 13.9-1.5 23.5-2.1 9.6-.5 18.6-1.3 20-1.7 1.6-.5 3.7-2.7 5.7-6.2 5.6-9.6 8.1-13.3 15.3-23 3.8-5.2 7.7-11 8.6-12.8.9-1.9 2.7-5 4.1-7 1.3-2 6.3-11.3 11-20.7 4.7-9.3 9.1-17.7 9.9-18.5 4-4.6 5.7-14.7 2.9-17-2.5-2.1-9.7-2.4-12.3-.4zM654 519.4c2.5 0 6.2-.3 8.2-.8 3.4-.7 3.9-.6 4.8 1.6 1 2 .8 3-1 5.9-1.2 1.9-5.2 8.8-8.9 15.4-8.6 15.4-12 20.8-20.4 32.5-5.2 7.3-6.7 10.1-6.4 12.2.4 3.1.2 3.1-19.3 4.3-5.2.4-11.5 1.1-14 1.6s-18.9 1.3-36.5 1.8c-19.9.6-36.4 1.6-43.5 2.6-13.5 2-38.8 1.7-41.4-.4-1.2-1-3.1-1-9.3-.1-9.1 1.4-28.5.7-38.3-1.4-3.6-.8-9.1-2-12.3-2.6-6.7-1.4-6.7-.8-.4-11.8 3.8-6.7 12.7-18.4 25.8-33.7 2.8-3.3 7.3-9.2 9.9-13.1 3.2-4.7 5.8-7.4 7.4-7.8 1.4-.3 3.7-2.1 5.1-3.9l2.6-3.2 91.7.4c50.4.3 93.7.5 96.2.5zm-247 45.1c0 .2-1.3 1.9-2.9 3.7-1.6 1.8-4.1 5.5-5.6 8.3-1.5 2.7-4.5 7.3-6.6 10.1-5.4 7.1-5.3 12.1.2 16.2 1.9 1.4 14.2 4.7 21.5 5.7 2.7.4 7.2 1.3 10 2s10.2 1.6 16.5 1.9c16.1.9 17.9 1.3 17.9 3.6 0 1.1-.8 2.8-1.7 3.8-3.6 3.8-6.2 10.1-6.9 16.6-.4 3.6-1.2 7-1.8 7.5-1.7 1.4-31.8 3.1-72.1 4-25.2.5-37.7 1.2-39.2 2-2 1-2.2.9-1.6-.5.5-1.5 0-1.6-3.8-1-2.4.3-13.6 1-24.9 1.6-11.3.5-22.5 1.2-25 1.5-2.5.3-14.6.1-27-.4-18.7-.8-25.7-1.6-41.5-4.5-21.3-4-33.5-6.6-33.5-7.1 0-3.7 13.5-25.5 17.4-28 1.4-.9 2.8-2.5 3.1-3.6.4-1.1 4.3-5.8 8.7-10.6 7.9-8.5 15.5-18.2 21.4-27.3l3-4.5 8.9-.6c11-.6 165.5-1.1 165.5-.4z"/><path d="M526 527.9c0 .4-1.6 1.1-3.5 1.4-5.5.9-10.5 4.4-10.5 7.3 0 4 4.2 7.4 9.2 7.4 10.3 0 15.6-3.8 14.4-10.4-.8-4.1-2.8-6-5.6-5.3-1.1.3-2 .1-2-.4s-.4-.9-1-.9c-.5 0-1 .4-1 .9z"/><path d="M492.3 541c-5 1.2-9.7 4.4-10.8 7.4-1.4 4 1.8 9.3 7.5 12.4 5 2.7 11.5 2.6 17.2-.3 1.7-.8 4.6-1.5 6.5-1.5 1.9 0 4.3-.5 5.4-1 2.5-1.4 2.4-.8-.6 3.6s-3.1 6.8-.5 9.4c4.1 4.1 17.8 1.8 20.2-3.4.9-2.1.8-2.9-.5-4.4-1-1-1.7-2.3-1.7-2.9 0-1.4-2.4-2.3-6.3-2.4-3.2 0-3.2 0-.7-1 1.5-.6 6.3-.8 12-.4 5.2.4 11.4.8 13.6 1 2.3.1 4.7.6 5.4 1 .7.4 4.1 1 7.5 1.3 6.4.5 6.4.5 8.5-2.9 1.3-2.1 1.9-4.7 1.8-6.9l-.3-3.5-7 .1c-3.8 0-11.7.2-17.5.4-5.8.2-11.8 0-13.4-.5-1.7-.5-6.8-.3-12.6.5l-9.8 1.3-8.4-4.2c-8.5-4.2-9.9-4.5-15.5-3.1zM311.3 579.1c-2.6.6-4.3 1.6-4.3 2.5 0 .8-1 1.4-2.3 1.4-2.4 0-6.7 3.7-6.7 5.8 0 .7-.4 1.2-1 1.2-1.7 0-1.1 4.1 1.1 6.8l2.1 2.7-4.3.3c-2.3.2-8.4-.4-13.6-1.4-10.7-1.9-11.9-1.5-12.1 4.2-.1 3.7 1.2 7.3 3.2 8.6 2 1.4 16.5.9 20.3-.7 1.9-.8 4.7-1.5 6.1-1.5 2.4 0 2.4.2 1.8 4.6-1 7.7 1.3 9.8 13.1 11.5 5.4.8 6.9.6 13-1.7 20.1-7.6 44.1-23.5 44.3-29.4 0-.8-.5-3.2-1.1-5.3-.9-3.2-1.9-4.2-7.2-7l-6.2-3.2-21-.2c-11.5-.1-22.9.3-25.2.8z"/></svg> \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 05328d0c12a4badfda097d8a2af0afb802f99e49..4b5f9872d7b52a12eeeabf8ea7500833a522d8f3 100644 --- a/templates/base.html +++ b/templates/base.html @@ -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> diff --git a/templates/member/manage_auth_tokens.html b/templates/member/manage_auth_tokens.html new file mode 100644 index 0000000000000000000000000000000000000000..0103fbbba7ff17d4c9650cd67806b9e556ffeb96 --- /dev/null +++ b/templates/member/manage_auth_tokens.html @@ -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 %} diff --git a/templates/member/profile_detail.html b/templates/member/profile_detail.html index 53a0b9a0c3608584f8d7b8136a77f01ecfe0ea12..655f9893271d4c582a087d701b0ad53ae0c1ff62 100644 --- a/templates/member/profile_detail.html +++ b/templates/member/profile_detail.html @@ -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> - </div> + <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> + <dd class="col-xl-6">{{ object.section }}</dd> - <div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordionProfile"> - <div class="card-body"> - {% render_table club_list %} - </div> - </div> - </div> - <div class="card"> - <div class="card-header" id="headingTwo"> - <h5 class="mb-0"> - <button class="btn btn-link collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo"> - <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> + <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 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> - {% endblock %} + <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> +{% endblock %} diff --git a/templates/member/profile_update.html b/templates/member/profile_update.html index 10936cf77edc95a19096d846628647419f8dd657..a47a147bdc9d1c07b62c6a411396bfa68b681f6b 100644 --- a/templates/member/profile_update.html +++ b/templates/member/profile_update.html @@ -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 %} diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html new file mode 100644 index 0000000000000000000000000000000000000000..b121ad54d12f970a66f9572194d3b45d8a3376c8 --- /dev/null +++ b/templates/note/conso_form.html @@ -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 %} diff --git a/tox.ini b/tox.ini index c86913729a5e88184c9947fe10a7cee4af2766f1..c4e88c786dc93b3d03e10e9b775644415267a2a1 100644 --- a/tox.ini +++ b/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 -