...
 
Commits (19)
# Re2o # Re2o
Gnu public license v2.0 GNU public license v2.0
## Avant propos ## 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 se veut agnostique au réseau considéré, de manière à être installable en
quelques clics. quelques clics.
...@@ -31,15 +31,15 @@ Pour cela : ...@@ -31,15 +31,15 @@ Pour cela :
## Fonctionnement général ## Fonctionnement général
Re2o est séparé entre les models, qui sont visible sur le schéma des 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 etant les dépendances. Il s'agit en réalité des tables sql, et les fields étant les
colonnes. colonnes.
Ceci dit il n'est jamais nécessaire de toucher directement au sql, django Ceci dit il n'est jamais nécessaire de toucher directement au sql, django
procédant automatiquement à tout cela. procédant automatiquement à tout cela.
On crée donc différents models (user, right pour les droits des users, On crée donc différents models (user, right pour les droits des users,
interfaces, IpList pour l'ensemble des adresses ip, etc) 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 s'agit de ModelForms django, qui héritent des models très simplement, voir la
documentation django models forms. documentation django models forms.
...@@ -56,12 +56,20 @@ d'accéder à ces vues, utilisé par re2o-tools. ...@@ -56,12 +56,20 @@ d'accéder à ces vues, utilisé par re2o-tools.
# Requète en base de donnée # Requète en base de donnée
Pour avoir un shell, il suffit de lancer '''python3 manage.py shell''' Pour avoir un shell, lancer :
Pour charger des objets, example avec User, faire : ```.bash
''' from users.models import User''' python3 manage.py shell
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 Pour charger des objets (exemple avec User), faire :
User.objects.filter(pseudo='test') ```.python
Des exemples et la documentation complète sur les requètes django sont 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. disponible sur le site officiel.
...@@ -29,6 +29,7 @@ the response (JSON or other), the CSRF exempting, ... ...@@ -29,6 +29,7 @@ the response (JSON or other), the CSRF exempting, ...
import datetime import datetime
from django.conf import settings from django.conf import settings
from django.db.models import Q
from rest_framework.authtoken.views import ObtainAuthToken from rest_framework.authtoken.views import ObtainAuthToken
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.response import Response from rest_framework.response import Response
...@@ -421,7 +422,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -421,7 +422,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet): class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes infos of `users.models.Users` objects to create homes. """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 serializer_class = serializers.HomeCreationSerializer
class ClubViewSet(viewsets.ReadOnlyModelViewSet): class ClubViewSet(viewsets.ReadOnlyModelViewSet):
......
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-31 13:32
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cotisations', '0033_auto_20180818_1319'),
]
operations = [
migrations.AlterField(
model_name='facture',
name='valid',
field=models.BooleanField(default=False, verbose_name='validated'),
),
]
...@@ -50,7 +50,7 @@ from machines.models import regen ...@@ -50,7 +50,7 @@ from machines.models import regen
from re2o.field_permissions import FieldPermissionModelMixin from re2o.field_permissions import FieldPermissionModelMixin
from re2o.mixins import AclMixin, RevMixin from re2o.mixins import AclMixin, RevMixin
from cotisations.utils import find_payment_method from cotisations.utils import find_payment_method, send_mail_invoice
from cotisations.validators import check_no_balance from cotisations.validators import check_no_balance
...@@ -137,7 +137,7 @@ class Facture(BaseInvoice): ...@@ -137,7 +137,7 @@ class Facture(BaseInvoice):
) )
# TODO : change name to validity for clarity # TODO : change name to validity for clarity
valid = models.BooleanField( valid = models.BooleanField(
default=True, default=False,
verbose_name=_("validated") verbose_name=_("validated")
) )
# TODO : changed name to controlled for clarity # TODO : changed name to controlled for clarity
...@@ -231,19 +231,26 @@ class Facture(BaseInvoice): ...@@ -231,19 +231,26 @@ class Facture(BaseInvoice):
self.field_permissions = { self.field_permissions = {
'control': self.can_change_control, 'control': self.can_change_control,
} }
self.__original_valid = self.valid
def save(self, *args, **kwargs):
super(Facture, self).save(*args, **kwargs)
if not self.__original_valid and self.valid:
send_mail_invoice(self)
def __str__(self): def __str__(self):
return str(self.user) + ' ' + str(self.date) return str(self.user) + ' ' + str(self.date)
@receiver(post_save, sender=Facture) @receiver(post_save, sender=Facture)
def facture_post_save(**kwargs): def facture_post_save(**kwargs):
""" """
Synchronise the LDAP user after an invoice has been saved. Synchronise the LDAP user after an invoice has been saved.
""" """
facture = kwargs['instance'] facture = kwargs['instance']
user = facture.user if facture.valid:
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user = facture.user
user.set_active()
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
@receiver(post_delete, sender=Facture) @receiver(post_delete, sender=Facture)
...@@ -472,7 +479,8 @@ def vente_post_save(**kwargs): ...@@ -472,7 +479,8 @@ def vente_post_save(**kwargs):
purchase.create_cotis() purchase.create_cotis()
purchase.cotisation.save() purchase.cotisation.save()
user = purchase.facture.user user = purchase.facture.user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.set_active()
user.ldap_sync(base=True, access_refresh=True, mac_refresh=False)
# TODO : change vente to purchase # TODO : change vente to purchase
...@@ -700,6 +708,10 @@ class Paiement(RevMixin, AclMixin, models.Model): ...@@ -700,6 +708,10 @@ class Paiement(RevMixin, AclMixin, models.Model):
if payment_method is not None and use_payment_method: if payment_method is not None and use_payment_method:
return payment_method.end_payment(invoice, request) return payment_method.end_payment(invoice, request)
## So make this invoice valid, trigger send mail
invoice.valid = True
invoice.save()
# In case a cotisation was bought, inform the user, the # In case a cotisation was bought, inform the user, the
# cotisation time has been extended too # cotisation time has been extended too
if any(sell.type_cotisation for sell in invoice.vente_set.all()): if any(sell.type_cotisation for sell in invoice.vente_set.all()):
......
...@@ -74,8 +74,6 @@ class BalancePayment(PaymentMethodMixin, models.Model): ...@@ -74,8 +74,6 @@ class BalancePayment(PaymentMethodMixin, models.Model):
user = invoice.user user = invoice.user
total_price = invoice.prix_total() total_price = invoice.prix_total()
if float(user.solde) - float(total_price) < self.minimum_balance: if float(user.solde) - float(total_price) < self.minimum_balance:
invoice.valid = False
invoice.save()
messages.error( messages.error(
request, request,
_("Your balance is too low for this operation.") _("Your balance is too low for this operation.")
......
...@@ -46,8 +46,6 @@ class ChequePayment(PaymentMethodMixin, models.Model): ...@@ -46,8 +46,6 @@ class ChequePayment(PaymentMethodMixin, models.Model):
"""Invalidates the invoice then redirect the user towards a view asking """Invalidates the invoice then redirect the user towards a view asking
for informations to add to the invoice before validating it. for informations to add to the invoice before validating it.
""" """
invoice.valid = False
invoice.save()
return redirect(reverse( return redirect(reverse(
'cotisations:cheque:validate', 'cotisations:cheque:validate',
kwargs={'invoice_pk': invoice.pk} kwargs={'invoice_pk': invoice.pk}
......
...@@ -81,8 +81,6 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): ...@@ -81,8 +81,6 @@ class ComnpayPayment(PaymentMethodMixin, models.Model):
a facture id, the price and the secret transaction data stored in a facture id, the price and the secret transaction data stored in
the preferences. the preferences.
""" """
invoice.valid = False
invoice.save()
host = request.get_host() host = request.get_host()
p = Transaction( p = Transaction(
str(self.payment_credential), str(self.payment_credential),
......
...@@ -68,7 +68,7 @@ def accept_payment(request, factureid): ...@@ -68,7 +68,7 @@ def accept_payment(request, factureid):
) )
return redirect(reverse( return redirect(reverse(
'users:profil', 'users:profil',
kwargs={'userid': request.user.id} kwargs={'userid': invoice.user.id}
)) ))
......
...@@ -73,7 +73,7 @@ with this program; if not, write to the Free Software Foundation, Inc., ...@@ -73,7 +73,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% can_delete facture %} {% can_delete facture %}
{% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %} {% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %}
{% acl_end %} {% acl_end %}
{% history_button facture text=True html_class=False %} {% history_button facture %}
</td> </td>
<td> <td>
{% if facture.valid %} {% if facture.valid %}
......
...@@ -81,7 +81,7 @@ from .forms import ( ...@@ -81,7 +81,7 @@ from .forms import (
) )
from .tex import render_invoice from .tex import render_invoice
from .payment_methods.forms import payment_method_factory from .payment_methods.forms import payment_method_factory
from .utils import find_payment_method, send_mail_invoice from .utils import find_payment_method
@login_required @login_required
...@@ -148,8 +148,6 @@ def new_facture(request, user, userid): ...@@ -148,8 +148,6 @@ def new_facture(request, user, userid):
p.facture = new_invoice_instance p.facture = new_invoice_instance
p.save() p.save()
send_mail_invoice(new_invoice_instance)
return new_invoice_instance.paiement.end_payment( return new_invoice_instance.paiement.end_payment(
new_invoice_instance, new_invoice_instance,
request request
...@@ -848,8 +846,6 @@ def credit_solde(request, user, **_kwargs): ...@@ -848,8 +846,6 @@ def credit_solde(request, user, **_kwargs):
number=1 number=1
) )
send_mail_invoice(invoice)
return invoice.paiement.end_payment(invoice, request) return invoice.paiement.end_payment(invoice, request)
p = get_object_or_404(Paiement, is_balance=True) p = get_object_or_404(Paiement, is_balance=True)
return form({ return form({
......
...@@ -232,7 +232,7 @@ msgstr "Accéder à mon profil" ...@@ -232,7 +232,7 @@ msgstr "Accéder à mon profil"
#: templates/re2o/index.html:79 #: templates/re2o/index.html:79
msgid "Services of the organisation" msgid "Services of the organisation"
msgstr "Serices de l'association" msgstr "Services de l'association"
#: templates/re2o/index.html:93 #: templates/re2o/index.html:93
msgid "Go there" msgid "Go there"
......
...@@ -41,6 +41,8 @@ from django.utils import timezone ...@@ -41,6 +41,8 @@ from django.utils import timezone
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from machines.models import Interface, Machine, Nas
from topologie.models import Port
from preferences.models import OptionalUser from preferences.models import OptionalUser
from re2o.utils import remove_user_room, get_input_formats_help_text from re2o.utils import remove_user_room, get_input_formats_help_text
from re2o.mixins import FormRevMixin from re2o.mixins import FormRevMixin
...@@ -660,3 +662,53 @@ class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): ...@@ -660,3 +662,53 @@ class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
model = User model = User
fields = ['email','local_email_enabled', 'local_email_redirect'] fields = ['email','local_email_enabled', 'local_email_redirect']
class InitialRegisterForm(forms.Form):
register_room = forms.BooleanField(required=False)
register_machine = forms.BooleanField(required=False)
def __init__(self, *args, **kwargs):
switch_ip = kwargs.pop('switch_ip')
switch_port = kwargs.pop('switch_port')
client_mac = kwargs.pop('client_mac')
self.user = kwargs.pop('user')
if switch_ip and switch_port:
# Looking for a port
port = Port.objects.filter(switch__interface__ipv4__ipv4=switch_ip, port=switch_port).first()
# If a port exists, checking there is a room AND radius
if port:
if port.get_port_profile.radius_type != 'NO' and port.get_port_profile.radius_mode == 'STRICT' and hasattr(port, 'room'):
# Requesting user is not in this room ?
if self.user.room != port.room:
self.new_room = port.room
if client_mac and switch_ip:
# If this interface doesn't already exists
if not Interface.objects.filter(mac_address=client_mac):
self.mac_address = client_mac
self.nas_type = Nas.objects.filter(nas_type__interface__ipv4__ipv4=switch_ip).first()
super(InitialRegisterForm, self).__init__(*args, **kwargs)
if hasattr(self, 'new_room'):
self.fields['register_room'].label = _("New connection from room %s. Is it yours? If that is the case, type OK." % self.new_room)
else:
self.fields.pop('register_room')
if hasattr(self, 'mac_address'):
self.fields['register_machine'].label = _("New connection from new device. Register it? Say Yes to get Internet access from it (MAC Address : %s)." % self.mac_address)
else:
self.fields.pop('register_machine')
def clean_register_room(self):
if self.cleaned_data['register_room']:
if self.user.is_class_adherent:
remove_user_room(self.new_room)
user = self.user.adherent
user.room = self.new_room
user.save()
if self.user.is_class_club:
user = self.user.club
user.room = self.new_room
user.save()
def clean_register_machine(self):
if self.cleaned_data['register_machine']:
if self.mac_address and self.nas_type:
self.user.autoregister_machine(self.mac_address, self.nas_type)
This diff is collapsed.
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-08-24 15:50
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0076_auto_20180818_1321'),
]
operations = [
migrations.AlterField(
model_name='user',
name='state',
field=models.IntegerField(choices=[(0, 'STATE_ACTIVE'), (1, 'STATE_DISABLED'), (2, 'STATE_ARCHIVE'), (3, 'STATE_NOT_YET_ACTIVE')], default=3),
),
]
...@@ -186,10 +186,12 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, ...@@ -186,10 +186,12 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
STATE_ACTIVE = 0 STATE_ACTIVE = 0
STATE_DISABLED = 1 STATE_DISABLED = 1
STATE_ARCHIVE = 2 STATE_ARCHIVE = 2
STATE_NOT_YET_ACTIVE = 3
STATES = ( STATES = (
(0, 'STATE_ACTIVE'), (0, 'STATE_ACTIVE'),
(1, 'STATE_DISABLED'), (1, 'STATE_DISABLED'),
(2, 'STATE_ARCHIVE'), (2, 'STATE_ARCHIVE'),
(3, 'STATE_NOT_YET_ACTIVE'),
) )
surname = models.CharField(max_length=255) surname = models.CharField(max_length=255)
...@@ -231,7 +233,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, ...@@ -231,7 +233,7 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
blank=True blank=True
) )
pwd_ntlm = models.CharField(max_length=255) pwd_ntlm = models.CharField(max_length=255)
state = models.IntegerField(choices=STATES, default=STATE_ACTIVE) state = models.IntegerField(choices=STATES, default=STATE_NOT_YET_ACTIVE)
registered = models.DateTimeField(auto_now_add=True) registered = models.DateTimeField(auto_now_add=True)
telephone = models.CharField(max_length=15, blank=True, null=True) telephone = models.CharField(max_length=15, blank=True, null=True)
uid_number = models.PositiveIntegerField( uid_number = models.PositiveIntegerField(
...@@ -329,7 +331,14 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, ...@@ -329,7 +331,14 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
@property @property
def is_active(self): def is_active(self):
""" Renvoie si l'user est à l'état actif""" """ Renvoie si l'user est à l'état actif"""
return self.state == self.STATE_ACTIVE return self.state == self.STATE_ACTIVE or self.state == self.STATE_NOT_YET_ACTIVE
def set_active(self):
"""Enable this user if he subscribed successfully one time before"""
if self.state == self.STATE_NOT_YET_ACTIVE:
if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists():
self.state = self.STATE_ACTIVE
self.save()
@property @property
def is_staff(self): def is_staff(self):
...@@ -561,6 +570,9 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser, ...@@ -561,6 +570,9 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
try: try:
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number) user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
except LdapUser.DoesNotExist: except LdapUser.DoesNotExist:
# Freshly created users are NOT synced in ldap base
if self.state == self.STATE_NOT_YET_ACTIVE:
return
user_ldap = LdapUser(uidNumber=self.uid_number) user_ldap = LdapUser(uidNumber=self.uid_number)
base = True base = True
access_refresh = True access_refresh = True
......
{% extends "users/sidebar.html" %}
{% comment %}
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
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.
{% endcomment %}
{% load bootstrap3 %}
{% load massive_bootstrap_form %}
{% load static %}
{% load i18n %}
{% block title %}{% trans "Users" %}{% endblock %}
{% block content %}
<h3>{% trans "Your machine and your room were successfully registered. Please disconnect and reconnect your Ethernet cable to benefit from a wired connection." %}</h3>
<center><img src="{% static '/images/rj45.gif' %}" alt="rj45_in_out"></center>
<br/>
<br/>
<br/>
{% endblock %}
...@@ -52,7 +52,7 @@ with this program; if not, write to the Free Software Foundation, Inc., ...@@ -52,7 +52,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% elif not users.has_access %} {% elif not users.has_access %}
<div class="panel panel-danger"> <div class="panel panel-danger">
<div class="panel-heading dashboard">{% trans "No connection" %}</div> <div class="panel-heading dashboard">{% trans "No connection" %}</div>
<div class="panel-body dashboard"> <div class="panel-body dashboard">
{% can_create Facture %} {% can_create Facture %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}"> <a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}">
<i class="fas fa-sign-in-alt"></i> {% trans "Pay for a connection" %} <i class="fas fa-sign-in-alt"></i> {% trans "Pay for a connection" %}
...@@ -213,9 +213,12 @@ with this program; if not, write to the Free Software Foundation, Inc., ...@@ -213,9 +213,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if users.state == 0 %} {% if users.state == 0 %}
<td><i class="text-success">{% trans "Active" %}</i></td> <td><i class="text-success">{% trans "Active" %}</i></td>
{% elif users.state == 1 %} {% elif users.state == 1 %}
<td><i class="text-danger">{% trans "Disabled" %}</i></td> <td><i class="text-warning">{% trans "Disabled" %}</i></td>
{% else %} {% elif users.state == 2 %}
<td><i class="text-warning">{% trans "Archived" %}</i></td> <td><i class="text-danger">{% trans "Archived" %}</i></td>
{% elif users.state == 3 %}
<td><i class="text-danger">{% trans "Not yet Member" %}</i></td>
{% endif %} {% endif %}
</tr> </tr>
<tr> <tr>
......
...@@ -109,6 +109,7 @@ urlpatterns = [ ...@@ -109,6 +109,7 @@ urlpatterns = [
url(r'^mass_archive/$', views.mass_archive, name='mass-archive'), url(r'^mass_archive/$', views.mass_archive, name='mass-archive'),
url(r'^$', views.index, name='index'), url(r'^$', views.index, name='index'),
url(r'^index_clubs/$', views.index_clubs, name='index-clubs'), url(r'^index_clubs/$', views.index_clubs, name='index-clubs'),
url(r'^initial_register/$', views.initial_register, name='initial-register'),
url(r'^rest/ml/std/$', url(r'^rest/ml/std/$',
views.ml_std_list, views.ml_std_list,
name='ml-std-list'), name='ml-std-list'),
......
...@@ -105,7 +105,8 @@ from .forms import ( ...@@ -105,7 +105,8 @@ from .forms import (
PassForm, PassForm,
ResetPasswordForm, ResetPasswordForm,
ClubAdminandMembersForm, ClubAdminandMembersForm,
GroupForm GroupForm,
InitialRegisterForm
) )
...@@ -1081,6 +1082,30 @@ def process_passwd(request, req): ...@@ -1081,6 +1082,30 @@ def process_passwd(request, req):
request request
) )
@login_required
def initial_register(request):
u_form = InitialRegisterForm(request.POST or None, user=request.user, switch_ip=request.GET.get('switch_ip', None), switch_port=request.GET.get('switch_port', None), client_mac=request.GET.get('client_mac', None))
if not u_form.fields:
messages.error(request, _("Incorrect URL, or already registered device"))
return redirect(reverse(
'users:profil',
kwargs={'userid': str(request.user.id)}
))
if u_form.is_valid():
messages.success(request, _("Successful registration! Please"
" disconnect and reconnect your Ethernet"
" cable to get Internet access."))
return form(
{},
'users/plugin_out.html',
request
)
return form(
{'userform': u_form, 'action_name': _("Register device or room")},
'users/user.html',
request
)
class JSONResponse(HttpResponse): class JSONResponse(HttpResponse):
""" Framework Rest """ """ Framework Rest """
......