...
 
Commits (136)
settings_local.py
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.swp
*.pyc
# Translations
#*.mo TODO
*.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,17 @@ 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
```
# 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,8 +26,8 @@ done.
"""
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
......
......@@ -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.
"""
......@@ -46,4 +48,4 @@ class ExpiringTokenAuthentication(TokenAuthentication):
if token.created < utc_now - token_duration:
raise exceptions.AuthenticationFailed(_('Token has expired'))
return (token.user, token)
return token.user, token
......@@ -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,6 +71,7 @@ class NamespacedHMSerializer(serializers.HyperlinkedModelSerializer):
class FactureSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Facture` objects.
"""
class Meta:
model = cotisations.Facture
fields = ('user', 'paiement', 'banque', 'cheque', 'date', 'valid',
......@@ -79,6 +81,7 @@ class FactureSerializer(NamespacedHMSerializer):
class VenteSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Vente` objects.
"""
class Meta:
model = cotisations.Vente
fields = ('facture', 'number', 'name', 'prix', 'duration',
......@@ -88,6 +91,7 @@ class VenteSerializer(NamespacedHMSerializer):
class ArticleSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Article` objects.
"""
class Meta:
model = cotisations.Article
fields = ('name', 'prix', 'duration', 'type_user',
......@@ -97,6 +101,7 @@ class ArticleSerializer(NamespacedHMSerializer):
class BanqueSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Banque` objects.
"""
class Meta:
model = cotisations.Banque
fields = ('name', 'api_url')
......@@ -105,6 +110,7 @@ class BanqueSerializer(NamespacedHMSerializer):
class PaiementSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Paiement` objects.
"""
class Meta:
model = cotisations.Paiement
fields = ('moyen', 'type_paiement', 'api_url')
......@@ -113,6 +119,7 @@ class PaiementSerializer(NamespacedHMSerializer):
class CotisationSerializer(NamespacedHMSerializer):
"""Serialize `cotisations.models.Cotisation` objects.
"""
class Meta:
model = cotisations.Cotisation
fields = ('vente', 'type_cotisation', 'date_start', 'date_end',
......@@ -125,6 +132,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 +141,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 +150,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 +161,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 +181,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 +191,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 +201,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 +210,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 +228,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 +237,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 +268,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 +277,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 +287,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 +296,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 +306,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 +333,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
......@@ -328,6 +367,7 @@ class OptionalUserSerializer(NamespacedHMSerializer):
class OptionalMachineSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.OptionalMachine` objects.
"""
class Meta:
model = preferences.OptionalMachine
fields = ('password_machine', 'max_lambdauser_interfaces',
......@@ -338,16 +378,21 @@ 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')
'vlan_decision_nok', '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 GeneralOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.GeneralOption` objects.
"""
class Meta:
model = preferences.GeneralOption
fields = ('general_message', 'search_display_page',
......@@ -359,6 +404,7 @@ class GeneralOptionSerializer(NamespacedHMSerializer):
class HomeServiceSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.Service` objects.
"""
class Meta:
model = preferences.Service
fields = ('name', 'url', 'description', 'image', 'api_url')
......@@ -370,6 +416,7 @@ class HomeServiceSerializer(NamespacedHMSerializer):
class AssoOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.AssoOption` objects.
"""
class Meta:
model = preferences.AssoOption
fields = ('name', 'siret', 'adresse1', 'adresse2', 'contact',
......@@ -380,6 +427,7 @@ class AssoOptionSerializer(NamespacedHMSerializer):
class HomeOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.HomeOption` objects.
"""
class Meta:
model = preferences.HomeOption
fields = ('facebook_url', 'twitter_url', 'twitter_account_name')
......@@ -388,18 +436,19 @@ class HomeOptionSerializer(NamespacedHMSerializer):
class MailMessageOptionSerializer(NamespacedHMSerializer):
"""Serialize `preferences.models.MailMessageOption` objects.
"""
class Meta:
model = preferences.MailMessageOption
fields = ('welcome_mail_fr', 'welcome_mail_en')
# TOPOLOGIE
class StackSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Stack` objects
"""
class Meta:
model = topologie.Stack
fields = ('name', 'stack_id', 'details', 'member_id_min',
......@@ -409,6 +458,7 @@ class StackSerializer(NamespacedHMSerializer):
class AccessPointSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.AccessPoint` objects
"""
class Meta:
model = topologie.AccessPoint
fields = ('user', 'name', 'active', 'location', 'api_url')
......@@ -418,6 +468,7 @@ class SwitchSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Switch` objects
"""
port_amount = serializers.IntegerField(source='number')
class Meta:
model = topologie.Switch
fields = ('user', 'name', 'active', 'port_amount', 'stack',
......@@ -427,6 +478,7 @@ class SwitchSerializer(NamespacedHMSerializer):
class ServerSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Server` objects
"""
class Meta:
model = topologie.Server
fields = ('user', 'name', 'active', 'api_url')
......@@ -435,6 +487,7 @@ class ServerSerializer(NamespacedHMSerializer):
class ModelSwitchSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.ModelSwitch` objects
"""
class Meta:
model = topologie.ModelSwitch
fields = ('reference', 'constructor', 'api_url')
......@@ -443,6 +496,7 @@ class ModelSwitchSerializer(NamespacedHMSerializer):
class ConstructorSwitchSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.ConstructorSwitch` objects
"""
class Meta:
model = topologie.ConstructorSwitch
fields = ('name', 'api_url')
......@@ -451,6 +505,7 @@ class ConstructorSwitchSerializer(NamespacedHMSerializer):
class SwitchBaySerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.SwitchBay` objects
"""
class Meta:
model = topologie.SwitchBay
fields = ('name', 'building', 'info', 'api_url')
......@@ -459,6 +514,7 @@ class SwitchBaySerializer(NamespacedHMSerializer):
class BuildingSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Building` objects
"""
class Meta:
model = topologie.Building
fields = ('name', 'api_url')
......@@ -467,19 +523,34 @@ class BuildingSerializer(NamespacedHMSerializer):
class SwitchPortSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Port` objects
"""
get_port_profile = NamespacedHIField(view_name='portprofile-detail', read_only=True)
class Meta:
model = topologie.Port
fields = ('switch', 'port', 'room', 'machine_interface', 'related',
'custom_profile', 'state', 'details', 'api_url')
'custom_profile', 'state', 'get_port_profile', 'details', 'api_url')
extra_kwargs = {
'related': {'view_name': 'switchport-detail'},
'api_url': {'view_name': 'switchport-detail'},
}
class PortProfileSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Room` objects
"""
class Meta:
model = topologie.PortProfile
fields = ('name', 'profil_default', 'vlan_untagged', 'vlan_tagged',
'radius_type', 'radius_mode', 'speed', 'mac_limit', 'flow_control',
'dhcp_snooping', 'dhcpv6_snooping', 'dhcpv6_snooping', 'arp_protect',
'ra_guard', 'loop_protect', 'api_url')
class RoomSerializer(NamespacedHMSerializer):
"""Serialize `topologie.models.Room` objects
"""
class Meta:
model = topologie.Room
fields = ('name', 'details', 'api_url')
......@@ -547,11 +618,12 @@ class AdherentSerializer(NamespacedHMSerializer):
fields = ('name', 'surname', 'pseudo', 'email', 'local_email_redirect',
'local_email_enabled', 'school', 'shell', 'comment',
'state', 'registered', 'telephone', 'room', 'solde',
'access', 'end_access', 'uid', 'api_url','gid')
'access', 'end_access', 'uid', 'api_url', 'gid')
extra_kwargs = {
'shell': {'view_name': 'shell-detail'}
}
class HomeCreationSerializer(NamespacedHMSerializer):
"""Serialize 'users.models.User' minimal infos to create home
"""
......@@ -562,9 +634,11 @@ class HomeCreationSerializer(NamespacedHMSerializer):
model = users.User
fields = ('pseudo', 'uid', 'gid')
class ServiceUserSerializer(NamespacedHMSerializer):
"""Serialize `users.models.ServiceUser` objects.
"""
class Meta:
model = users.ServiceUser
fields = ('pseudo', 'access_group', 'comment', 'api_url')
......@@ -573,6 +647,7 @@ class ServiceUserSerializer(NamespacedHMSerializer):
class SchoolSerializer(NamespacedHMSerializer):
"""Serialize `users.models.School` objects.
"""
class Meta:
model = users.School
fields = ('name', 'api_url')
......@@ -581,6 +656,7 @@ class SchoolSerializer(NamespacedHMSerializer):
class ListRightSerializer(NamespacedHMSerializer):
"""Serialize `users.models.ListRight` objects.
"""
class Meta:
model = users.ListRight
fields = ('unix_name', 'gid', 'critical', 'details', 'api_url')
......@@ -589,6 +665,7 @@ class ListRightSerializer(NamespacedHMSerializer):
class ShellSerializer(NamespacedHMSerializer):
"""Serialize `users.models.ListShell` objects.
"""
class Meta:
model = users.ListShell
fields = ('shell', 'api_url')
......@@ -622,6 +699,7 @@ class EMailAddressSerializer(NamespacedHMSerializer):
"""Serialize `users.models.EMailAddress` objects.
"""
user = serializers.CharField(source='user.pseudo', read_only=True)
class Meta:
model = users.EMailAddress
fields = ('user', 'local_part', 'complete_email_address', 'api_url')
......@@ -644,6 +722,89 @@ class ServiceRegenSerializer(NamespacedHMSerializer):
'api_url': {'view_name': 'serviceregen-detail'}
}
# Switches et ports
class InterfaceVlanSerializer(NamespacedHMSerializer):
domain = serializers.CharField(read_only=True)
ipv4 = serializers.CharField(read_only=True)
ipv6 = Ipv6ListSerializer(read_only=True, many=True)
vlan_id = serializers.IntegerField(source='type.ip_type.vlan.vlan_id', read_only=True)
class Meta:
model = machines.Interface
fields = ('ipv4', 'ipv6', 'domain', 'vlan_id')
class InterfaceRoleSerializer(NamespacedHMSerializer):
interface = InterfaceVlanSerializer(source='machine.interface_set', read_only=True, many=True)
class Meta:
model = machines.Interface
fields = ('interface',)
class RoleSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.OuverturePort` objects.
"""
servers = InterfaceRoleSerializer(read_only=True, many=True)
class Meta:
model = machines.Role
fields = ('role_type', 'servers', 'specific_role')
class VlanPortSerializer(NamespacedHMSerializer):
class Meta:
model = machines.Vlan
fields = ('vlan_id', 'name')
class ProfilSerializer(NamespacedHMSerializer):
vlan_untagged = VlanSerializer(read_only=True)
vlan_tagged = VlanPortSerializer(read_only=True, many=True)
class Meta:
model = topologie.PortProfile
fields = ('name', 'profil_default', 'vlan_untagged', 'vlan_tagged', 'radius_type', 'radius_mode', 'speed', 'mac_limit', 'flow_control', 'dhcp_snooping', 'dhcpv6_snooping', 'arp_protect', 'ra_guard', 'loop_protect', 'vlan_untagged', 'vlan_tagged')
class ModelSwitchSerializer(NamespacedHMSerializer):
constructor = serializers.CharField(read_only=True)
class Meta:
model = topologie.ModelSwitch
fields = ('reference', 'firmware', 'constructor')
class SwitchBaySerializer(NamespacedHMSerializer):
class Meta:
model = topologie.SwitchBay
fields = ('name',)
class PortsSerializer(NamespacedHMSerializer):
"""Serialize `machines.models.Ipv6List` objects.
"""
get_port_profile = ProfilSerializer(read_only=True)
class Meta:
model = topologie.Port
fields = ('state', 'port', 'pretty_name', 'get_port_profile')
class SwitchPortSerializer(serializers.ModelSerializer):
"""Serialize the data about the switches"""
ports = PortsSerializer(many=True, read_only=True)
model = ModelSwitchSerializer(read_only=True)
switchbay = SwitchBaySerializer(read_only=True)
class Meta:
model = topologie.Switch
fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6',
'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled',
'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value')
# LOCAL EMAILS
......@@ -660,13 +821,14 @@ class LocalEmailUsersSerializer(NamespacedHMSerializer):
'email_address', 'email')
#Firewall
# Firewall
class FirewallPortListSerializer(serializers.ModelSerializer):
class Meta:
model = machines.OuverturePort
fields = ('begin', 'end', 'protocole', 'io', 'show_port')
class FirewallOuverturePortListSerializer(serializers.ModelSerializer):
tcp_ports_in = FirewallPortListSerializer(many=True, read_only=True)
udp_ports_in = FirewallPortListSerializer(many=True, read_only=True)
......@@ -677,6 +839,7 @@ class FirewallOuverturePortListSerializer(serializers.ModelSerializer):
model = machines.OuverturePortList
fields = ('tcp_ports_in', 'udp_ports_in', 'tcp_ports_out', 'udp_ports_out')
class SubnetPortsOpenSerializer(serializers.ModelSerializer):
ouverture_ports = FirewallOuverturePortListSerializer(read_only=True)
......@@ -684,6 +847,7 @@ class SubnetPortsOpenSerializer(serializers.ModelSerializer):
model = machines.IpType
fields = ('type', 'domaine_ip_start', 'domaine_ip_stop', 'complete_prefixv6', 'ouverture_ports')
class InterfacePortsOpenSerializer(serializers.ModelSerializer):
port_lists = FirewallOuverturePortListSerializer(read_only=True, many=True)
ipv4 = serializers.CharField(source='ipv4.ipv4', read_only=True)
......@@ -693,6 +857,7 @@ class InterfacePortsOpenSerializer(serializers.ModelSerializer):
model = machines.Interface
fields = ('port_lists', 'ipv4', 'ipv6')
# DHCP
......@@ -717,6 +882,7 @@ class SOARecordSerializer(SOASerializer):
"""Serialize `machines.models.SOA` objects with the data needed to
generate a SOA DNS record.
"""
class Meta:
model = machines.SOA
fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl')
......@@ -726,6 +892,7 @@ class OriginV4RecordSerializer(IpListSerializer):
"""Serialize `machines.models.IpList` objects with the data needed to
generate an IPv4 Origin DNS record.
"""
class Meta(IpListSerializer.Meta):
fields = ('ipv4',)
......@@ -754,6 +921,7 @@ class TXTRecordSerializer(TxtSerializer):
"""Serialize `machines.models.Txt` objects with the data needed to
generate a TXT DNS record.
"""
class Meta(TxtSerializer.Meta):
fields = ('field1', 'field2')
......@@ -772,6 +940,7 @@ class SSHFPRecordSerializer(SshFpSerializer):
"""Serialize `machines.models.SshFp` objects with the data needed to
generate a SSHFP DNS record.
"""
class Meta(SshFpSerializer.Meta):
fields = ('algo_id', 'hash')
......@@ -845,6 +1014,26 @@ class DNSZonesSerializer(serializers.ModelSerializer):
'mx_records', 'txt_records', 'srv_records', 'a_records',
'aaaa_records', 'cname_records', 'sshfp_records')
#REMINDER
class ReminderUsersSerializer(UserSerializer):
"""Serialize the data about a mailing member.
"""
class Meta(UserSerializer.Meta):
fields = ('get_full_name', 'email')
class ReminderSerializer(serializers.ModelSerializer):
"""
Serialize the data about a reminder
"""
users_to_remind = ReminderUsersSerializer(many=True)
class Meta:
model = preferences.Reminder
fields = ('days','message','users_to_remind')
class DNSReverseZonesSerializer(serializers.ModelSerializer):
"""Serialize the data about DNS Zones.
......@@ -858,22 +1047,24 @@ class DNSReverseZonesSerializer(serializers.ModelSerializer):
ptr_records = ARecordSerializer(many=True, source='get_associated_ptr_records')
ptr_v6_records = AAAARecordSerializer(many=True, source='get_associated_ptr_v6_records')
class Meta:
model = machines.IpType
fields = ('type', 'extension', 'soa', 'ns_records', 'mx_records',
'txt_records', 'ptr_records', 'ptr_v6_records', 'cidrs',
'prefix_v6', 'prefix_v6_length')
# MAILING
class MailingMemberSerializer(UserSerializer):
"""Serialize the data about a mailing member.
"""
class Meta(UserSerializer.Meta):
fields = ('name', 'pseudo', 'get_mail')
class MailingSerializer(ClubSerializer):
"""Serialize the data about a mailing.
"""
......
......@@ -48,4 +48,4 @@ API_APPS = (
)
# The expiration time for an authentication token
API_TOKEN_DURATION = 86400 # 24 hours
API_TOKEN_DURATION = 86400 # 24 hours
......@@ -21,10 +21,11 @@
"""Defines the test suite for the API
"""
import json
import datetime
from rest_framework.test import APITestCase
import json
from requests import codes
from rest_framework.test import APITestCase
import cotisations.models as cotisations
import machines.models as machines
......@@ -33,7 +34,7 @@ import topologie.models as topologie
import users.models as users
class APIEndpointsTestCase(APITestCase):
class APIEndpointsTestCase(APITestCase):
"""Test case to test that all endpoints are reachable with respects to
authentication and permission checks.
......@@ -148,10 +149,10 @@ class APIEndpointsTestCase(APITestCase):
'/api/users/club/',
# 4th user to be create (stduser, superuser, users_adherent_1,
# users_club_1)
'/api/users/club/4/',
'/api/users/club/4/',
'/api/users/listright/',
# TODO: Merge !145
# '/api/users/listright/1/',
# TODO: Merge !145
# '/api/users/listright/1/',
'/api/users/school/',
'/api/users/school/1/',
'/api/users/serviceuser/',
......@@ -215,7 +216,7 @@ class APIEndpointsTestCase(APITestCase):
'/api/users/user/4242/',
'/api/users/whitelist/4242/',
]
stduser = None
superuser = None
......@@ -363,7 +364,7 @@ class APIEndpointsTestCase(APITestCase):
machine=cls.machines_machine_1, # Dep machines.Machine
type=cls.machines_machinetype_1, # Dep machines.MachineType
details="machines Interface 1",
#port_lists=[cls.machines_ouvertureportlist_1] # Dep machines.OuverturePortList
# port_lists=[cls.machines_ouvertureportlist_1] # Dep machines.OuverturePortList
)
cls.machines_domain_1 = machines.Domain.objects.create(
interface_parent=cls.machines_interface_1, # Dep machines.Interface
......@@ -525,14 +526,14 @@ class APIEndpointsTestCase(APITestCase):
uid_number=21103,
rezo_rez_uid=21103
)
# Need merge of MR145 to work
# TODO: Merge !145
# cls.users_listright_1 = users.ListRight.objects.create(
# unix_name="userslistright",
# gid=601,
# critical=False,
# details="userslistright"
# )
# Need merge of MR145 to work
# TODO: Merge !145
# cls.users_listright_1 = users.ListRight.objects.create(
# unix_name="userslistright",
# gid=601,
# critical=False,
# details="userslistright"
# )
cls.users_serviceuser_1 = users.ServiceUser.objects.create(
password="password",
last_login=datetime.datetime.now(datetime.timezone.utc),
......@@ -663,7 +664,7 @@ class APIEndpointsTestCase(APITestCase):
AssertionError: An endpoint did not have a 200 status code.
"""
self.client.force_authenticate(user=self.superuser)
urls = self.no_auth_endpoints + self.auth_no_perm_endpoints + \
self.auth_perm_endpoints
......@@ -676,6 +677,7 @@ class APIEndpointsTestCase(APITestCase):
formats=[None, 'json', 'api'],
assert_more=assert_more)
class APIPaginationTestCase(APITestCase):
"""Test case to check that the pagination is used on all endpoints that
should use it.
......@@ -756,7 +758,7 @@ class APIPaginationTestCase(APITestCase):
@classmethod
def tearDownClass(cls):
cls.superuser.delete()
super().tearDownClass()
super(APIPaginationTestCase, self).tearDownClass()
def test_pagination(self):
"""Tests that every endpoint is using the pagination correctly.
......@@ -776,4 +778,3 @@ class APIPaginationTestCase(APITestCase):
assert 'previous' in res_json.keys()
assert 'results' in res_json.keys()
assert not len('results') > 100
......@@ -32,7 +32,6 @@ from django.conf.urls import url, include
from . import views
from .routers import AllViewsRouter
router = AllViewsRouter()
# COTISATIONS
router.register_viewset(r'cotisations/facture', views.FactureViewSet)
......@@ -63,6 +62,7 @@ router.register_viewset(r'machines/service', views.ServiceViewSet)
router.register_viewset(r'machines/servicelink', views.ServiceLinkViewSet, base_name='servicelink')
router.register_viewset(r'machines/ouvertureportlist', views.OuverturePortListViewSet)
router.register_viewset(r'machines/ouvertureport', views.OuverturePortViewSet)
router.register_viewset(r'machines/role', views.RoleViewSet)
# PREFERENCES
router.register_view(r'preferences/optionaluser', views.OptionalUserView),
router.register_view(r'preferences/optionalmachine', views.OptionalMachineView),
......@@ -81,7 +81,8 @@ router.register_viewset(r'topologie/modelswitch', views.ModelSwitchViewSet)
router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchViewSet)
router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet)
router.register_viewset(r'topologie/building', views.BuildingViewSet)
router.register(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport')
router.register_viewset(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport')
router.register_viewset(r'topologie/portprofile', views.PortProfileViewSet, base_name='portprofile')
router.register_viewset(r'topologie/room', views.RoomViewSet)
router.register(r'topologie/portprofile', views.PortProfileViewSet)
# USERS
......@@ -105,6 +106,11 @@ router.register_view(r'localemail/users', views.LocalEmailUsersView),
# Firewall
router.register_view(r'firewall/subnet-ports', views.SubnetPortsOpenView),
router.register_view(r'firewall/interface-ports', views.InterfacePortsOpenView),
# Switches config
router.register_view(r'switchs/ports-config', views.SwitchPortView),
router.register_view(r'switchs/role', views.RoleView),
# Reminder
router.register_view(r'reminder/get-users', views.ReminderView),
# DNS
router.register_view(r'dns/zones', views.DNSZonesView),
router.register_view(r'dns/reverse-zones', views.DNSReverseZonesView),
......@@ -114,7 +120,6 @@ router.register_view(r'mailing/club', views.ClubMailingView),
# TOKEN AUTHENTICATION
router.register_view(r'token-auth', views.ObtainExpiringAuthToken)
urlpatterns = [
url(r'^', include(router.urls)),
]
......@@ -29,10 +29,11 @@ the response (JSON or other), the CSRF exempting, ...
import datetime
from django.conf import settings
from rest_framework.authtoken.views import ObtainAuthToken
from django.db.models import Q
from rest_framework import viewsets, generics, views
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.response import Response
from rest_framework import viewsets, generics, views
import cotisations.models as cotisations
import machines.models as machines
......@@ -40,7 +41,6 @@ import preferences.models as preferences
import topologie.models as topologie
import users.models as users
from re2o.utils import all_active_interfaces, all_has_access
from . import serializers
from .pagination import PageSizedPagination
from .permissions import ACLPermission
......@@ -163,6 +163,7 @@ class TxtViewSet(viewsets.ReadOnlyModelViewSet):
queryset = machines.Txt.objects.all()
serializer_class = serializers.TxtSerializer
class DNameViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `machines.models.DName` objects.
"""
......@@ -241,6 +242,13 @@ class OuverturePortViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.OuverturePortSerializer
class RoleViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `machines.models.Machine` objects.
"""
queryset = machines.Role.objects.all()
serializer_class = serializers.RoleSerializer
# PREFERENCES
# Those views differ a bit because there is only one object
# to display, so we don't bother with the listing part
......@@ -248,8 +256,8 @@ class OuverturePortViewSet(viewsets.ReadOnlyModelViewSet):
class OptionalUserView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.` settings.
"""
permission_classes = (ACLPermission, )
perms_map = {'GET' : [preferences.OptionalUser.can_view_all]}
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.OptionalUser.can_view_all]}
serializer_class = serializers.OptionalUserSerializer
def get_object(self):
......@@ -259,8 +267,8 @@ class OptionalUserView(generics.RetrieveAPIView):
class OptionalMachineView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.OptionalMachine` settings.
"""
permission_classes = (ACLPermission, )
perms_map = {'GET' : [preferences.OptionalMachine.can_view_all]}
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.OptionalMachine.can_view_all]}
serializer_class = serializers.OptionalMachineSerializer
def get_object(self):
......@@ -270,8 +278,8 @@ class OptionalMachineView(generics.RetrieveAPIView):
class OptionalTopologieView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.OptionalTopologie` settings.
"""
permission_classes = (ACLPermission, )
perms_map = {'GET' : [preferences.OptionalTopologie.can_view_all]}
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.OptionalTopologie.can_view_all]}
serializer_class = serializers.OptionalTopologieSerializer
def get_object(self):
......@@ -281,8 +289,8 @@ class OptionalTopologieView(generics.RetrieveAPIView):
class GeneralOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.GeneralOption` settings.
"""
permission_classes = (ACLPermission, )
perms_map = {'GET' : [preferences.GeneralOption.can_view_all]}
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.GeneralOption.can_view_all]}
serializer_class = serializers.GeneralOptionSerializer
def get_object(self):
......@@ -299,8 +307,8 @@ class HomeServiceViewSet(viewsets.ReadOnlyModelViewSet):
class AssoOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.AssoOption` settings.
"""
permission_classes = (ACLPermission, )
perms_map = {'GET' : [preferences.AssoOption.can_view_all]}
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.AssoOption.can_view_all]}
serializer_class = serializers.AssoOptionSerializer
def get_object(self):
......@@ -310,8 +318,8 @@ class AssoOptionView(generics.RetrieveAPIView):
class HomeOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.HomeOption` settings.
"""
permission_classes = (ACLPermission, )
perms_map = {'GET' : [preferences.HomeOption.can_view_all]}
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.HomeOption.can_view_all]}
serializer_class = serializers.HomeOptionSerializer
def get_object(self):
......@@ -321,8 +329,8 @@ class HomeOptionView(generics.RetrieveAPIView):
class MailMessageOptionView(generics.RetrieveAPIView):
"""Exposes details of `preferences.models.MailMessageOption` settings.
"""
permission_classes = (ACLPermission, )
perms_map = {'GET' : [preferences.MailMessageOption.can_view_all]}
permission_classes = (ACLPermission,)
perms_map = {'GET': [preferences.MailMessageOption.can_view_all]}
serializer_class = serializers.MailMessageOptionSerializer
def get_object(self):
......@@ -396,6 +404,13 @@ class SwitchPortViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.SwitchPortSerializer
class PortProfileViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `topologie.models.PortProfile` objects.
"""
queryset = topologie.PortProfile.objects.all()
serializer_class = serializers.PortProfileSerializer
class RoomViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `topologie.models.Room` objects.
"""
......@@ -409,6 +424,7 @@ class PortProfileViewSet(viewsets.ReadOnlyModelViewSet):
queryset = topologie.PortProfile.objects.all()
serializer_class = serializers.PortProfileSerializer
# USER
......@@ -418,12 +434,14 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = users.User.objects.all()
serializer_class = serializers.UserSerializer
class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users` objects to create homes.
"""
queryset = users.User.objects.all()
queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE))
serializer_class = serializers.HomeCreationSerializer
class ClubViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `users.models.Club` objects.
"""
......@@ -488,7 +506,7 @@ class EMailAddressViewSet(viewsets.ReadOnlyModelViewSet):
def get_queryset(self):
if preferences.OptionalUser.get_cached_value(
'local_email_accounts_enabled'):
'local_email_accounts_enabled'):
return (users.EMailAddress.objects
.filter(user__local_email_enabled=True))
else:
......@@ -514,6 +532,33 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
queryset = queryset.filter(server__domain__name__iexact=hostname)
return queryset
# Config des switches
class SwitchPortView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""
queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room")
serializer_class = serializers.SwitchPortSerializer
# Rappel fin adhésion
class ReminderView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""
queryset = preferences.Reminder.objects.all()
serializer_class = serializers.ReminderSerializer
class RoleView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
"""
queryset = machines.Role.objects.all().prefetch_related('servers')
serializer_class = serializers.RoleSerializer
# LOCAL EMAILS
......@@ -525,7 +570,7 @@ class LocalEmailUsersView(generics.ListAPIView):
def get_queryset(self):
if preferences.OptionalUser.get_cached_value(
'local_email_accounts_enabled'):
'local_email_accounts_enabled'):
return (users.User.objects
.filter(local_email_enabled=True))
else:
......@@ -543,16 +588,18 @@ class HostMacIpView(generics.ListAPIView):
serializer_class = serializers.HostMacIpSerializer
#Firewall
# Firewall
class SubnetPortsOpenView(generics.ListAPIView):
queryset = machines.IpType.objects.all()
serializer_class = serializers.SubnetPortsOpenSerializer
class InterfacePortsOpenView(generics.ListAPIView):
queryset = machines.Interface.objects.filter(port_lists__isnull=False).distinct()
serializer_class = serializers.InterfacePortsOpenSerializer
# DNS
......@@ -570,6 +617,7 @@ class DNSZonesView(generics.ListAPIView):
.all())
serializer_class = serializers.DNSZonesSerializer
class DNSReverseZonesView(generics.ListAPIView):
"""Exposes the detailed information about each extension (hostnames,
IPs, DNS records, etc.) in order to build the DNS zone files.
......@@ -578,8 +626,6 @@ class DNSReverseZonesView(generics.ListAPIView):
serializer_class = serializers.DNSReverseZonesSerializer
# MAILING
......@@ -588,8 +634,8 @@ class StandardMailingView(views.APIView):
order to building the corresponding mailing lists.
"""
pagination_class = PageSizedPagination
permission_classes = (ACLPermission, )
perms_map = {'GET' : [users.User.can_view_all]}
permission_classes = (ACLPermission,)
perms_map = {'GET': [users.User.can_view_all]}
def get(self, request, format=None):
adherents_data = serializers.MailingMemberSerializer(all_has_access(), many=True).data
......@@ -617,6 +663,7 @@ class ObtainExpiringAuthToken(ObtainAuthToken):
`rest_framework.auth_token.views.ObtainAuthToken` view except that the
expiration time is send along with the token as an addtional information.
"""
def post(self, request, *args, **kwargs):
serializer = self.serializer_class(data=request.data)
serializer.is_valid(raise_exception=True)
......
......@@ -102,7 +102,7 @@ class SelectArticleForm(FormRevMixin, Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop('user')
target_user = kwargs.pop('target_user')
target_user = kwargs.pop('target_user',