Commit 2ca271bf authored by Maël Kervella's avatar Maël Kervella

Pylint compliance on re2o

parent 332e8a34
......@@ -20,3 +20,15 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""re2o
The main app of Re2o. In charge of all the basics elements which are not
specific to anyother apps. It includes :
* Templates used in multiple places
* Templatetags used in multiple places
* ACL base
* Mixins base
* Settings for the Django project
* The login part
* Some utility scripts
* ...
"""
......@@ -33,14 +33,6 @@ from django.contrib import messages
from django.shortcuts import redirect
from django.urls import reverse
import cotisations
import logs
import machines
import preferences
import search
import topologie
import users
def can_create(model):
"""Decorator to check if an user can create a model.
......@@ -49,7 +41,11 @@ def can_create(model):
of models.
"""
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
can, msg = model.can_create(request.user, *args, **kwargs)
if not can:
messages.error(
......@@ -68,30 +64,36 @@ def can_edit(model, *field_list):
kind of models.
"""
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
try:
instance = model.get_instance(*args, **kwargs)
except model.DoesNotExist:
messages.error(request, u"Entrée inexistante")
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
can, msg = instance.can_edit(request.user)
if not can:
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
for field in field_list:
can_change = getattr(instance, 'can_change_' + field)
can, msg = can_change(request.user, *args, **kwargs)
can_change_fct = getattr(instance, 'can_change_' + field)
can, msg = can_change_fct(request.user, *args, **kwargs)
if not can:
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('users:profil',
kwargs={'userid': str(
request.user.id)}
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return view(request, instance, *args, **kwargs)
return wrapper
......@@ -103,16 +105,20 @@ def can_change(model, *field_list):
Difference with can_edit : take a class and not an instance
"""
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
for field in field_list:
can_change = getattr(model, 'can_change_' + field)
can, msg = can_change(request.user, *args, **kwargs)
can_change_fct = getattr(model, 'can_change_' + field)
can, msg = can_change_fct(request.user, *args, **kwargs)
if not can:
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('users:profil',
kwargs={'userid': str(
request.user.id)}
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return view(request, *args, **kwargs)
return wrapper
......@@ -127,19 +133,25 @@ def can_delete(model):
kind of models.
"""
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
try:
instance = model.get_instance(*args, **kwargs)
except model.DoesNotExist:
messages.error(request, u"Entrée inexistante")
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
can, msg = instance.can_delete(request.user)
if not can:
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return view(request, instance, *args, **kwargs)
......@@ -151,7 +163,11 @@ def can_delete_set(model):
"""Decorator which returns a list of detable models by request user.
If none of them, return an error"""
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
all_objects = model.objects.all()
instances_id = []
for instance in all_objects:
......@@ -160,8 +176,10 @@ def can_delete_set(model):
instances_id.append(instance.id)
instances = model.objects.filter(id__in=instances_id)
if not instances:
messages.error(request, "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('users:profil',
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return view(request, instances, *args, **kwargs)
......@@ -177,19 +195,25 @@ def can_view(model):
kind of models.
"""
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
try:
instance = model.get_instance(*args, **kwargs)
except model.DoesNotExist:
messages.error(request, u"Entrée inexistante")
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
can, msg = instance.can_view(request.user)
if not can:
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return view(request, instance, *args, **kwargs)
......@@ -201,12 +225,17 @@ def can_view_all(model):
"""Decorator to check if an user can view a class of model.
"""
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
can, msg = model.can_view_all(request.user)
if not can:
messages.error(
request, msg or "Vous ne pouvez pas accéder à ce menu")
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return view(request, *args, **kwargs)
......@@ -220,13 +249,18 @@ def can_view_app(app_name):
assert app_name in sys.modules.keys()
def decorator(view):
"""The decorator to use on a specific view
"""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
app = sys.modules[app_name]
can, msg = app.can_view(request.user)
if can:
return view(request, *args, **kwargs)
messages.error(request, msg)
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return wrapper
......@@ -236,13 +270,16 @@ def can_view_app(app_name):
def can_edit_history(view):
"""Decorator to check if an user can edit history."""
def wrapper(request, *args, **kwargs):
"""The wrapper used for a specific request
"""
if request.user.has_perm('admin.change_logentry'):
return view(request, *args, **kwargs)
messages.error(
request,
"Vous ne pouvez pas éditer l'historique."
)
return redirect(reverse('users:profil',
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
return wrapper
#!/usr/bin/env python3
"""re2o.contributors
A list of the proud contributors to Re2o
"""
CONTRIBUTORS = [
'Gabriel "Chirac" Détraz',
......
from django.db import models
from django import forms
from functools import partial
# -*- mode: python; coding: utf-8 -*-
# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Goulven Kermarec
# Copyright © 2017 Augustin Lemesle
# Copyright © 2018 Maël Kervella
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""re2o.field_permissions
A model mixin and a field mixin used to remove some unauthorized fields
from the form automatically generated from the model. The model must
subclass `FieldPermissionModelMixin` and the form must subclass
`FieldPermissionFieldMixin` so when a Django form is generated from the
fields of the models, some fields will be removed if the user don't have
the rights to change them (can_change_{name})
"""
class FieldPermissionModelMixin:
""" The model mixin. Defines the `has_field_perm` function """
field_permissions = {} # {'field_name': callable}
FIELD_PERM_CODENAME = 'can_change_{model}_{name}'
FIELD_PERMISSION_GETTER = 'can_change_{name}'
FIELD_PERMISSION_MISSING_DEFAULT = True
def has_field_perm(self, user, field):
""" Checks if a `user` has the right to edit the `field`
of this model """
if field in self.field_permissions:
checks = self.field_permissions[field]
if not isinstance(checks, (list, tuple)):
......@@ -39,7 +69,7 @@ class FieldPermissionModelMixin:
# Try to find a user setting that qualifies them for permission.
for perm in checks:
if callable(perm):
result, reason = perm(user_request=user)
result, _reason = perm(user_request=user)
if result is not None:
return result
else:
......@@ -52,11 +82,6 @@ class FieldPermissionModelMixin:
return False
class FieldPermissionModel(FieldPermissionModelMixin, models.Model):
class Meta:
abstract = True
class FieldPermissionFormMixin:
"""
Construit le formulaire et retire les champs interdits
......@@ -73,8 +98,5 @@ class FieldPermissionFormMixin:
self.remove_unauthorized_field(name)
def remove_unauthorized_field(self, name):
""" Remove one field from the fields of the form """
del self.fields[name]
class FieldPermissionForm(FieldPermissionFormMixin, forms.ModelForm):
pass
......@@ -24,7 +24,9 @@
# -*- coding: utf-8 -*-
# Module d'authentification
# David Sinquin, Gabriel Détraz, Goulven Kermarec
"""re2o.login
Module in charge of handling the login process and verifications
"""
import hashlib
import binascii
......@@ -42,6 +44,7 @@ DIGEST_LEN = 20
def makeSecret(password):
""" Build a hashed and salted version of the password """
salt = os.urandom(4)
h = hashlib.sha1(password.encode())
h.update(salt)
......@@ -49,11 +52,13 @@ def makeSecret(password):
def hashNT(password):
hash = hashlib.new('md4', password.encode('utf-16le')).digest()
return binascii.hexlify(hash).upper()
""" Build a md4 hash of the password to use as the NT-password """
hash_str = hashlib.new('md4', password.encode('utf-16le')).digest()
return binascii.hexlify(hash_str).upper()
def checkPassword(challenge_password, password):
""" Check if a given password match the hash of a stored password """
challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode())
digest = challenge_bytes[:DIGEST_LEN]
salt = challenge_bytes[DIGEST_LEN:]
......@@ -74,7 +79,7 @@ class SSHAPasswordHasher(hashers.BasePasswordHasher):
algorithm = ALGO_NAME
def encode(self, password, salt, iterations=None):
def encode(self, password, salt):
"""
Hash and salt the given password using SSHA algorithm
......@@ -92,16 +97,16 @@ class SSHAPasswordHasher(hashers.BasePasswordHasher):
def safe_summary(self, encoded):
"""
Provides a safe summary ofthe password
Provides a safe summary of the password
"""
assert encoded.startswith(self.algorithm)
hash = encoded[ALGO_LEN:]
hash = binascii.hexlify(decodestring(hash.encode())).decode()
hash_str = encoded[ALGO_LEN:]
hash_str = binascii.hexlify(decodestring(hash_str.encode())).decode()
return OrderedDict([
('algorithm', self.algorithm),
('iterations', 0),
('salt', hashers.mask_hash(hash[2*DIGEST_LEN:], show=2)),
('hash', hashers.mask_hash(hash[:2*DIGEST_LEN])),
('salt', hashers.mask_hash(hash_str[2*DIGEST_LEN:], show=2)),
('hash', hashers.mask_hash(hash_str[:2*DIGEST_LEN])),
])
def harden_runtime(self, password, encoded):
......
......@@ -24,11 +24,12 @@ Write in a python file the list of all contributors sorted by number of
commits. This list is extracted from the current gitlab repository.
"""
from django.core.management.base import BaseCommand, CommandError
import os
from django.core.management.base import BaseCommand
class Command(BaseCommand):
""" The command object for `gen_contrib` """
help = 'Update contributors list'
def handle(self, *args, **options):
......@@ -39,6 +40,8 @@ class Command(BaseCommand):
]
self.stdout.write(self.style.SUCCESS("Exportation Sucessfull"))
with open("re2o/contributors.py", "w") as contrib_file:
contrib_file.write("#!/usr/bin/env python3\n")
contrib_file.write("\"\"\"re2o.contributors\n")
contrib_file.write("A list of the proud contributors to Re2o\n")
contrib_file.write("\"\"\"\n")
contrib_file.write("\n")
contrib_file.write("CONTRIBUTORS = " + str(contributeurs))
......@@ -19,23 +19,34 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""re2o.mixins
A set of mixins used all over the project to avoid duplicating code
"""
from reversion import revisions as reversion
class RevMixin(object):
""" A mixin to subclass the save and delete function of a model
to enforce the versioning of the object before those actions
really happen """
def save(self, *args, **kwargs):
""" Creates a version of this object and save it to database """
if self.pk is None:
reversion.set_comment("Création")
return super(RevMixin, self).save(*args, **kwargs)
def delete(self, *args, **kwargs):
""" Creates a version of this object and delete it from database """
reversion.set_comment("Suppresion")
return super(RevMixin, self).delete(*args, **kwargs)
class FormRevMixin(object):
""" A mixin to subclass the save function of a form
to enforce the versionning of the object before it is really edited """
def save(self, *args, **kwargs):
""" Create a version of this object and save it to database """
if reversion.get_comment() != "" and self.changed_data != []:
reversion.set_comment(
reversion.get_comment() + ",%s"
......@@ -66,14 +77,16 @@ class AclMixin(object):
@classmethod
def get_classname(cls):
""" Returns the name of the class where this mixin is used """
return str(cls.__name__).lower()
@classmethod
def get_modulename(cls):
""" Returns the name of the module where this mixin is used """
return str(cls.__module__).split('.')[0].lower()
@classmethod
def get_instance(cls, *args, **kwargs):
def get_instance(cls, *_args, **kwargs):
"""Récupère une instance
:param objectid: Instance id à trouver
:return: Une instance de la classe évidemment"""
......@@ -81,7 +94,7 @@ class AclMixin(object):
return cls.objects.get(pk=object_id)
@classmethod
def can_create(cls, user_request, *args, **kwargs):
def can_create(cls, user_request, *_args, **_kwargs):
"""Verifie que l'user a les bons droits pour créer
un object
:param user_request: instance utilisateur qui fait la requête
......@@ -93,7 +106,7 @@ class AclMixin(object):
u"Vous n'avez pas le droit de créer un " + cls.get_classname()
)
def can_edit(self, user_request, *args, **kwargs):
def can_edit(self, user_request, *_args, **_kwargs):
"""Verifie que l'user a les bons droits pour editer
cette instance
:param self: Instance à editer
......@@ -106,7 +119,7 @@ class AclMixin(object):
u"Vous n'avez pas le droit d'éditer des " + self.get_classname()
)
def can_delete(self, user_request, *args, **kwargs):
def can_delete(self, user_request, *_args, **_kwargs):
"""Verifie que l'user a les bons droits pour delete
cette instance
:param self: Instance à delete
......@@ -120,7 +133,7 @@ class AclMixin(object):
)
@classmethod
def can_view_all(cls, user_request, *args, **kwargs):
def can_view_all(cls, user_request, *_args, **_kwargs):
"""Vérifie qu'on peut bien afficher l'ensemble des objets,
droit particulier view objet correspondant
:param user_request: instance user qui fait l'edition
......@@ -132,7 +145,7 @@ class AclMixin(object):
u"Vous n'avez pas le droit de voir des " + cls.get_classname()
)
def can_view(self, user_request, *args, **kwargs):
def can_view(self, user_request, *_args, **_kwargs):
"""Vérifie qu'on peut bien voir cette instance particulière avec
droit view objet
:param self: instance à voir
......
......@@ -18,22 +18,27 @@
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""re2o.script_utils
A set of utility scripts that can be used as standalone to interact easily
with Re2o throught the CLI
"""
import os
from os.path import dirname
import sys
import pwd
from django.core.wsgi import get_wsgi_application
from getpass import getpass
from reversion import revisions as reversion
from django.core.wsgi import get_wsgi_application
from django.core.management.base import CommandError
from users.models import User
from django.utils.html import strip_tags
from reversion import revisions as reversion
from django.db import transaction
from getpass import getpass
from django.utils.html import strip_tags
from users.models import User
proj_path = "/var/www/re2o"
proj_path = dirname(dirname(__file__))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings")
sys.path.append(proj_path)
os.chdir(proj_path)
......
......@@ -35,38 +35,37 @@ https://docs.djangoproject.com/en/1.8/ref/settings/
from __future__ import unicode_literals
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
from .settings_local import *
# The root directory for the project
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# Auth definition
PASSWORD_HASHERS = (
're2o.login.SSHAPasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
)
AUTH_USER_MODEL = 'users.User'
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'
AUTH_USER_MODEL = 'users.User' # The class to use for authentication
LOGIN_URL = '/login/' # The URL for login page
LOGIN_REDIRECT_URL = '/' # The URL for redirecting after login
# Application definition
INSTALLED_APPS = (
DJANGO_CONTRIB_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
)
EXTERNAL_CONTRIB_APPS = (
'bootstrap3',
'rest_framework',
'reversion',
)
LOCAL_APPS = (
'users',
'machines',
'cotisations',
......@@ -75,11 +74,14 @@ INSTALLED_APPS = (
're2o',
'preferences',
'logs',
'rest_framework',
'reversion',
'api'
) + OPTIONNAL_APPS
'api',
)
INSTALLED_APPS = (
DJANGO_CONTRIB_APPS +
EXTERNAL_CONTRIB_APPS +
LOCAL_APPS +
OPTIONNAL_APPS
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
......@@ -93,12 +95,15 @@ MIDDLEWARE_CLASSES = (
'reversion.middleware.RevisionMiddleware',
)
# The root url module to define the project URLs
ROOT_URLCONF = 're2o.urls'
# The templates configuration (see Django documentation)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
# Use only absolute paths with '/' delimiters even on Windows
os.path.join(BASE_DIR, 'templates').replace('\\', '/'),
],
'APP_DIRS': True,
......@@ -115,56 +120,49 @@ TEMPLATES = [
},
]
# The WSGI module to use in a server environment
WSGI_APPLICATION = 're2o.wsgi.application'
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'en'
USE_I18N = True
USE_L10N = True
# Proritary location search for translations
# then searches in {app}/locale/ for app in INSTALLED_APPS
# Use only absolute paths with '/' delimiters even on Windows
LOCALE_PATHS = [
BASE_DIR + '/templates/locale/' # For translations outside of apps
# For translations outside of apps
os.path.join(BASE_DIR, 'templates', 'locale').repla