diff --git a/.gitignore b/.gitignore index f1650504..b57ed74a 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ coverage # PyCharm project settings .idea +# VSCode project settings +.vscode + # Local data secrets.py *.log diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bceb5f51..291ed490 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,20 +2,25 @@ image: python:3.6 stages: - test + - quality-assurance before_script: - pip install tox -python36: +py36-django22: image: python:3.6 stage: test - script: tox -e py36 + script: tox -e py36-django22 -python37: +py37-django22: image: python:3.7 stage: test - script: tox -e py37 + script: tox -e py37-django22 linters: - stage: test + image: python:3.6 + stage: quality-assurance script: tox -e linters + + # Be nice to new contributors, but please use `tox` + allow_failure: true diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 6ddf1f3c..00000000 --- a/.pylintrc +++ /dev/null @@ -1,379 +0,0 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS,.git - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=4 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. -optimize-ast=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence=INFERENCE_FAILURE - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=intern-builtin,nonzero-method,parameter-unpacking,backtick,raw_input-builtin,dict-view-method,filter-builtin-not-iterating,long-builtin,unichr-builtin,input-builtin,unicode-builtin,file-builtin,map-builtin-not-iterating,delslice-method,apply-builtin,cmp-method,setslice-method,coerce-method,long-suffix,raising-string,import-star-module-level,buffer-builtin,reload-builtin,unpacking-in-except,print-statement,hex-method,old-octal-literal,metaclass-assignment,dict-iter-method,range-builtin-not-iterating,using-cmp-argument,indexing-exception,no-absolute-import,coerce-builtin,getslice-method,suppressed-message,execfile-builtin,round-builtin,useless-suppression,reduce-builtin,old-raise-syntax,zip-builtin-not-iterating,cmp-builtin,xrange-builtin,standarderror-builtin,old-division,oct-method,next-method-called,old-ne-operator,basestring-builtin - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=no - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[BASIC] - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_ - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=yes - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work -# with qualified names. -ignored-classes= - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setUp - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=20 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=10 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in an if statement -max-bool-expr=5 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=optparse - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception - diff --git a/apps/activity/__init__.py b/apps/activity/__init__.py index 75df9e1f..195d5302 100644 --- a/apps/activity/__init__.py +++ b/apps/activity/__init__.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later default_app_config = 'activity.apps.ActivityConfig' diff --git a/apps/activity/admin.py b/apps/activity/admin.py index 1efe272c..5ceb4e81 100644 --- a/apps/activity/admin.py +++ b/apps/activity/admin.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin @@ -12,7 +11,7 @@ class ActivityAdmin(admin.ModelAdmin): Admin customisation for Activity """ list_display = ('name', 'activity_type', 'organizer') - list_filter = ('activity_type',) + list_filter = ('activity_type', ) search_fields = ['name', 'organizer__name'] # Organize activities by start date diff --git a/apps/activity/api/serializers.py b/apps/activity/api/serializers.py index f7f949e7..0b9302f1 100644 --- a/apps/activity/api/serializers.py +++ b/apps/activity/api/serializers.py @@ -1,10 +1,11 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from ..models import ActivityType, Activity, Guest from rest_framework import serializers +from ..models import ActivityType, Activity, Guest + + class ActivityTypeSerializer(serializers.ModelSerializer): """ REST API Serializer for Activity types. diff --git a/apps/activity/api/urls.py b/apps/activity/api/urls.py index 56665730..79e0ba30 100644 --- a/apps/activity/api/urls.py +++ b/apps/activity/api/urls.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py index 4a0973e5..5683d458 100644 --- a/apps/activity/api/views.py +++ b/apps/activity/api/views.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/apps/activity/apps.py b/apps/activity/apps.py index 29990f1b..bb72947f 100644 --- a/apps/activity/apps.py +++ b/apps/activity/apps.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.apps import AppConfig diff --git a/apps/activity/models.py b/apps/activity/models.py index 4dbc5522..8f23060c 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.conf import settings diff --git a/apps/api/urls.py b/apps/api/urls.py index 7ac56ca1..7e59a8c0 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -1,12 +1,9 @@ -# -*- mode: python; coding: utf-8 -*- # 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 rest_framework.authtoken import views as token_views - from activity.api.urls import register_activity_urls from member.api.urls import register_members_urls from note.api.urls import register_note_urls @@ -17,10 +14,13 @@ class UserSerializer(serializers.ModelSerializer): REST API Serializer for Users. The djangorestframework plugin will analyse the model `User` and parse all fields in the API. """ - class Meta: model = User - exclude = ('password', 'groups', 'user_permissions',) + exclude = ( + 'password', + 'groups', + 'user_permissions', + ) class UserViewSet(viewsets.ModelViewSet): diff --git a/apps/member/__init__.py b/apps/member/__init__.py index ec189d6f..298d1dda 100644 --- a/apps/member/__init__.py +++ b/apps/member/__init__.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later default_app_config = 'member.apps.MemberConfig' diff --git a/apps/member/admin.py b/apps/member/admin.py index f45d5f55..fb107377 100644 --- a/apps/member/admin.py +++ b/apps/member/admin.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin @@ -19,9 +18,9 @@ class ProfileInline(admin.StackedInline): class CustomUserAdmin(UserAdmin): - inlines = (ProfileInline,) + inlines = (ProfileInline, ) list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff') - list_select_related = ('profile',) + list_select_related = ('profile', ) form = ProfileForm def get_inline_instances(self, request, obj=None): diff --git a/apps/member/api/serializers.py b/apps/member/api/serializers.py index cf4420d5..f4df6799 100644 --- a/apps/member/api/serializers.py +++ b/apps/member/api/serializers.py @@ -1,10 +1,10 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from ..models import Profile, Club, Role, Membership from rest_framework import serializers +from ..models import Profile, Club, Role, Membership + class ProfileSerializer(serializers.ModelSerializer): """ diff --git a/apps/member/api/urls.py b/apps/member/api/urls.py index f60465c0..15bb83ca 100644 --- a/apps/member/api/urls.py +++ b/apps/member/api/urls.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/apps/member/api/views.py b/apps/member/api/views.py index 36e8a33f..79ba4c12 100644 --- a/apps/member/api/views.py +++ b/apps/member/api/views.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/apps/member/apps.py b/apps/member/apps.py index 928c00e4..2d7f4ab7 100644 --- a/apps/member/apps.py +++ b/apps/member/apps.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.apps import AppConfig diff --git a/apps/member/filters.py b/apps/member/filters.py index fb1a2128..418e52fc 100644 --- a/apps/member/filters.py +++ b/apps/member/filters.py @@ -1,31 +1,33 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from django_filters import FilterSet, CharFilter,NumberFilter +from django_filters import FilterSet, CharFilter from django.contrib.auth.models import User from django.db.models import CharField from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Submit -from .models import Club class UserFilter(FilterSet): class Meta: model = User - fields = ['last_name','first_name','username','profile__section'] - filter_overrides={ - CharField:{ - 'filter_class':CharFilter, - 'extra': lambda f:{ - 'lookup_expr':'icontains' + fields = ['last_name', 'first_name', 'username', 'profile__section'] + filter_overrides = { + CharField: { + 'filter_class': CharFilter, + 'extra': lambda f: { + 'lookup_expr': 'icontains' } } } + class UserFilterFormHelper(FormHelper): form_method = 'GET' layout = Layout( - 'last_name','first_name','username','profile__section', - Submit('Submit','Apply Filter'), + 'last_name', + 'first_name', + 'username', + 'profile__section', + Submit('Submit', 'Apply Filter'), ) diff --git a/apps/member/forms.py b/apps/member/forms.py index 4d03764e..66844cf4 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -1,26 +1,23 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later + from dal import autocomplete -from django.contrib.auth.forms import UserChangeForm, UserCreationForm +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,48 +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, - }), + '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 35b7027c..cd754bd8 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -1,14 +1,12 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.conf import settings from django.db import models -from django.db.models.signals import post_save -from django.dispatch import receiver from django.utils.translation import gettext_lazy as _ from django.urls import reverse, reverse_lazy + class Profile(models.Model): """ An user profile @@ -50,7 +48,8 @@ class Profile(models.Model): verbose_name_plural = _('user profile') def get_absolute_url(self): - return reverse('user_detail',args=(self.pk,)) + return reverse('user_detail', args=(self.pk, )) + class Club(models.Model): """ @@ -98,7 +97,7 @@ class Club(models.Model): return self.name def get_absolute_url(self): - return reverse_lazy('member:club_detail', args=(self.pk,)) + return reverse_lazy('member:club_detail', args=(self.pk, )) class Role(models.Model): @@ -129,15 +128,15 @@ class Membership(models.Model): """ user = models.ForeignKey( settings.AUTH_USER_MODEL, - on_delete=models.PROTECT + on_delete=models.PROTECT, ) club = models.ForeignKey( Club, - on_delete=models.PROTECT + on_delete=models.PROTECT, ) roles = models.ForeignKey( Role, - on_delete=models.PROTECT + on_delete=models.PROTECT, ) date_start = models.DateField( verbose_name=_('membership starts on'), diff --git a/apps/member/signals.py b/apps/member/signals.py index 6688516b..4e945ad5 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -1,6 +1,2 @@ -#!/usr/bin/env python - -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later - diff --git a/apps/member/tables.py b/apps/member/tables.py index 4218948c..591149ec 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -1,19 +1,25 @@ -#!/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-bordered table-condensed table-striped table-hover' + } model = Club template_name = 'django_tables2/bootstrap.html' - fields = ('id','name','email') - row_attrs = {'class':'table-row', - 'data-href': lambda record: record.pk } - + fields = ('id', 'name', 'email') + row_attrs = { + 'class': 'table-row', + 'data-href': lambda record: record.pk + } class UserTable(tables.Table): @@ -21,7 +27,10 @@ class UserTable(tables.Table): solde = tables.Column(accessor='note.balance') class Meta: - attrs = {'class':'table table-bordered table-condensed table-striped table-hover'} + attrs = { + 'class': + 'table table-bordered table-condensed table-striped table-hover' + } template_name = 'django_tables2/bootstrap.html' - fields = ('last_name','first_name','username','email') + fields = ('last_name', 'first_name', 'username', 'email') model = User diff --git a/apps/member/urls.py b/apps/member/urls.py index d4e3e6af..6a7ed5ce 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -1,7 +1,4 @@ -#!/usr/bin/env python - -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.urls import path @@ -10,16 +7,16 @@ from . import views app_name = 'member' urlpatterns = [ - path('signup/',views.UserCreateView.as_view(),name="signup"), - path('club/',views.ClubListView.as_view(),name="club_list"), - path('club//',views.ClubDetailView.as_view(),name="club_detail"), - path('club//add_member/',views.ClubAddMemberView.as_view(),name="club_add_member"), - path('club/create/',views.ClubCreateView.as_view(),name="club_create"), - path('user/',views.UserListView.as_view(),name="user_list"), - path('user/',views.UserDetailView.as_view(),name="user_detail"), - path('user//update',views.UserUpdateView.as_view(),name="user_update_profile"), + path('signup/', views.UserCreateView.as_view(), name="signup"), + path('club/', views.ClubListView.as_view(), name="club_list"), + path('club//', views.ClubDetailView.as_view(), name="club_detail"), + path('club//add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), + path('club/create/', views.ClubCreateView.as_view(), name="club_create"), + path('user/', views.UserListView.as_view(), name="user_list"), + path('user/', views.UserDetailView.as_view(), name="user_detail"), + path('user//update', views.UserUpdateView.as_view(), name="user_update_profile"), path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), # API for the user autocompleter - path('user/user-autocomplete',views.UserAutocomplete.as_view(),name="user_autocomplete"), + path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"), ] diff --git a/apps/member/views.py b/apps/member/views.py index be2d8d58..366523b0 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, RedirectView, TemplateView +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, Note, NoteUser -from .models import Profile, Club, Membership -from .forms import SignUpForm, ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper -from .tables import ClubTable,UserTable -from .filters import UserFilter, UserFilterFormHelper - - +from note.models import Alias, NoteUser from note.models.transactions import Transaction from note.tables import HistoryTable +from .models import Profile, Club, Membership +from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper +from .tables import ClubTable, UserTable +from .filters import UserFilter, UserFilterFormHelper + + class UserCreateView(CreateView): """ Une vue pour inscrire un utilisateur et lui créer un profile @@ -31,10 +28,10 @@ class UserCreateView(CreateView): form_class = SignUpForm success_url = reverse_lazy('login') - template_name ='member/signup.html' + template_name = 'member/signup.html' second_form = ProfileForm - def get_context_data(self,**kwargs): + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["profile_form"] = self.second_form() @@ -49,17 +46,20 @@ 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['user_modified'] = context['user'] context['user'] = self.request.user - context["profile_form"] = self.second_form(instance=context['user_modified'].profile) + context["profile_form"] = self.second_form( + instance=context['user_modified'].profile) return context @@ -71,44 +71,52 @@ class UserUpdateView(LoginRequiredMixin,UpdateView): new_username = form.data['username'] # Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant - note = NoteUser.objects.filter(alias__normalized_name=Alias.normalize(new_username)) + note = NoteUser.objects.filter( + alias__normalized_name=Alias.normalize(new_username)) if note.exists() and note.get().user != self.request.user: - form.add_error('username', _("An alias with a similar name already exists.")) + form.add_error('username', + _("An alias with a similar name already exists.")) return form - def form_valid(self, form): - profile_form = ProfileForm(data=self.request.POST,instance=self.request.user.profile) + profile_form = ProfileForm( + data=self.request.POST, + instance=self.request.user.profile, + ) if form.is_valid() and profile_form.is_valid(): new_username = form.data['username'] alias = Alias.objects.filter(name=new_username) # Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer if not alias.exists(): - similar = Alias.objects.filter(normalized_name=Alias.normalize(new_username)) + similar = Alias.objects.filter( + normalized_name=Alias.normalize(new_username)) if similar.exists(): similar.delete() user = form.save(commit=False) - profile = profile_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 ... """ model = Profile context_object_name = "profile" - def get_context_data(slef,**kwargs): + + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = context['profile'].user history_list = \ @@ -119,7 +127,8 @@ class UserDetailView(LoginRequiredMixin,DetailView): context['club_list'] = ClubTable(club_list) return context -class UserListView(LoginRequiredMixin,SingleTableView): + +class UserListView(LoginRequiredMixin, SingleTableView): """ Affiche la liste des utilisateurs, avec une fonction de recherche statique """ @@ -129,13 +138,13 @@ 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 @@ -149,22 +158,25 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): template_name = "member/manage_auth_tokens.html" def get(self, request, *args, **kwargs): - if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists(): + if 'regenerate' in request.GET and Token.objects.filter( + user=request.user).exists(): Token.objects.get(user=self.request.user).delete() - return redirect(reverse_lazy('member:auth_token') + "?show", permanent=True) + return redirect(reverse_lazy('member:auth_token') + "?show", + permanent=True) return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['token'] = Token.objects.get_or_create(user=self.request.user)[0] + context['token'] = Token.objects.get_or_create( + user=self.request.user)[0] return context + class UserAutocomplete(autocomplete.Select2QuerySetView): """ Auto complete users by usernames """ - def get_queryset(self): """ Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion. @@ -181,32 +193,36 @@ class UserAutocomplete(autocomplete.Select2QuerySetView): return qs -################################### -############## CLUB ############### -################################### -class ClubCreateView(LoginRequiredMixin,CreateView): +# ******************************* # +# CLUB # +# ******************************* # + + +class ClubCreateView(LoginRequiredMixin, CreateView): """ Create Club """ model = Club form_class = ClubForm - def form_valid(self,form): + def form_valid(self, form): return super().form_valid(form) -class ClubListView(LoginRequiredMixin,SingleTableView): + +class ClubListView(LoginRequiredMixin, SingleTableView): """ List existing Clubs """ model = Club table_class = ClubTable -class ClubDetailView(LoginRequiredMixin,DetailView): - model = Club - context_object_name="club" - def get_context_data(self,**kwargs): +class ClubDetailView(LoginRequiredMixin, DetailView): + model = Club + context_object_name = "club" + + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) club = context["club"] club_transactions = \ @@ -218,23 +234,27 @@ 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() return context - def post(self,request,*args,**kwargs): - formset = MembershipFormset(request.POST) - if formset.is_valid(): - return self.form_valid(formset) - else: - return self.form_invalid(formset) + def post(self, request, *args, **kwargs): + return + # TODO: Implement POST + # formset = MembershipFormset(request.POST) + # if formset.is_valid(): + # return self.form_valid(formset) + # else: + # return self.form_invalid(formset) - def form_valid(self,formset): + def form_valid(self, formset): formset.save() return super().form_valid(formset) diff --git a/apps/note/__init__.py b/apps/note/__init__.py index 4773b310..f7c331b2 100644 --- a/apps/note/__init__.py +++ b/apps/note/__init__.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later default_app_config = 'note.apps.NoteConfig' diff --git a/apps/note/admin.py b/apps/note/admin.py index 0b2461d0..3a9721ae 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin @@ -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): """ @@ -153,5 +159,5 @@ class TransactionCategoryAdmin(admin.ModelAdmin): """ Admin customisation for TransactionTemplate """ - list_display = ('name',) - list_filter = ('name',) + list_display = ('name', ) + list_filter = ('name', ) diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index afc3b419..db0e3531 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -1,12 +1,12 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias -from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction 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): """ @@ -17,7 +17,10 @@ class NoteSerializer(serializers.ModelSerializer): model = Note fields = '__all__' extra_kwargs = { - 'url': {'view_name': 'project-detail', 'lookup_field': 'pk'}, + 'url': { + 'view_name': 'project-detail', + 'lookup_field': 'pk' + }, } @@ -69,6 +72,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer): NoteSpecial: NoteSpecialSerializer } + class TransactionTemplateSerializer(serializers.ModelSerializer): """ REST API Serializer for Transaction templates. diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py index 4ef14e28..54218796 100644 --- a/apps/note/api/urls.py +++ b/apps/note/api/urls.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 37ca4e20..94b4a47a 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later @@ -69,17 +68,20 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet): queryset = Note.objects.all() alias = self.request.query_params.get("alias", ".*") - queryset = queryset.filter(Q(alias__name__regex=alias) | Q(alias__normalized_name__regex=alias.lower())) + queryset = queryset.filter( + Q(alias__name__regex=alias) + | Q(alias__normalized_name__regex=alias.lower())) note_type = self.request.query_params.get("type", None) if note_type: - l = str(note_type).lower() - if "user" in l: + types = str(note_type).lower() + if "user" in types: queryset = queryset.filter(polymorphic_ctype__model="noteuser") - elif "club" in l: + elif "club" in types: queryset = queryset.filter(polymorphic_ctype__model="noteclub") - elif "special" in l: - queryset = queryset.filter(polymorphic_ctype__model="notespecial") + elif "special" in types: + queryset = queryset.filter( + polymorphic_ctype__model="notespecial") else: queryset = queryset.none() @@ -104,7 +106,8 @@ class AliasViewSet(viewsets.ModelViewSet): queryset = Alias.objects.all() alias = self.request.query_params.get("alias", ".*") - queryset = queryset.filter(Q(name__regex=alias) | Q(normalized_name__regex=alias.lower())) + queryset = queryset.filter( + Q(name__regex=alias) | Q(normalized_name__regex=alias.lower())) note_id = self.request.query_params.get("note", None) if note_id: @@ -112,13 +115,16 @@ class AliasViewSet(viewsets.ModelViewSet): note_type = self.request.query_params.get("type", None) if note_type: - l = str(note_type).lower() - if "user" in l: - queryset = queryset.filter(note__polymorphic_ctype__model="noteuser") - elif "club" in l: - queryset = queryset.filter(note__polymorphic_ctype__model="noteclub") - elif "special" in l: - queryset = queryset.filter(note__polymorphic_ctype__model="notespecial") + 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() diff --git a/apps/note/apps.py b/apps/note/apps.py index c53f915a..4881e3b9 100644 --- a/apps/note/apps.py +++ b/apps/note/apps.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.apps import AppConfig @@ -20,9 +19,9 @@ class NoteConfig(AppConfig): """ post_save.connect( signals.save_user_note, - sender=settings.AUTH_USER_MODEL + sender=settings.AUTH_USER_MODEL, ) post_save.connect( signals.save_club_note, - sender='member.Club' + sender='member.Club', ) diff --git a/apps/note/forms.py b/apps/note/forms.py index 09818931..e4fd344c 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -1,13 +1,16 @@ -#!/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, forward +from dal import autocomplete from django import forms + 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 @@ -15,11 +18,14 @@ class TransactionTemplateForm(forms.ModelForm): # Pour force le type d'une note, il faut rajouter le paramètre : # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special} widgets = { - 'destination': autocomplete.ModelSelect2(url='note:note_autocomplete', - 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, + }, + ), } @@ -31,26 +37,38 @@ class TransactionForm(forms.ModelForm): class Meta: model = Transaction - fields = ('source', 'destination', 'reason', 'amount',) + fields = ( + 'source', + 'destination', + 'reason', + 'amount', + ) # Voir ci-dessus widgets = { - 'source': autocomplete.ModelSelect2(url='note:note_autocomplete', - 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, - },), + '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): +class ConsoForm(forms.ModelForm): def save(self, commit=True): - button: TransactionTemplate = TransactionTemplate.objects.filter(name=self.data['button']).get() + button: TransactionTemplate = TransactionTemplate.objects.filter( + name=self.data['button']).get() self.instance.destination = button.destination self.instance.amount = button.amount self.instance.transaction_type = 'bouton' @@ -59,15 +77,18 @@ class ConsoForm(forms.ModelForm): class Meta: model = Transaction - fields = ('source',) + 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, - }), + '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 ee290bb5..7e6cc310 100644 --- a/apps/note/models/__init__.py +++ b/apps/note/models/__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 from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 6a0c5ebe..3b616f0e 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later import unicodedata @@ -10,7 +9,6 @@ from django.core.validators import RegexValidator from django.db import models from django.utils.translation import gettext_lazy as _ from polymorphic.models import PolymorphicModel - """ Defines each note types """ @@ -34,8 +32,7 @@ class Note(PolymorphicModel): default=True, help_text=_( 'Designates whether this note should be treated as active. ' - 'Unselect this instead of deleting notes.' - ), + 'Unselect this instead of deleting notes.'), ) display_image = models.ImageField( verbose_name=_('display image'), @@ -85,7 +82,8 @@ class Note(PolymorphicModel): """ Verify alias (simulate save) """ - aliases = Alias.objects.filter(normalized_name=Alias.normalize(str(self))) + aliases = Alias.objects.filter( + normalized_name=Alias.normalize(str(self))) if aliases.exists(): # Alias exists, so check if it is linked to this note if aliases.first().note != self: @@ -181,15 +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,8 +225,9 @@ 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 diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 5c242255..042faa16 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,12 +6,13 @@ 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 @@ -32,6 +32,7 @@ class TransactionCategory(models.Model): def __str__(self): return str(self.name) + class TransactionTemplate(models.Model): """ Defined a recurrent transaction @@ -57,7 +58,7 @@ class TransactionTemplate(models.Model): TransactionCategory, on_delete=models.PROTECT, verbose_name=_('type'), - max_length=31 + max_length=31, ) class Meta: @@ -65,7 +66,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): @@ -98,9 +99,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, @@ -142,7 +141,7 @@ class Transaction(models.Model): @property def total(self): - return self.amount*self.quantity + return self.amount * self.quantity class MembershipTransaction(Transaction): diff --git a/apps/note/signals.py b/apps/note/signals.py index 6e5d5c9e..ad376ee0 100644 --- a/apps/note/signals.py +++ b/apps/note/signals.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/apps/note/tables.py b/apps/note/tables.py index 31cefe41..e2f5c763 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -1,4 +1,6 @@ -#!/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 @@ -7,16 +9,18 @@ from .models.transactions import Transaction class HistoryTable(tables.Table): class Meta: - attrs = {'class':'table table-bordered table-condensed table-striped table-hover'} + attrs = { + 'class': + 'table table-bordered table-condensed table-striped table-hover' + } model = Transaction template_name = 'django_tables2/bootstrap.html' - sequence = ('...','total','valid') + sequence = ('...', 'total', 'valid') - total = tables.Column() #will use Transaction.total() !! + total = tables.Column() # will use Transaction.total() !! - def order_total(self, QuerySet, is_descending): + def order_total(self, queryset, is_descending): # needed for rendering - QuerySet = QuerySet.annotate( - total=F('amount') * F('quantity') - ).order_by(('-' if is_descending else '') + 'total') - return (QuerySet, True) + queryset = queryset.annotate(total=F('amount') * F('quantity')) \ + .order_by(('-' if is_descending else '') + 'total') + return (queryset, True) diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index ec0a0d5b..12530c6e 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -1,11 +1,21 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + from django import template def pretty_money(value): - if value%100 == 0: - return "{:s}{:d} €".format("- " if value < 0 else "", abs(value) // 100) + if value % 100 == 0: + return "{:s}{:d} €".format( + "- " if value < 0 else "", + abs(value) // 100, + ) else: - return "{:s}{:d} € {:02d}".format("- " if value < 0 else "", abs(value) // 100, abs(value) % 100) + return "{:s}{:d} € {:02d}".format( + "- " if value < 0 else "", + abs(value) // 100, + abs(value) % 100, + ) register = template.Library() diff --git a/apps/note/urls.py b/apps/note/urls.py index 7c19c425..e1cc5216 100644 --- a/apps/note/urls.py +++ b/apps/note/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.urls import path @@ -10,12 +9,12 @@ from .models import Note app_name = 'note' urlpatterns = [ path('transfer/', views.TransactionCreate.as_view(), name='transfer'), - path('buttons/create/',views.TransactionTemplateCreateView.as_view(),name='template_create'), - path('buttons/update//',views.TransactionTemplateUpdateView.as_view(),name='template_update'), - path('buttons/',views.TransactionTemplateListView.as_view(),name='template_list'), - path('consos//',views.ConsoView.as_view(),name='consos'), - path('consos/',views.ConsoView.as_view(),name='consos'), + path('buttons/create/', views.TransactionTemplateCreateView.as_view(), name='template_create'), + path('buttons/update//', views.TransactionTemplateUpdateView.as_view(), name='template_update'), + path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'), + path('consos//', views.ConsoView.as_view(), name='consos'), + 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'), + 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 3414a6c0..b012ad8b 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -1,17 +1,17 @@ -# -*- 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_lazy, reverse +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 Note, Transaction, TransactionCategory, TransactionTemplate, Alias +from .models import Transaction, TransactionCategory, TransactionTemplate, Alias from .forms import TransactionForm, TransactionTemplateForm, ConsoForm + class TransactionCreate(LoginRequiredMixin, CreateView): """ Show transfer page @@ -30,14 +30,13 @@ class TransactionCreate(LoginRequiredMixin, CreateView): 'to one or others') return context - def get_form(self, form_class=None): """ If the user has no right to transfer funds, then it won't have the choice of the source of the transfer. """ form = super().get_form(form_class) - if False: # TODO: fix it with "if %user has no right to transfer funds" + if False: # TODO: fix it with "if %user has no right to transfer funds" del form.fields['source'] return form @@ -46,7 +45,7 @@ class TransactionCreate(LoginRequiredMixin, CreateView): """ 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" + 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) @@ -56,7 +55,6 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView): """ Auto complete note by aliases """ - def get_queryset(self): """ Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion. @@ -76,12 +74,12 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView): # Filtrage par type de note (user, club, special) note_type = self.forwarded.get("note_type", None) if note_type: - l = str(note_type).lower() - if "user" in l: + types = str(note_type).lower() + if "user" in types: qs = qs.filter(note__polymorphic_ctype__model="noteuser") - elif "club" in l: + elif "club" in types: qs = qs.filter(note__polymorphic_ctype__model="noteclub") - elif "special" in l: + elif "special" in types: qs = qs.filter(note__polymorphic_ctype__model="notespecial") else: qs = qs.none() @@ -101,27 +99,30 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView): return str(result.note.pk) -class TransactionTemplateCreateView(LoginRequiredMixin,CreateView): +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 -class ConsoView(LoginRequiredMixin,CreateView): + +class ConsoView(LoginRequiredMixin, CreateView): """ Consume """ @@ -139,11 +140,14 @@ class ConsoView(LoginRequiredMixin,CreateView): if 'template_type' not in self.kwargs.keys(): return context - template_type = TransactionCategory.objects.filter(name=self.kwargs.get('template_type')).get() - context['buttons'] = TransactionTemplate.objects.filter(template_type=template_type) + template_type = TransactionCategory.objects.filter( + name=self.kwargs.get('template_type')).get() + context['buttons'] = TransactionTemplate.objects.filter( + template_type=template_type) context['title'] = template_type return context def get_success_url(self): - return reverse('note:consos',args=(self.kwargs.get('template_type'),)) + return reverse('note:consos', + args=(self.kwargs.get('template_type'), )) diff --git a/entrypoint.sh b/entrypoint.sh index da32571a..f05e962a 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,11 @@ #!/bin/bash +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + python manage.py compilemessages python manage.py makemigrations + +# Wait for database sleep 5 python manage.py migrate diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 234e70b9..68a40b88 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -30,12 +30,17 @@ read_env() app_stage = os.environ.get('DJANGO_APP_STAGE', 'dev') if app_stage == 'prod': from .production import * - DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD','CHANGE_ME_IN_ENV_SETTINGS'); - SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY','CHANGE_ME_IN_ENV_SETTINGS'); - ALLOWED_HOSTS.append(os.environ.get('ALLOWED_HOSTS','localhost')); + DATABASES["default"]["PASSWORD"] = os.environ.get('DJANGO_DB_PASSWORD','CHANGE_ME_IN_ENV_SETTINGS') + SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY','CHANGE_ME_IN_ENV_SETTINGS') + ALLOWED_HOSTS.append(os.environ.get('ALLOWED_HOSTS','localhost')) else: from .development import * +try: + from .secrets import * +except ImportError: + pass + # env variables set at the of in /env/bin/activate # don't forget to unset in deactivate ! diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 9a526863..410f496f 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 diff --git a/note_kfet/settings/development.py b/note_kfet/settings/development.py index f6d48776..60055ee2 100644 --- a/note_kfet/settings/development.py +++ b/note_kfet/settings/development.py @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + ######################## # Development Settings # ######################## diff --git a/note_kfet/settings/production.py b/note_kfet/settings/production.py index 02c46ed2..296c17a4 100644 --- a/note_kfet/settings/production.py +++ b/note_kfet/settings/production.py @@ -1,3 +1,6 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + ######################## # Production Settings # ######################## diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 9fd63eef..303e229a 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -1,5 +1,4 @@ -# -*- mode: python; coding: utf-8 -*- -# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later from django.contrib import admin diff --git a/note_kfet/wsgi.py b/note_kfet/wsgi.py index 94a8e054..b89430ec 100644 --- a/note_kfet/wsgi.py +++ b/note_kfet/wsgi.py @@ -1,4 +1,3 @@ -# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2016-2019 by BDE # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/requirements.txt b/requirements.txt index d103764e..2899ef61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ 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.3.0 django-crispy-forms==1.7.2 diff --git a/tox.ini b/tox.ini index c8691372..c4e88c78 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 -