Merge branch 'code_format' into 'master'

Format code

See merge request bde/nk20!9
This commit is contained in:
erdnaxe 2020-02-18 22:58:44 +01:00
commit 8f7a4b8b35
48 changed files with 406 additions and 703 deletions

3
.gitignore vendored
View File

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

View File

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

379
.pylintrc
View File

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

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'activity.apps.ActivityConfig' default_app_config = 'activity.apps.ActivityConfig'

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin

View File

@ -1,10 +1,11 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from ..models import ActivityType, Activity, Guest
from rest_framework import serializers from rest_framework import serializers
from ..models import ActivityType, Activity, Guest
class ActivityTypeSerializer(serializers.ModelSerializer): class ActivityTypeSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Activity types. REST API Serializer for Activity types.

View File

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

View File

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

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings

View File

@ -1,12 +1,9 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf.urls import url, include from django.conf.urls import url, include
from django.contrib.auth.models import User from django.contrib.auth.models import User
from rest_framework import routers, serializers, viewsets 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 activity.api.urls import register_activity_urls
from member.api.urls import register_members_urls from member.api.urls import register_members_urls
from note.api.urls import register_note_urls from note.api.urls import register_note_urls
@ -17,10 +14,13 @@ class UserSerializer(serializers.ModelSerializer):
REST API Serializer for Users. REST API Serializer for Users.
The djangorestframework plugin will analyse the model `User` and parse all fields in the API. The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
""" """
class Meta: class Meta:
model = User model = User
exclude = ('password', 'groups', 'user_permissions',) exclude = (
'password',
'groups',
'user_permissions',
)
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig' default_app_config = 'member.apps.MemberConfig'

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin

View File

@ -1,10 +1,10 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from ..models import Profile, Club, Role, Membership
from rest_framework import serializers from rest_framework import serializers
from ..models import Profile, Club, Role, Membership
class ProfileSerializer(serializers.ModelSerializer): class ProfileSerializer(serializers.ModelSerializer):
""" """

View File

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

View File

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

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig

View File

@ -1,14 +1,12 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # 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.contrib.auth.models import User
from django.db.models import CharField from django.db.models import CharField
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit from crispy_forms.layout import Layout, Submit
from .models import Club
class UserFilter(FilterSet): class UserFilter(FilterSet):
class Meta: class Meta:
@ -23,9 +21,13 @@ class UserFilter(FilterSet):
} }
} }
class UserFilterFormHelper(FormHelper): class UserFilterFormHelper(FormHelper):
form_method = 'GET' form_method = 'GET'
layout = Layout( layout = Layout(
'last_name','first_name','username','profile__section', 'last_name',
'first_name',
'username',
'profile__section',
Submit('Submit', 'Apply Filter'), Submit('Submit', 'Apply Filter'),
) )

View File

@ -1,27 +1,24 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete 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.contrib.auth.models import User
from django import forms from django import forms
from .models import Profile, Club, Membership from .models import Profile, Club, Membership
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms import layout, bootstrap from crispy_forms.bootstrap import Div
from crispy_forms.bootstrap import InlineField, FormActions, StrictButton, Div, Field
from crispy_forms.layout import Layout from crispy_forms.layout import Layout
class SignUpForm(UserCreationForm): class SignUpForm(UserCreationForm):
class Meta: class Meta:
model = User model = User
fields = ['first_name', 'last_name', 'username', 'email'] fields = ['first_name', 'last_name', 'username', 'email']
class ProfileForm(forms.ModelForm): class ProfileForm(forms.ModelForm):
""" """
A form for the extras field provided by the :model:`member.Profile` model. A form for the extras field provided by the :model:`member.Profile` model.
@ -31,15 +28,18 @@ class ProfileForm(forms.ModelForm):
fields = '__all__' fields = '__all__'
exclude = ['user'] exclude = ['user']
class ClubForm(forms.ModelForm): class ClubForm(forms.ModelForm):
class Meta: class Meta:
model = Club model = Club
fields = '__all__' fields = '__all__'
class AddMembersForm(forms.Form): class AddMembersForm(forms.Form):
class Meta: class Meta:
fields = ('', ) fields = ('', )
class MembershipForm(forms.ModelForm): class MembershipForm(forms.ModelForm):
class Meta: class Meta:
model = Membership model = Membership
@ -48,18 +48,24 @@ class MembershipForm(forms.ModelForm):
# Quand des lettres sont tapées, une requête est envoyée sur l'API 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 # et récupère les noms d'utilisateur valides
widgets = { widgets = {
'user': autocomplete.ModelSelect2(url='member:user_autocomplete', 'user':
autocomplete.ModelSelect2(
url='member:user_autocomplete',
attrs={ attrs={
'data-placeholder': 'Nom ...', 'data-placeholder': 'Nom ...',
'data-minimum-input-length': 1, 'data-minimum-input-length': 1,
}), },
),
} }
MemberFormSet = forms.modelformset_factory(Membership, MemberFormSet = forms.modelformset_factory(
Membership,
form=MembershipForm, form=MembershipForm,
extra=2, extra=2,
can_delete=True) can_delete=True,
)
class FormSetHelper(FormHelper): class FormSetHelper(FormHelper):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -74,5 +80,4 @@ class FormSetHelper(FormHelper):
Div('roles', css_class='col-sm-2'), Div('roles', css_class='col-sm-2'),
Div('date_start', css_class='col-sm-2'), Div('date_start', css_class='col-sm-2'),
css_class="row formset-row", css_class="row formset-row",
) ))
)

View File

@ -1,14 +1,12 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings from django.conf import settings
from django.db import models 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.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
class Profile(models.Model): class Profile(models.Model):
""" """
An user profile An user profile
@ -52,6 +50,7 @@ class Profile(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('user_detail', args=(self.pk, )) return reverse('user_detail', args=(self.pk, ))
class Club(models.Model): class Club(models.Model):
""" """
A club is a group of people, whose membership is handle by their A club is a group of people, whose membership is handle by their
@ -129,15 +128,15 @@ class Membership(models.Model):
""" """
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, settings.AUTH_USER_MODEL,
on_delete=models.PROTECT on_delete=models.PROTECT,
) )
club = models.ForeignKey( club = models.ForeignKey(
Club, Club,
on_delete=models.PROTECT on_delete=models.PROTECT,
) )
roles = models.ForeignKey( roles = models.ForeignKey(
Role, Role,
on_delete=models.PROTECT on_delete=models.PROTECT,
) )
date_start = models.DateField( date_start = models.DateField(
verbose_name=_('membership starts on'), verbose_name=_('membership starts on'),

View File

@ -1,6 +1,2 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -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 import django_tables2 as tables
from .models import Club
from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .models import Club
class ClubTable(tables.Table): class ClubTable(tables.Table):
class Meta: 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 model = Club
template_name = 'django_tables2/bootstrap.html' template_name = 'django_tables2/bootstrap.html'
fields = ('id', 'name', 'email') fields = ('id', 'name', 'email')
row_attrs = {'class':'table-row', row_attrs = {
'data-href': lambda record: record.pk } 'class': 'table-row',
'data-href': lambda record: record.pk
}
class UserTable(tables.Table): class UserTable(tables.Table):
@ -21,7 +27,10 @@ class UserTable(tables.Table):
solde = tables.Column(accessor='note.balance') solde = tables.Column(accessor='note.balance')
class Meta: 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' template_name = 'django_tables2/bootstrap.html'
fields = ('last_name', 'first_name', 'username', 'email') fields = ('last_name', 'first_name', 'username', 'email')
model = User model = User

View File

@ -1,7 +1,4 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path

View File

@ -1,29 +1,26 @@
#!/usr/bin/env python # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _ 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.contrib.auth.models import User
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db.models import Q from django.db.models import Q
from django_tables2.views import SingleTableView from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token 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 note.models import Alias, Note, NoteUser
from .models import Profile, Club, Membership from .models import Profile, Club, Membership
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
from .tables import ClubTable, UserTable from .tables import ClubTable, UserTable
from .filters import UserFilter, UserFilterFormHelper from .filters import UserFilter, UserFilterFormHelper
from note.models.transactions import Transaction
from note.tables import HistoryTable
class UserCreateView(CreateView): class UserCreateView(CreateView):
""" """
Une vue pour inscrire un utilisateur et lui créer un profile Une vue pour inscrire un utilisateur et lui créer un profile
@ -49,17 +46,20 @@ class UserCreateView(CreateView):
profile.save() profile.save()
return super().form_valid(form) return super().form_valid(form)
class UserUpdateView(LoginRequiredMixin, UpdateView): class UserUpdateView(LoginRequiredMixin, UpdateView):
model = User model = User
fields = ['first_name', 'last_name', 'username', 'email'] fields = ['first_name', 'last_name', 'username', 'email']
template_name = 'member/profile_update.html' template_name = 'member/profile_update.html'
second_form = ProfileForm second_form = ProfileForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['user_modified'] = context['user'] context['user_modified'] = context['user']
context['user'] = self.request.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 return context
@ -71,21 +71,26 @@ class UserUpdateView(LoginRequiredMixin,UpdateView):
new_username = form.data['username'] new_username = form.data['username']
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant # 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: 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 return form
def form_valid(self, 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(): if form.is_valid() and profile_form.is_valid():
new_username = form.data['username'] new_username = form.data['username']
alias = Alias.objects.filter(name=new_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 # Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer
if not alias.exists(): 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(): if similar.exists():
similar.delete() similar.delete()
@ -98,17 +103,20 @@ class UserUpdateView(LoginRequiredMixin,UpdateView):
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
if kwargs: if kwargs:
return reverse_lazy('member:user_detail', kwargs = {'pk': kwargs['id']}) return reverse_lazy('member:user_detail',
kwargs={'pk': kwargs['id']})
else: 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 model = Profile
context_object_name = "profile" context_object_name = "profile"
def get_context_data(slef,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
user = context['profile'].user user = context['profile'].user
history_list = \ history_list = \
@ -119,6 +127,7 @@ class UserDetailView(LoginRequiredMixin,DetailView):
context['club_list'] = ClubTable(club_list) context['club_list'] = ClubTable(club_list)
return context return context
class UserListView(LoginRequiredMixin, SingleTableView): class UserListView(LoginRequiredMixin, SingleTableView):
""" """
Affiche la liste des utilisateurs, avec une fonction de recherche statique Affiche la liste des utilisateurs, avec une fonction de recherche statique
@ -149,22 +158,25 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
template_name = "member/manage_auth_tokens.html" template_name = "member/manage_auth_tokens.html"
def get(self, request, *args, **kwargs): 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() 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) return super().get(request, *args, **kwargs)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**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 return context
class UserAutocomplete(autocomplete.Select2QuerySetView): class UserAutocomplete(autocomplete.Select2QuerySetView):
""" """
Auto complete users by usernames Auto complete users by usernames
""" """
def get_queryset(self): 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. 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,9 +193,11 @@ class UserAutocomplete(autocomplete.Select2QuerySetView):
return qs return qs
###################################
############## CLUB ############### # ******************************* #
################################### # CLUB #
# ******************************* #
class ClubCreateView(LoginRequiredMixin, CreateView): class ClubCreateView(LoginRequiredMixin, CreateView):
""" """
@ -195,6 +209,7 @@ class ClubCreateView(LoginRequiredMixin,CreateView):
def form_valid(self, form): def form_valid(self, form):
return super().form_valid(form) return super().form_valid(form)
class ClubListView(LoginRequiredMixin, SingleTableView): class ClubListView(LoginRequiredMixin, SingleTableView):
""" """
List existing Clubs List existing Clubs
@ -202,6 +217,7 @@ class ClubListView(LoginRequiredMixin,SingleTableView):
model = Club model = Club
table_class = ClubTable table_class = ClubTable
class ClubDetailView(LoginRequiredMixin, DetailView): class ClubDetailView(LoginRequiredMixin, DetailView):
model = Club model = Club
context_object_name = "club" context_object_name = "club"
@ -218,10 +234,12 @@ class ClubDetailView(LoginRequiredMixin,DetailView):
context['member_list'] = club_member context['member_list'] = club_member
return context return context
class ClubAddMemberView(LoginRequiredMixin, CreateView): class ClubAddMemberView(LoginRequiredMixin, CreateView):
model = Membership model = Membership
form_class = MembershipForm form_class = MembershipForm
template_name = 'member/add_members.html' 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 = super().get_context_data(**kwargs)
context['formset'] = MemberFormSet() context['formset'] = MemberFormSet()
@ -229,11 +247,13 @@ class ClubAddMemberView(LoginRequiredMixin,CreateView):
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
formset = MembershipFormset(request.POST) return
if formset.is_valid(): # TODO: Implement POST
return self.form_valid(formset) # formset = MembershipFormset(request.POST)
else: # if formset.is_valid():
return self.form_invalid(formset) # return self.form_valid(formset)
# else:
# return self.form_invalid(formset)
def form_valid(self, formset): def form_valid(self, formset):
formset.save() formset.save()

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'note.apps.NoteConfig' default_app_config = 'note.apps.NoteConfig'

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin
@ -25,7 +24,10 @@ class NoteAdmin(PolymorphicParentModelAdmin):
Parent regrouping all note types as children Parent regrouping all note types as children
""" """
child_models = (NoteClub, NoteSpecial, NoteUser) child_models = (NoteClub, NoteSpecial, NoteUser)
list_filter = (PolymorphicChildModelFilter, 'is_active',) list_filter = (
PolymorphicChildModelFilter,
'is_active',
)
# Use a polymorphic list # Use a polymorphic list
list_display = ('pretty', 'balance', 'is_active') list_display = ('pretty', 'balance', 'is_active')
@ -49,6 +51,7 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
# We can't change club after creation or the balance # We can't change club after creation or the balance
readonly_fields = ('club', 'balance') readonly_fields = ('club', 'balance')
search_fields = ('club', ) search_fields = ('club', )
def has_add_permission(self, request): def has_add_permission(self, request):
""" """
A club note should not be manually added A club note should not be manually added
@ -101,7 +104,10 @@ class TransactionAdmin(admin.ModelAdmin):
list_display = ('created_at', 'poly_source', 'poly_destination', list_display = ('created_at', 'poly_source', 'poly_destination',
'quantity', 'amount', 'transaction_type', 'valid') 'quantity', 'amount', 'transaction_type', 'valid')
list_filter = ('transaction_type', 'valid') list_filter = ('transaction_type', 'valid')
autocomplete_fields = ('source', 'destination',) autocomplete_fields = (
'source',
'destination',
)
def poly_source(self, obj): def poly_source(self, obj):
""" """

View File

@ -1,12 +1,12 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # 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_framework import serializers
from rest_polymorphic.serializers import PolymorphicSerializer 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): class NoteSerializer(serializers.ModelSerializer):
""" """
@ -17,7 +17,10 @@ class NoteSerializer(serializers.ModelSerializer):
model = Note model = Note
fields = '__all__' fields = '__all__'
extra_kwargs = { 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 NoteSpecial: NoteSpecialSerializer
} }
class TransactionTemplateSerializer(serializers.ModelSerializer): class TransactionTemplateSerializer(serializers.ModelSerializer):
""" """
REST API Serializer for Transaction templates. REST API Serializer for Transaction templates.

View File

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

View File

@ -1,4 +1,3 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
@ -69,17 +68,20 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet):
queryset = Note.objects.all() queryset = Note.objects.all()
alias = self.request.query_params.get("alias", ".*") 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) note_type = self.request.query_params.get("type", None)
if note_type: if note_type:
l = str(note_type).lower() types = str(note_type).lower()
if "user" in l: if "user" in types:
queryset = queryset.filter(polymorphic_ctype__model="noteuser") queryset = queryset.filter(polymorphic_ctype__model="noteuser")
elif "club" in l: elif "club" in types:
queryset = queryset.filter(polymorphic_ctype__model="noteclub") queryset = queryset.filter(polymorphic_ctype__model="noteclub")
elif "special" in l: elif "special" in types:
queryset = queryset.filter(polymorphic_ctype__model="notespecial") queryset = queryset.filter(
polymorphic_ctype__model="notespecial")
else: else:
queryset = queryset.none() queryset = queryset.none()
@ -104,7 +106,8 @@ class AliasViewSet(viewsets.ModelViewSet):
queryset = Alias.objects.all() queryset = Alias.objects.all()
alias = self.request.query_params.get("alias", ".*") 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) note_id = self.request.query_params.get("note", None)
if note_id: if note_id:
@ -112,13 +115,16 @@ class AliasViewSet(viewsets.ModelViewSet):
note_type = self.request.query_params.get("type", None) note_type = self.request.query_params.get("type", None)
if note_type: if note_type:
l = str(note_type).lower() types = str(note_type).lower()
if "user" in l: if "user" in types:
queryset = queryset.filter(note__polymorphic_ctype__model="noteuser") queryset = queryset.filter(
elif "club" in l: note__polymorphic_ctype__model="noteuser")
queryset = queryset.filter(note__polymorphic_ctype__model="noteclub") elif "club" in types:
elif "special" in l: queryset = queryset.filter(
queryset = queryset.filter(note__polymorphic_ctype__model="notespecial") note__polymorphic_ctype__model="noteclub")
elif "special" in types:
queryset = queryset.filter(
note__polymorphic_ctype__model="notespecial")
else: else:
queryset = queryset.none() queryset = queryset.none()

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig from django.apps import AppConfig
@ -20,9 +19,9 @@ class NoteConfig(AppConfig):
""" """
post_save.connect( post_save.connect(
signals.save_user_note, signals.save_user_note,
sender=settings.AUTH_USER_MODEL sender=settings.AUTH_USER_MODEL,
) )
post_save.connect( post_save.connect(
signals.save_club_note, signals.save_club_note,
sender='member.Club' sender='member.Club',
) )

View File

@ -1,9 +1,12 @@
#!/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 django import forms
from .models import Transaction, TransactionTemplate from .models import Transaction, TransactionTemplate
class TransactionTemplateForm(forms.ModelForm): class TransactionTemplateForm(forms.ModelForm):
class Meta: class Meta:
model = TransactionTemplate model = TransactionTemplate
@ -15,11 +18,14 @@ class TransactionTemplateForm(forms.ModelForm):
# Pour force le type d'une note, il faut rajouter le paramètre : # 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} # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
widgets = { widgets = {
'destination': autocomplete.ModelSelect2(url='note:note_autocomplete', 'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={ attrs={
'data-placeholder': 'Note ...', 'data-placeholder': 'Note ...',
'data-minimum-input-length': 1, 'data-minimum-input-length': 1,
}), },
),
} }
@ -31,26 +37,38 @@ class TransactionForm(forms.ModelForm):
class Meta: class Meta:
model = Transaction model = Transaction
fields = ('source', 'destination', 'reason', 'amount',) fields = (
'source',
'destination',
'reason',
'amount',
)
# Voir ci-dessus # Voir ci-dessus
widgets = { widgets = {
'source': autocomplete.ModelSelect2(url='note:note_autocomplete', 'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={ attrs={
'data-placeholder': 'Note ...', 'data-placeholder': 'Note ...',
'data-minimum-input-length': 1, 'data-minimum-input-length': 1,
},), },
'destination': autocomplete.ModelSelect2(url='note:note_autocomplete', ),
'destination':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={ attrs={
'data-placeholder': 'Note ...', 'data-placeholder': 'Note ...',
'data-minimum-input-length': 1, 'data-minimum-input-length': 1,
},), },
),
} }
class ConsoForm(forms.ModelForm):
class ConsoForm(forms.ModelForm):
def save(self, commit=True): 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.destination = button.destination
self.instance.amount = button.amount self.instance.amount = button.amount
self.instance.transaction_type = 'bouton' self.instance.transaction_type = 'bouton'
@ -65,9 +83,12 @@ class ConsoForm(forms.ModelForm):
# Quand des lettres sont tapées, une requête est envoyée sur l'API 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 # et récupère les aliases de note valides
widgets = { widgets = {
'source': autocomplete.ModelSelect2(url='note:note_autocomplete', 'source':
autocomplete.ModelSelect2(
url='note:note_autocomplete',
attrs={ attrs={
'data-placeholder': 'Note ...', 'data-placeholder': 'Note ...',
'data-minimum-input-length': 1, 'data-minimum-input-length': 1,
}), },
),
} }

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import unicodedata import unicodedata
@ -10,7 +9,6 @@ from django.core.validators import RegexValidator
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
""" """
Defines each note types Defines each note types
""" """
@ -34,8 +32,7 @@ class Note(PolymorphicModel):
default=True, default=True,
help_text=_( help_text=_(
'Designates whether this note should be treated as active. ' '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( display_image = models.ImageField(
verbose_name=_('display image'), verbose_name=_('display image'),
@ -85,7 +82,8 @@ class Note(PolymorphicModel):
""" """
Verify alias (simulate save) 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(): if aliases.exists():
# Alias exists, so check if it is linked to this note # Alias exists, so check if it is linked to this note
if aliases.first().note != self: if aliases.first().note != self:
@ -181,15 +179,15 @@ class Alias(models.Model):
validators=[ validators=[
RegexValidator( RegexValidator(
regex=settings.ALIAS_VALIDATOR_REGEX, 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( normalized_name = models.CharField(
max_length=255, max_length=255,
unique=True, unique=True,
default='', default='',
editable=False editable=False,
) )
note = models.ForeignKey( note = models.ForeignKey(
Note, Note,
@ -209,11 +207,9 @@ class Alias(models.Model):
Normalizes a string: removes most diacritics and does casefolding Normalizes a string: removes most diacritics and does casefolding
""" """
return ''.join( return ''.join(
char char for char in unicodedata.normalize('NFKD', string.casefold())
for char in unicodedata.normalize('NFKD', string.casefold())
if all(not unicodedata.category(char).startswith(cat) if all(not unicodedata.category(char).startswith(cat)
for cat in {'M', 'P', 'Z', 'C'}) for cat in {'M', 'P', 'Z', 'C'})).casefold()
).casefold()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" """
@ -229,7 +225,8 @@ class Alias(models.Model):
raise ValidationError(_('Alias too long.')) raise ValidationError(_('Alias too long.'))
try: try:
if self != Alias.objects.get(normalized_name=normalized_name): if self != Alias.objects.get(normalized_name=normalized_name):
raise ValidationError(_('An alias with a similar name ' raise ValidationError(
_('An alias with a similar name '
'already exists.')) 'already exists.'))
except Alias.DoesNotExist: except Alias.DoesNotExist:
pass pass

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.db import models from django.db import models
@ -13,6 +12,7 @@ from .notes import Note,NoteClub
Defines transactions Defines transactions
""" """
class TransactionCategory(models.Model): class TransactionCategory(models.Model):
""" """
Defined a recurrent transaction category Defined a recurrent transaction category
@ -32,6 +32,7 @@ class TransactionCategory(models.Model):
def __str__(self): def __str__(self):
return str(self.name) return str(self.name)
class TransactionTemplate(models.Model): class TransactionTemplate(models.Model):
""" """
Defined a recurrent transaction Defined a recurrent transaction
@ -57,7 +58,7 @@ class TransactionTemplate(models.Model):
TransactionCategory, TransactionCategory,
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_('type'), verbose_name=_('type'),
max_length=31 max_length=31,
) )
class Meta: class Meta:
@ -98,9 +99,7 @@ class Transaction(models.Model):
verbose_name=_('quantity'), verbose_name=_('quantity'),
default=1, default=1,
) )
amount = models.PositiveIntegerField( amount = models.PositiveIntegerField(verbose_name=_('amount'), )
verbose_name=_('amount'),
)
transaction_type = models.CharField( transaction_type = models.CharField(
verbose_name=_('type'), verbose_name=_('type'),
max_length=31, max_length=31,

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -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 import django_tables2 as tables
from django.db.models import F from django.db.models import F
@ -7,16 +9,18 @@ from .models.transactions import Transaction
class HistoryTable(tables.Table): class HistoryTable(tables.Table):
class Meta: 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 model = Transaction
template_name = 'django_tables2/bootstrap.html' 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 # needed for rendering
QuerySet = QuerySet.annotate( queryset = queryset.annotate(total=F('amount') * F('quantity')) \
total=F('amount') * F('quantity') .order_by(('-' if is_descending else '') + 'total')
).order_by(('-' if is_descending else '') + 'total') return (queryset, True)
return (QuerySet, True)

View File

@ -1,11 +1,21 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django import template from django import template
def pretty_money(value): def pretty_money(value):
if value % 100 == 0: if value % 100 == 0:
return "{:s}{:d}".format("- " if value < 0 else "", abs(value) // 100) return "{:s}{:d}".format(
"- " if value < 0 else "",
abs(value) // 100,
)
else: 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() register = template.Library()

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path from django.urls import path

View File

@ -1,17 +1,17 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q 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.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 from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
class TransactionCreate(LoginRequiredMixin, CreateView): class TransactionCreate(LoginRequiredMixin, CreateView):
""" """
Show transfer page Show transfer page
@ -30,7 +30,6 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
'to one or others') 'to one or others')
return context return context
def get_form(self, form_class=None): 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. If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
@ -56,7 +55,6 @@ class NoteAutocomplete(autocomplete.Select2QuerySetView):
""" """
Auto complete note by aliases Auto complete note by aliases
""" """
def get_queryset(self): 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. 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) # Filtrage par type de note (user, club, special)
note_type = self.forwarded.get("note_type", None) note_type = self.forwarded.get("note_type", None)
if note_type: if note_type:
l = str(note_type).lower() types = str(note_type).lower()
if "user" in l: if "user" in types:
qs = qs.filter(note__polymorphic_ctype__model="noteuser") qs = qs.filter(note__polymorphic_ctype__model="noteuser")
elif "club" in l: elif "club" in types:
qs = qs.filter(note__polymorphic_ctype__model="noteclub") qs = qs.filter(note__polymorphic_ctype__model="noteclub")
elif "special" in l: elif "special" in types:
qs = qs.filter(note__polymorphic_ctype__model="notespecial") qs = qs.filter(note__polymorphic_ctype__model="notespecial")
else: else:
qs = qs.none() qs = qs.none()
@ -108,6 +106,7 @@ class TransactionTemplateCreateView(LoginRequiredMixin,CreateView):
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
class TransactionTemplateListView(LoginRequiredMixin, ListView): class TransactionTemplateListView(LoginRequiredMixin, ListView):
""" """
List TransactionsTemplates List TransactionsTemplates
@ -115,12 +114,14 @@ class TransactionTemplateListView(LoginRequiredMixin,ListView):
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
""" """
""" """
model = TransactionTemplate model = TransactionTemplate
form_class = TransactionTemplateForm form_class = TransactionTemplateForm
class ConsoView(LoginRequiredMixin, CreateView): class ConsoView(LoginRequiredMixin, CreateView):
""" """
Consume Consume
@ -139,11 +140,14 @@ class ConsoView(LoginRequiredMixin,CreateView):
if 'template_type' not in self.kwargs.keys(): if 'template_type' not in self.kwargs.keys():
return context return context
template_type = TransactionCategory.objects.filter(name=self.kwargs.get('template_type')).get() template_type = TransactionCategory.objects.filter(
context['buttons'] = TransactionTemplate.objects.filter(template_type=template_type) name=self.kwargs.get('template_type')).get()
context['buttons'] = TransactionTemplate.objects.filter(
template_type=template_type)
context['title'] = template_type context['title'] = template_type
return context return context
def get_success_url(self): 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'), ))

View File

@ -1,6 +1,11 @@
#!/bin/bash #!/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 compilemessages
python manage.py makemigrations python manage.py makemigrations
# Wait for database
sleep 5 sleep 5
python manage.py migrate python manage.py migrate

View File

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

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import os import os

View File

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

View File

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

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*- # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin from django.contrib import admin

View File

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

View File

@ -1,7 +1,7 @@
certifi==2019.6.16 certifi==2019.6.16
chardet==3.0.4 chardet==3.0.4
defusedxml==0.6.0 defusedxml==0.6.0
Django==2.2.3 Django~=2.2
django-allauth==0.39.1 django-allauth==0.39.1
django-autocomplete-light==3.3.0 django-autocomplete-light==3.3.0
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2

22
tox.ini
View File

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