Commit a8dbe462 authored by Hugo LEVY-FALK's avatar Hugo LEVY-FALK

Release : 2.7

parents ef4e430e 833d7177
Pipeline #955 failed with stage
settings_local.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.swp
*.pyc
# Translations
*.mo
*.pot
# Django stuff
*.log
local_settings.py
db.sqlite3
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# PyCharm project settings
.idea/
# Django statics
static_files/
static/logo/
# re2o specific
settings_local.py
re2o.png
__pycache__/*
static_files/*
static/logo/*
media/*
media/
......@@ -150,3 +150,31 @@ On some database engines (postgreSQL) you also need to update the id sequences:
```bash
python3 manage.py sqlsequencereset cotisations | python3 manage.py dbshell
```
## MR 296: Frontend changes
Install fonts-font-awesome
```bash
apt-get -y install fonts-font-awesome
```
Collec new statics
```bash
python3 manage.py collectstatic
```
## MR 391: Document templates and subscription vouchers
Re2o can now use templates for generated invoices. To load default templates run
```bash
./install update
```
Be carefull, you need the proper rights to edit a DocumentTemplate.
Re2o now sends subscription voucher when an invoice is controlled. It uses one
of the templates. You also need to set the name of the president of your association
to be set in your settings.
# Re2o
Gnu public license v2.0
GNU public license v2.0
## Avant propos
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
Re2o est un logiciel d'administration développé initialement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
......@@ -31,15 +31,15 @@ Pour cela :
## Fonctionnement général
Re2o est séparé entre les models, qui sont visible sur le schéma des
dépendances. Il s'agit en réalité des tables sql, et les fields etant les
Re2o est séparé entre les models, qui sont visibles sur le schéma des
dépendances. Il s'agit en réalité des tables sql, et les fields étant les
colonnes.
Ceci dit il n'est jamais nécessaire de toucher directement au sql, django
procédant automatiquement à tout cela.
On crée donc différents models (user, right pour les droits des users,
interfaces, IpList pour l'ensemble des adresses ip, etc)
Du coté des forms, il s'agit des formulaire d'édition des models. Il
Du coté des forms, il s'agit des formulaires d'édition des models. Il
s'agit de ModelForms django, qui héritent des models très simplement, voir la
documentation django models forms.
......@@ -56,12 +56,20 @@ d'accéder à ces vues, utilisé par re2o-tools.
# Requète en base de donnée
Pour avoir un shell, il suffit de lancer '''python3 manage.py shell'''
Pour charger des objets, example avec User, faire :
''' from users.models import User'''
Pour charger les objets django, il suffit de faire User.objects.all()
pour tous les users par exemple.
Il est ensuite aisé de faire des requètes, par exemple
User.objects.filter(pseudo='test')
Des exemples et la documentation complète sur les requètes django sont
Pour avoir un shell, lancer :
```.bash
python3 manage.py shell
```
Pour charger des objets (exemple avec User), faire :
```.python
from users.models import User
```
Pour charger les objets django, il suffit de faire `User.objects.all()`
pour tous les users par exemple.
Il est ensuite aisé de faire des requêtes, par exemple
`User.objects.filter(pseudo='test')`
Des exemples et la documentation complète sur les requêtes django sont
disponible sur le site officiel.
......@@ -26,9 +26,9 @@ done.
"""
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission
from django.utils.translation import ugettext_lazy as _
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _
def _create_api_permission():
......@@ -71,4 +71,5 @@ def can_view(user):
'codename': settings.API_PERMISSION_CODENAME
}
can = user.has_perm('%(app_label)s.%(codename)s' % kwargs)
return can, None if can else _("You cannot see this application.")
return can, None if can else _("You don't have the right to see this"
" application.")
......@@ -26,12 +26,14 @@ import datetime
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
class ExpiringTokenAuthentication(TokenAuthentication):
"""Authenticate a user if the provided token is valid and not expired.
"""
def authenticate_credentials(self, key):
"""See base class. Add the verification the token is not expired.
"""
......@@ -44,6 +46,6 @@ class ExpiringTokenAuthentication(TokenAuthentication):
)
utc_now = datetime.datetime.now(datetime.timezone.utc)
if token.created < utc_now - token_duration:
raise exceptions.AuthenticationFailed(_('Token has expired'))
raise exceptions.AuthenticationFailed(_("The token has expired."))
return (token.user, token)
return token.user, token
# 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 © 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.
msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-01-08 23:06+0100\n"
"PO-Revision-Date: 2019-01-07 01:37+0100\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: acl.py:74
msgid "You don't have the right to see this application."
msgstr "Vous n'avez pas le droit de voir cette application."
#: authentication.py:49
msgid "The token has expired."
msgstr "Le jeton a expiré."
......@@ -24,8 +24,6 @@
from rest_framework import permissions, exceptions
from re2o.acl import can_create, can_edit, can_delete, can_view_all
from . import acl
......@@ -57,14 +55,14 @@ def _get_param_in_view(view, param_name):
AssertionError: None of the getter function or the attribute are
defined in the view.
"""
assert hasattr(view, 'get_'+param_name) \
or getattr(view, param_name, None) is not None, (
assert hasattr(view, 'get_' + param_name) \
or getattr(view, param_name, None) is not None, (
'cannot apply {} on a view that does not set '
'`.{}` or have a `.get_{}()` method.'
).format(self.__class__.__name__, param_name, param_name)
if hasattr(view, 'get_'+param_name):
param = getattr(view, 'get_'+param_name)()
if hasattr(view, 'get_' + param_name):
param = getattr(view, 'get_' + param_name)()
assert param is not None, (
'{}.get_{}() returned None'
).format(view.__class__.__name__, param_name)
......@@ -80,7 +78,8 @@ class ACLPermission(permissions.BasePermission):
See the wiki for the syntax of this attribute.
"""
def get_required_permissions(self, method, view):
@staticmethod
def get_required_permissions(method, view):
"""Build the list of permissions required for the request to be
accepted.
......@@ -153,15 +152,15 @@ class AutodetectACLPermission(permissions.BasePermission):
'OPTIONS': [can_see_api, lambda model: model.can_view_all],
'HEAD': [can_see_api, lambda model: model.can_view_all],
'POST': [can_see_api, lambda model: model.can_create],
'PUT': [], # No restrictions, apply to objects
'PATCH': [], # No restrictions, apply to objects
'PUT': [], # No restrictions, apply to objects
'PATCH': [], # No restrictions, apply to objects
'DELETE': [], # No restrictions, apply to objects
}
perms_obj_map = {
'GET': [can_see_api, lambda obj: obj.can_view],
'OPTIONS': [can_see_api, lambda obj: obj.can_view],
'HEAD': [can_see_api, lambda obj: obj.can_view],
'POST': [], # No restrictions, apply to models
'POST': [], # No restrictions, apply to models
'PUT': [can_see_api, lambda obj: obj.can_edit],
'PATCH': [can_see_api, lambda obj: obj.can_edit],
'DELETE': [can_see_api, lambda obj: obj.can_delete],
......@@ -209,7 +208,8 @@ class AutodetectACLPermission(permissions.BasePermission):
return [perm(obj) for perm in self.perms_obj_map[method]]
def _queryset(self, view):
@staticmethod
def _queryset(view):
return _get_param_in_view(view, 'queryset')
def has_permission(self, request, view):
......@@ -282,4 +282,3 @@ class AutodetectACLPermission(permissions.BasePermission):
return False
return True
......@@ -24,12 +24,12 @@
from collections import OrderedDict
from django.conf.urls import url, include
from django.conf.urls import url
from django.core.urlresolvers import NoReverseMatch
from rest_framework import views
from rest_framework.routers import DefaultRouter
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.routers import DefaultRouter
from rest_framework.schemas import SchemaGenerator
from rest_framework.settings import api_settings
......@@ -64,7 +64,8 @@ class AllViewsRouter(DefaultRouter):
name = self.get_default_name(pattern)
self.view_registry.append((pattern, view, name))
def get_default_name(self, pattern):
@staticmethod
def get_default_name(pattern):
"""Returns the name to use for the route if none was specified.
Args:
......@@ -113,7 +114,8 @@ class AllViewsRouter(DefaultRouter):
_ignore_model_permissions = True
renderer_classes = view_renderers
def get(self, request, *args, **kwargs):
@staticmethod
def get(request, *args, **kwargs):
if request.accepted_renderer.media_type in schema_media_types:
# Return a schema response.
schema = schema_generator.get_schema(request)
......
......@@ -30,7 +30,6 @@ import preferences.models as preferences
import topologie.models as topologie
import users.models as users
# The namespace used for the API. It must match the namespace used in the
# urlpatterns to include the API URLs.
API_NAMESPACE = 'api'
......@@ -40,6 +39,7 @@ class NamespacedHRField(serializers.HyperlinkedRelatedField):
"""A `rest_framework.serializers.HyperlinkedRelatedField` subclass to
automatically prefix view names with the API namespace.
"""
def __init__(self, view_name=None, **kwargs):
if view_name is not None:
view_name = '%s:%s' % (API_NAMESPACE, view_name)
......@@ -50,6 +50,7 @@ class NamespacedHIField(serializers.HyperlinkedIdentityField):
"""A `rest_framework.serializers.HyperlinkedIdentityField` subclass to
automatically prefix view names with teh API namespace.
"""
def __init__(self, view_name=None, **kwargs):
if view_name is not None:
view_name = '%s:%s' % (API_NAMESPACE, view_name)
......@@ -70,24 +71,33 @@ class NamespacedHMSerializer(serializers.HyperlinkedModelSerializer):
class FactureSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Facture` objects.
"""
class Meta:
model = cotisations.Facture
fields = ('user', 'paiement', 'banque', 'cheque', 'date', 'valid',
'control', 'prix_total', 'name', 'api_url')
class BaseInvoiceSerializer(NamespacedHMSerializer):
class Meta:
model = cotisations.BaseInvoice
fields = ('__all__')
class VenteSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Vente` objects.
"""
class Meta:
model = cotisations.Vente
fields = ('facture', 'number', 'name', 'prix', 'duration',
fields = ('facture',
'number', 'name', 'prix', 'duration',
'type_cotisation', 'prix_total', 'api_url')
class ArticleSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Article` objects.
"""
class Meta:
model = cotisations.Article
fields = ('name', 'prix', 'duration', 'type_user',
......@@ -97,6 +107,7 @@ class ArticleSerializer(NamespacedHMSerializer):
class BanqueSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Banque` objects.
"""
class Meta:
model = cotisations.Banque
fields = ('name', 'api_url')
......@@ -105,14 +116,16 @@ class BanqueSerializer(NamespacedHMSerializer):
class PaiementSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Paiement` objects.
"""
class Meta:
model = cotisations.Paiement
fields = ('moyen', 'type_paiement', 'api_url')
fields = ('moyen', 'api_url')
class CotisationSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Cotisation` objects.
"""
class Meta:
model = cotisations.Cotisation
fields = ('vente', 'type_cotisation', 'date_start', 'date_end',
......@@ -125,6 +138,7 @@ class CotisationSerializer(NamespacedHMSerializer):
class MachineSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Machine` objects.
"""
class Meta:
model = machines.Machine
fields = ('user', 'name', 'active', 'api_url')
......@@ -133,6 +147,7 @@ class MachineSerializer(NamespacedHMSerializer):
class MachineTypeSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.MachineType` objects.
"""
class Meta:
model = machines.MachineType
fields = ('type', 'ip_type', 'api_url')
......@@ -141,6 +156,7 @@ class MachineTypeSerializer(NamespacedHMSerializer):
class IpTypeSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.IpType` objects.
"""
class Meta:
model = machines.IpType
fields = ('type', 'extension', 'need_infra', 'domaine_ip_start',
......@@ -151,14 +167,17 @@ class IpTypeSerializer(NamespacedHMSerializer):
class VlanSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Vlan` objects.
"""
class Meta:
model = machines.Vlan
fields = ('vlan_id', 'name', 'comment', 'api_url')
fields = ('vlan_id', 'name', 'comment', 'arp_protect', 'dhcp_snooping',
'dhcpv6_snooping', 'igmp', 'mld', 'api_url')
class NasSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Nas` objects.
"""
class Meta:
model = machines.Nas
fields = ('name', 'nas_type', 'machine_type', 'port_access_mode',
......@@ -168,6 +187,7 @@ class NasSerializer(NamespacedHMSerializer):
class SOASerializer(NamespacedHMSerializer):
"""Serialize `machines.models.SOA` objects.
"""
class Meta:
model = machines.SOA
fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl',
......@@ -177,6 +197,7 @@ class SOASerializer(NamespacedHMSerializer):
class ExtensionSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Extension` objects.
"""
class Meta:
model = machines.Extension
fields = ('name', 'need_infra', 'origin', 'origin_v6', 'soa',
......@@ -186,6 +207,7 @@ class ExtensionSerializer(NamespacedHMSerializer):
class MxSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Mx` objects.
"""
class Meta:
model = machines.Mx
fields = ('zone', 'priority', 'name', 'api_url')
......@@ -194,13 +216,16 @@ class MxSerializer(NamespacedHMSerializer):
class DNameSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.DName` objects.
"""
class Meta:
model = machines.DName
fields = ('zone', 'alias', 'api_url')
class NsSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Ns` objects.
"""
class Meta:
model = machines.Ns
fields = ('zone', 'ns', 'api_url')
......@@ -209,6 +234,7 @@ class NsSerializer(NamespacedHMSerializer):
class TxtSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Txt` objects.
"""
class Meta:
model = machines.Txt
fields = ('zone', 'field1', 'field2', 'api_url')
......@@ -217,14 +243,17 @@ class TxtSerializer(NamespacedHMSerializer):
class SrvSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Srv` objects.
"""
class Meta:
model = machines.Srv
fields = ('service', 'protocole', 'extension', 'ttl', 'priority',
'weight', 'port', 'target', 'api_url')
class SshFpSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.SSHFP` objects.
"""
class Meta:
model = machines.SshFp
field = ('machine', 'pub_key_entry', 'algo', 'comment', 'api_url')
......@@ -245,6 +274,7 @@ class InterfaceSerializer(NamespacedHMSerializer):
class Ipv6ListSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Ipv6List` objects.
"""
class Meta:
model = machines.Ipv6List
fields = ('ipv6', 'interface', 'slaac_ip', 'api_url')
......@@ -253,6 +283,7 @@ class Ipv6ListSerializer(NamespacedHMSerializer):
class DomainSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Domain` objects.
"""
class Meta:
model = machines.Domain
fields = ('interface_parent', 'name', 'extension', 'cname',
......@@ -262,6 +293,7 @@ class DomainSerializer(NamespacedHMSerializer):
class IpListSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.IpList` objects.
"""
class Meta:
model = machines.IpList
fields = ('ipv4', 'ip_type', 'need_infra', 'api_url')
......@@ -270,6 +302,7 @@ class IpListSerializer(NamespacedHMSerializer):
class ServiceSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Service` objects.
"""
class Meta:
model = machines.Service
fields = ('service_type', 'min_time_regen', 'regular_time_regen',
......@@ -279,6 +312,7 @@ class ServiceSerializer(NamespacedHMSerializer):
class ServiceLinkSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Service_link` objects.
"""
class Meta:
model = machines.Service_link
fields = ('service', 'server', 'last_regen', 'asked_regen',
......@@ -305,11 +339,22 @@ class OuverturePortListSerializer(NamespacedHMSerializer):
class OuverturePortSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.OuverturePort` objects.
"""
class Meta:
model = machines.OuverturePort
fields = ('begin', 'end', 'port_list', 'protocole', 'io', 'api_url')
class RoleSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.OuverturePort` objects.
"""
servers = InterfaceSerializer(read_only=True, many=True)
class Meta:
model = machines.Role
fields = ('role_type', 'servers', 'api_url')
# PREFERENCES
......@@ -317,17 +362,21 @@ class OptionalUserSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.OptionalUser` objects.
"""
tel_mandatory = serializers.BooleanField(source='is_tel_mandatory')
shell_default = serializers.StringRelatedField()
class Meta:
model = preferences.OptionalUser
fields = ('tel_mandatory', 'user_solde', 'solde_negatif', 'max_solde',
'min_online_payment', 'gpg_fingerprint',
'all_can_create_club', 'self_adhesion', 'shell_default')
fields = ('tel_mandatory', 'gpg_fingerprint',
'all_can_create_club', 'self_adhesion', 'shell_default',
'self_change_shell', 'local_email_accounts_enabled', 'local_email_domain',
'max_email_address',
)
class OptionalMachineSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.OptionalMachine` objects.
"""
class Meta:
model = preferences.OptionalMachine
fields = ('password_machine', 'max_lambdauser_interfaces',
......@@ -338,27 +387,45 @@ class OptionalMachineSerializer(NamespacedHMSerializer):
class OptionalTopologieSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.OptionalTopologie` objects.
"""
switchs_management_interface_ip = serializers.CharField()
class Meta:
model = preferences.OptionalTopologie
fields = ('radius_general_policy', 'vlan_decision_ok',
'vlan_decision_nok')
fields = ('switchs_ip_type', 'switchs_web_management',
'switchs_web_management_ssl', 'switchs_rest_management',
'switchs_management_utils', 'switchs_management_interface_ip',
'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds')
class RadiusOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.RadiusOption` objects
"""
class Meta:
model = preferences.RadiusOption
fields = ('radius_general_policy', 'unknown_machine',
'unknown_machine_vlan', 'unknown_port',
'unknown_port_vlan', 'unknown_room', 'unknown_room_vlan',
'non_member', 'non_member_vlan', 'banned', 'banned_vlan',
'vlan_decision_ok')
class GeneralOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.GeneralOption` objects.
"""
class Meta:
model = preferences.GeneralOption
fields