mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-12-26 00:43:46 +00:00
Merge branch 'master' into ouverture_des_ports
This commit is contained in:
commit
a08ae1027e
18 changed files with 8105 additions and 79 deletions
|
@ -106,7 +106,7 @@ def index(request):
|
|||
'user_id': v.revision.user_id,
|
||||
'version': v }
|
||||
else :
|
||||
to_remove.append(i)
|
||||
to_remove.insert(0,i)
|
||||
# Remove all tagged invalid items
|
||||
for i in to_remove :
|
||||
versions.object_list.pop(i)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- 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.
|
||||
|
@ -5,6 +6,7 @@
|
|||
# Copyright © 2017 Gabriel Détraz
|
||||
# Copyright © 2017 Goulven Kermarec
|
||||
# Copyright © 2017 Augustin Lemesle
|
||||
# Copyright © 2017 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
|
||||
|
@ -52,8 +54,7 @@ class BaseEditMachineForm(EditMachineForm):
|
|||
class EditInterfaceForm(ModelForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
# fields = '__all__'
|
||||
exclude = ['port_lists']
|
||||
fields = ['machine', 'type', 'ipv4', 'mac_address', 'details']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(EditInterfaceForm, self).__init__(*args, **kwargs)
|
||||
|
@ -63,12 +64,14 @@ class EditInterfaceForm(ModelForm):
|
|||
if "ipv4" in self.fields:
|
||||
self.fields['ipv4'].empty_label = "Assignation automatique de l'ipv4"
|
||||
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True)
|
||||
# Add it's own address
|
||||
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance)
|
||||
if "machine" in self.fields:
|
||||
self.fields['machine'].queryset = Machine.objects.all().select_related('user')
|
||||
|
||||
class AddInterfaceForm(EditInterfaceForm):
|
||||
class Meta(EditInterfaceForm.Meta):
|
||||
fields = ['ipv4','mac_address','type','details']
|
||||
fields = ['type','ipv4','mac_address','details']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
infra = kwargs.pop('infra')
|
||||
|
@ -82,11 +85,11 @@ class AddInterfaceForm(EditInterfaceForm):
|
|||
|
||||
class NewInterfaceForm(EditInterfaceForm):
|
||||
class Meta(EditInterfaceForm.Meta):
|
||||
fields = ['mac_address','type','details']
|
||||
fields = ['type','mac_address','details']
|
||||
|
||||
class BaseEditInterfaceForm(EditInterfaceForm):
|
||||
class Meta(EditInterfaceForm.Meta):
|
||||
fields = ['ipv4','mac_address','type','details']
|
||||
fields = ['type','ipv4','mac_address','details']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
infra = kwargs.pop('infra')
|
||||
|
@ -95,8 +98,11 @@ class BaseEditInterfaceForm(EditInterfaceForm):
|
|||
if not infra:
|
||||
self.fields['type'].queryset = MachineType.objects.filter(ip_type__in=IpType.objects.filter(need_infra=False))
|
||||
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True).filter(ip_type__in=IpType.objects.filter(need_infra=False))
|
||||
# Add it's own address
|
||||
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance)
|
||||
else:
|
||||
self.fields['ipv4'].queryset = IpList.objects.filter(interface__isnull=True)
|
||||
self.fields['ipv4'].queryset |= IpList.objects.filter(interface=self.instance)
|
||||
|
||||
class AliasForm(ModelForm):
|
||||
class Meta:
|
||||
|
|
|
@ -56,6 +56,7 @@ class MachineType(models.Model):
|
|||
ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True)
|
||||
|
||||
def all_interfaces(self):
|
||||
""" Renvoie toutes les interfaces (cartes réseaux) de type machinetype"""
|
||||
return Interface.objects.filter(type=self)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -76,23 +77,31 @@ class IpType(models.Model):
|
|||
|
||||
@cached_property
|
||||
def ip_range(self):
|
||||
return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
|
||||
""" Renvoie un objet IPRange à partir de l'objet IpType"""
|
||||
return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
|
||||
|
||||
@cached_property
|
||||
def ip_set(self):
|
||||
""" Renvoie une IPSet à partir de l'iptype"""
|
||||
return IPSet(self.ip_range)
|
||||
|
||||
@cached_property
|
||||
def ip_set_as_str(self):
|
||||
""" Renvoie une liste des ip en string"""
|
||||
return [str(x) for x in self.ip_set]
|
||||
|
||||
def ip_objects(self):
|
||||
""" Renvoie tous les objets ipv4 relié à ce type"""
|
||||
return IpList.objects.filter(ip_type=self)
|
||||
|
||||
def free_ip(self):
|
||||
""" Renvoie toutes les ip libres associées au type donné (self)"""
|
||||
return IpList.objects.filter(interface__isnull=True).filter(ip_type=self)
|
||||
|
||||
def gen_ip_range(self):
|
||||
""" Cree les IpList associées au type self. Parcours pédestrement et crée
|
||||
les ip une par une. Si elles existent déjà, met à jour le type associé
|
||||
à l'ip"""
|
||||
# Creation du range d'ip dans les objets iplist
|
||||
networks = []
|
||||
for net in self.ip_range.cidrs():
|
||||
|
@ -115,6 +124,11 @@ class IpType(models.Model):
|
|||
ip.delete()
|
||||
|
||||
def clean(self):
|
||||
""" Nettoyage. Vérifie :
|
||||
- Que ip_stop est après ip_start
|
||||
- Qu'on ne crée pas plus gros qu'un /16
|
||||
- Que le range crée ne recoupe pas un range existant
|
||||
- Formate l'ipv6 donnée en /64"""
|
||||
if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop):
|
||||
raise ValidationError("Domaine end doit être après start...")
|
||||
# On ne crée pas plus grand qu'un /16
|
||||
|
@ -137,6 +151,7 @@ class IpType(models.Model):
|
|||
return self.type
|
||||
|
||||
class Vlan(models.Model):
|
||||
""" Un vlan : vlan_id et nom"""
|
||||
PRETTY_NAME = "Vlans"
|
||||
|
||||
vlan_id = models.IntegerField()
|
||||
|
@ -147,6 +162,9 @@ class Vlan(models.Model):
|
|||
return self.name
|
||||
|
||||
class Nas(models.Model):
|
||||
""" Les nas. Associé à un machine_type.
|
||||
Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour
|
||||
le radius. Champ autocapture de la mac à true ou false"""
|
||||
PRETTY_NAME = "Correspondance entre les nas et les machines connectées"
|
||||
|
||||
default_mode = '802.1X'
|
||||
|
@ -165,6 +183,8 @@ class Nas(models.Model):
|
|||
return self.name
|
||||
|
||||
class Extension(models.Model):
|
||||
""" Extension dns type example.org. Précise si tout le monde peut l'utiliser,
|
||||
associé à un origin (ip d'origine)"""
|
||||
PRETTY_NAME = "Extensions dns"
|
||||
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
|
@ -173,12 +193,15 @@ class Extension(models.Model):
|
|||
|
||||
@cached_property
|
||||
def dns_entry(self):
|
||||
""" Une entrée DNS A"""
|
||||
return "@ IN A " + str(self.origin)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Mx(models.Model):
|
||||
""" Entrées des MX. Enregistre la zone (extension) associée et la priorité
|
||||
Todo : pouvoir associer un MX à une interface """
|
||||
PRETTY_NAME = "Enregistrements MX"
|
||||
|
||||
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
||||
|
@ -206,6 +229,7 @@ class Ns(models.Model):
|
|||
return str(self.zone) + ' ' + str(self.ns)
|
||||
|
||||
class Text(models.Model):
|
||||
""" Un enregistrement TXT associé à une extension"""
|
||||
PRETTY_NAME = "Enregistrement text"
|
||||
|
||||
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
||||
|
@ -220,6 +244,12 @@ class Text(models.Model):
|
|||
return str(self.field1) + " IN TXT " + str(self.field2)
|
||||
|
||||
class Interface(models.Model):
|
||||
""" Une interface. Objet clef de l'application machine :
|
||||
- une address mac unique. Possibilité de la rendre unique avec le typemachine
|
||||
- une onetoone vers IpList pour attribution ipv4
|
||||
- le type parent associé au range ip et à l'extension
|
||||
- un objet domain associé contenant son nom
|
||||
- la liste des ports oiuvert"""
|
||||
PRETTY_NAME = "Interface"
|
||||
|
||||
ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True)
|
||||
|
@ -239,6 +269,7 @@ class Interface(models.Model):
|
|||
|
||||
@cached_property
|
||||
def ipv6_object(self):
|
||||
""" Renvoie un objet type ipv6 à partir du prefix associé à l'iptype parent"""
|
||||
if self.type.ip_type.prefix_v6:
|
||||
return EUI(self.mac_address).ipv6(IPNetwork(self.type.ip_type.prefix_v6).network)
|
||||
else:
|
||||
|
@ -246,18 +277,23 @@ class Interface(models.Model):
|
|||
|
||||
@cached_property
|
||||
def ipv6(self):
|
||||
""" Renvoie l'ipv6 en str. Mise en cache et propriété de l'objet"""
|
||||
return str(self.ipv6_object)
|
||||
|
||||
def mac_bare(self):
|
||||
""" Formatage de la mac type mac_bare"""
|
||||
return str(EUI(self.mac_address, dialect=mac_bare)).lower()
|
||||
|
||||
def filter_macaddress(self):
|
||||
""" Tente un formatage mac_bare, si échoue, lève une erreur de validation"""
|
||||
try:
|
||||
self.mac_address = str(EUI(self.mac_address))
|
||||
except :
|
||||
raise ValidationError("La mac donnée est invalide")
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
""" Formate l'addresse mac en mac_bare (fonction filter_mac)
|
||||
et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
|
||||
self.filter_macaddress()
|
||||
self.mac_address = str(EUI(self.mac_address)) or None
|
||||
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
|
||||
|
@ -274,6 +310,7 @@ class Interface(models.Model):
|
|||
return
|
||||
|
||||
def unassign_ipv4(self):
|
||||
""" Sans commentaire, désassigne une ipv4"""
|
||||
self.ipv4 = None
|
||||
|
||||
def update_type(self):
|
||||
|
@ -296,15 +333,20 @@ class Interface(models.Model):
|
|||
return str(domain)
|
||||
|
||||
def has_private_ip(self):
|
||||
""" True si l'ip associée est privée"""
|
||||
if self.ipv4:
|
||||
return IPAddress(str(self.ipv4)).is_private()
|
||||
else:
|
||||
return False
|
||||
|
||||
def may_have_port_open(self):
|
||||
""" True si l'interface a une ip et une ip publique.
|
||||
Permet de ne pas exporter des ouvertures sur des ip privées (useless)"""
|
||||
return self.ipv4 and not self.has_private_ip()
|
||||
|
||||
class Domain(models.Model):
|
||||
""" Objet domain. Enregistrement A et CNAME en même temps : permet de stocker les
|
||||
alias et les nom de machines, suivant si interface_parent ou cname sont remplis"""
|
||||
PRETTY_NAME = "Domaine dns"
|
||||
|
||||
interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
@ -316,6 +358,8 @@ class Domain(models.Model):
|
|||
unique_together = (("name", "extension"),)
|
||||
|
||||
def get_extension(self):
|
||||
""" Retourne l'extension de l'interface parente si c'est un A
|
||||
Retourne l'extension propre si c'est un cname, renvoie None sinon"""
|
||||
if self.interface_parent:
|
||||
return self.interface_parent.type.ip_type.extension
|
||||
elif hasattr(self,'extension'):
|
||||
|
@ -324,6 +368,11 @@ class Domain(models.Model):
|
|||
return None
|
||||
|
||||
def clean(self):
|
||||
""" Validation :
|
||||
- l'objet est bien soit A soit CNAME
|
||||
- le cname est pas pointé sur lui-même
|
||||
- le nom contient bien les caractères autorisés par la norme dns et moins de 63 caractères au total
|
||||
- le couple nom/extension est bien unique"""
|
||||
if self.get_extension():
|
||||
self.extension=self.get_extension()
|
||||
""" Validation du nom de domaine, extensions dans type de machine, prefixe pas plus long que 63 caractères """
|
||||
|
@ -342,10 +391,12 @@ class Domain(models.Model):
|
|||
|
||||
@cached_property
|
||||
def dns_entry(self):
|
||||
""" Une entrée DNS"""
|
||||
if self.cname:
|
||||
return str(self.name) + " IN CNAME " + str(self.cname) + "."
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Empèche le save sans extension valide. Force à avoir appellé clean avant"""
|
||||
if not self.get_extension():
|
||||
raise ValidationError("Extension invalide")
|
||||
self.full_clean()
|
||||
|
@ -362,9 +413,11 @@ class IpList(models.Model):
|
|||
|
||||
@cached_property
|
||||
def need_infra(self):
|
||||
""" Permet de savoir si un user basique peut assigner cette ip ou non"""
|
||||
return self.ip_type.need_infra
|
||||
|
||||
def clean(self):
|
||||
""" Erreur si l'ip_type est incorrect"""
|
||||
if not str(self.ipv4) in self.ip_type.ip_set_as_str:
|
||||
raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!")
|
||||
return
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- 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.
|
||||
|
|
|
@ -27,30 +27,36 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
|
||||
<table class="table">
|
||||
<colgroup>
|
||||
<col>
|
||||
<col>
|
||||
<col>
|
||||
<col width="{% if ipv6_enabled %}300{% else %}150{% endif %}px">
|
||||
<col width="144px">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
<th>Proprietaire</th>
|
||||
<th>Nom dns</th>
|
||||
<th>Type</th>
|
||||
<th>Mac</th>
|
||||
<th>IP</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<th>Nom DNS</th>
|
||||
<th>Type</th>
|
||||
<th>MAC</th>
|
||||
<th>IP</th>
|
||||
<th>Actions</th>
|
||||
<tbody>
|
||||
{% for machine in machines_list %}
|
||||
{% for interface in machine.interface_set.all %}
|
||||
<tr class="active">
|
||||
{% if forloop.first %}
|
||||
<td rowspan="{{ machine.interface_set.all|length }}">
|
||||
<tr class="info">
|
||||
<td colspan="4">
|
||||
<b>{{ machine.name }}</b> <i class="glyphicon glyphicon-chevron-right"></i>
|
||||
<a href="{% url 'users:profil' userid=machine.user.id %}" title="Voir le profil">
|
||||
<i class="glyphicon glyphicon-user"></i> {{ machine.user }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %}
|
||||
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
|
||||
{% include 'buttons/history.html' with href='machines:history' name='machine' id=machine.id %}
|
||||
{% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %}
|
||||
</td>
|
||||
<td rowspan="{{ machine.interface_set.all|length }}">
|
||||
<a href="{% url 'users:profil' userid=machine.user.id %}"><b>{{ machine.user }}</b></a>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% for interface in machine.interface_set.all %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if interface.domain.related_domain.all %}
|
||||
<div class="dropdown">
|
||||
|
@ -72,19 +78,26 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{{ interface.domain }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ interface.type }}</td>
|
||||
<td>{{ interface.mac_address }}</td>
|
||||
<td><b>IPv4</b> {{ interface.ipv4 }}
|
||||
{% if ipv6_enabled %}
|
||||
<br>
|
||||
<b>IPv6</b> {{ interface.ipv6 }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
Modifier <span class="caret"></span>
|
||||
{{ interface.type }}
|
||||
</td>
|
||||
<td>
|
||||
{{ interface.mac_address }}
|
||||
</td>
|
||||
<td>
|
||||
<b>IPv4</b> {{ interface.ipv4 }}
|
||||
<br>
|
||||
{% if ipv6_enabled and interface.ipv6 != 'None'%}
|
||||
<b>IPv6</b> {{ interface.ipv6 }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="dropdown" style="width: 128px;">
|
||||
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
|
||||
<i class="glyphicon glyphicon-edit"></i> <span class="caret"></span>
|
||||
</button>
|
||||
{% include 'buttons/history.html' with href='machines:history' name='interface' id=interface.id %}
|
||||
{% include 'buttons/suppr.html' with href='machines:del-interface' id=interface.id %}
|
||||
<ul class="dropdown-menu" aria-labelledby="editioninterface">
|
||||
<li>
|
||||
<a href="{% url 'machines:edit-interface' interface.id %}">
|
||||
|
@ -96,19 +109,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<i class="glyphicon glyphicon-edit"></i> Gerer les alias
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'machines:port-config' interface.id%}">
|
||||
<i class="glyphicon glyphicon-edit"></i> Gerer la configuration des ports
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'machines:history' 'interface' interface.id %}">
|
||||
<i class="glyphicon glyphicon-time"></i> Historique
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'machines:del-interface' interface.id %}">
|
||||
<i class="glyphicon glyphicon-trash"></i> Supprimer
|
||||
<a href="{% url 'machines:port-config' interface.id%}">
|
||||
<i class="glyphicon glyphicon-edit"></i> Gerer la configuration des ports
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -120,5 +123,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<td colspan="8"></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ quelques clics.
|
|||
Copyright © 2017 Gabriel Détraz
|
||||
Copyright © 2017 Goulven Kermarec
|
||||
Copyright © 2017 Augustin Lemesle
|
||||
Copyright © 2017 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
|
||||
|
@ -24,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endcomment %}
|
||||
|
||||
{% load bootstrap3 %}
|
||||
{% load bootstrap_form_typeahead %}
|
||||
|
||||
{% block title %}Création et modification de machines{% endblock %}
|
||||
|
||||
|
@ -38,16 +40,30 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% bootstrap_form_errors domainform %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<form class="form" method="post">
|
||||
{% csrf_token %}
|
||||
{% if machineform %}
|
||||
<h3>Machine</h3>
|
||||
{% bootstrap_form machineform %}
|
||||
{% endif %}
|
||||
{% if interfaceform %}
|
||||
{% bootstrap_form interfaceform %}
|
||||
<h3>Interface</h3>
|
||||
{% if i_bft_param %}
|
||||
{% if 'machine' in interfaceform.fields %}
|
||||
{% bootstrap_form_typeahead interfaceform 'ipv4,machine' bft_param=i_bft_param %}
|
||||
{% else %}
|
||||
{% bootstrap_form_typeahead interfaceform 'ipv4' bft_param=i_bft_param %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if 'machine' in interfaceform.fields %}
|
||||
{% bootstrap_form_typeahead interfaceform 'ipv4,machine' %}
|
||||
{% else %}
|
||||
{% bootstrap_form_typeahead interfaceform 'ipv4' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if domainform %}
|
||||
<h3>Domaine</h3>
|
||||
{% bootstrap_form domainform %}
|
||||
{% endif %}
|
||||
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
|
||||
|
|
21
machines/templatetags/__init__.py
Normal file
21
machines/templatetags/__init__.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# -*- 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 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.
|
||||
|
386
machines/templatetags/bootstrap_form_typeahead.py
Normal file
386
machines/templatetags/bootstrap_form_typeahead.py
Normal file
|
@ -0,0 +1,386 @@
|
|||
# -*- 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 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.
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.forms import TextInput
|
||||
from bootstrap3.templatetags.bootstrap3 import bootstrap_form
|
||||
from bootstrap3.utils import render_tag
|
||||
from bootstrap3.forms import render_field
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.simple_tag
|
||||
def bootstrap_form_typeahead(django_form, typeahead_fields, *args, **kwargs):
|
||||
"""
|
||||
Render a form where some specific fields are rendered using Typeahead.
|
||||
Using Typeahead really improves the performance, the speed and UX when
|
||||
dealing with very large datasets (select with 50k+ elts for instance).
|
||||
For convenience, it accepts the same parameters as a standard bootstrap
|
||||
can accept.
|
||||
|
||||
**Tag name**::
|
||||
|
||||
bootstrap_form_typeahead
|
||||
|
||||
**Parameters**:
|
||||
|
||||
form
|
||||
The form that is to be rendered
|
||||
|
||||
typeahead_fields
|
||||
A list of field names (comma separated) that should be rendered
|
||||
with typeahead instead of the default bootstrap renderer.
|
||||
|
||||
bft_param
|
||||
A dict of parameters for the bootstrap_form_typeahead tag. The
|
||||
possible parameters are the following.
|
||||
|
||||
choices
|
||||
A dict of strings representing the choices in JS. The keys of
|
||||
the dict are the names of the concerned fields. The choices
|
||||
must be an array of objects. Each of those objects must at
|
||||
least have the fields 'key' (value to send) and 'value' (value
|
||||
to display). Other fields can be added as desired.
|
||||
For a more complex structure you should also consider
|
||||
reimplementing the engine and the match_func.
|
||||
If not specified, the key is the id of the object and the value
|
||||
is its string representation as in a normal bootstrap form.
|
||||
Example :
|
||||
'choices' : {
|
||||
'field_A':'[{key:0,value:"choice0",extra:"data0"},{...},...]',
|
||||
'field_B':...,
|
||||
...
|
||||
}
|
||||
|
||||
engine
|
||||
A dict of strings representating the engine used for matching
|
||||
queries and possible values with typeahead. The keys of the
|
||||
dict are the names of the concerned fields. The string is valid
|
||||
JS code.
|
||||
If not specified, BloodHound with relevant basic properties is
|
||||
used.
|
||||
Example :
|
||||
'engine' : {'field_A': 'new Bloodhound()', 'field_B': ..., ...}
|
||||
|
||||
match_func
|
||||
A dict of strings representing a valid JS function used in the
|
||||
dataset to overload the matching engine. The keys of the dict
|
||||
are the names of the concerned fields. This function is used
|
||||
the source of the dataset. This function receives 2 parameters,
|
||||
the query and the synchronize function as specified in
|
||||
typeahead.js documentation. If needed, the local variables
|
||||
'choices_<fieldname>' and 'engine_<fieldname>' contains
|
||||
respectively the array of all possible values and the engine
|
||||
to match queries with possible values.
|
||||
If not specified, the function used display up to the 10 first
|
||||
elements if the query is empty and else the matching results.
|
||||
Example :
|
||||
'match_func' : {
|
||||
'field_A': 'function(q, sync) { engine.search(q, sync); }',
|
||||
'field_B': ...,
|
||||
...
|
||||
}
|
||||
|
||||
update_on
|
||||
A dict of list of ids that the values depends on. The engine
|
||||
and the typeahead properties are recalculated and reapplied.
|
||||
Example :
|
||||
'addition' : {
|
||||
'field_A' : [ 'id0', 'id1', ... ] ,
|
||||
'field_B' : ... ,
|
||||
...
|
||||
}
|
||||
|
||||
See boostrap_form_ for other arguments
|
||||
|
||||
**Usage**::
|
||||
|
||||
{% bootstrap_form_typeahead
|
||||
form
|
||||
[ '<field1>[,<field2>[,...]]' ]
|
||||
[ {
|
||||
[ 'choices': {
|
||||
[ '<field1>': '<choices1>'
|
||||
[, '<field2>': '<choices2>'
|
||||
[, ... ] ] ]
|
||||
} ]
|
||||
[, 'engine': {
|
||||
[ '<field1>': '<engine1>'
|
||||
[, '<field2>': '<engine2>'
|
||||
[, ... ] ] ]
|
||||
} ]
|
||||
[, 'match_func': {
|
||||
[ '<field1>': '<match_func1>'
|
||||
[, '<field2>': '<match_func2>'
|
||||
[, ... ] ] ]
|
||||
} ]
|
||||
[, 'update_on': {
|
||||
[ '<field1>': '<update_on1>'
|
||||
[, '<field2>': '<update_on2>'
|
||||
[, ... ] ] ]
|
||||
} ]
|
||||
} ]
|
||||
[ <standard boostrap_form parameters> ]
|
||||
%}
|
||||
|
||||
**Example**:
|
||||
|
||||
{% bootstrap_form_typeahead form 'ipv4' choices='[...]' %}
|
||||
"""
|
||||
|
||||
t_fields = typeahead_fields.split(',')
|
||||
params = kwargs.get('bft_param', {})
|
||||
exclude = params.get('exclude', None)
|
||||
exclude = exclude.split(',') if exclude else []
|
||||
t_choices = params.get('choices', {})
|
||||
t_engine = params.get('engine', {})
|
||||
t_match_func = params.get('match_func', {})
|
||||
t_update_on = params.get('update_on', {})
|
||||
hidden = [h.name for h in django_form.hidden_fields()]
|
||||
|
||||
form = ''
|
||||
for f_name, f_value in django_form.fields.items() :
|
||||
if not f_name in exclude :
|
||||
if f_name in t_fields and not f_name in hidden :
|
||||
f_bound = f_value.get_bound_field( django_form, f_name )
|
||||
f_value.widget = TextInput(
|
||||
attrs={
|
||||
'name': 'typeahead_'+f_name,
|
||||
'placeholder': f_value.empty_label
|
||||
}
|
||||
)
|
||||
form += render_field(
|
||||
f_value.get_bound_field( django_form, f_name ),
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
form += render_tag(
|
||||
'div',
|
||||
content = hidden_tag( f_bound, f_name ) +
|
||||
typeahead_js(
|
||||
f_name,
|
||||
f_value,
|
||||
f_bound,
|
||||
t_choices,
|
||||
t_engine,
|
||||
t_match_func,
|
||||
t_update_on
|
||||
)
|
||||
)
|
||||
else:
|
||||
form += render_field(
|
||||
f_value.get_bound_field(django_form, f_name),
|
||||
*args,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
return mark_safe( form )
|
||||
|
||||
def input_id( f_name ) :
|
||||
""" The id of the HTML input element """
|
||||
return 'id_'+f_name
|
||||
|
||||
def hidden_id( f_name ):
|
||||
""" The id of the HTML hidden input element """
|
||||
return 'typeahead_hidden_'+f_name
|
||||
|
||||
def hidden_tag( f_bound, f_name ):
|
||||
""" The HTML hidden input element """
|
||||
return render_tag(
|
||||
'input',
|
||||
attrs={
|
||||
'id': hidden_id(f_name),
|
||||
'name': f_name,
|
||||
'type': 'hidden',
|
||||
'value': f_bound.value() or ""
|
||||
}
|
||||
)
|
||||
|
||||
def typeahead_js( f_name, f_value, f_bound,
|
||||
t_choices, t_engine, t_match_func, t_update_on ) :
|
||||
""" The whole script to use """
|
||||
|
||||
choices = mark_safe( t_choices[f_name] ) if f_name in t_choices.keys() \
|
||||
else default_choices( f_value )
|
||||
|
||||
engine = mark_safe( t_engine[f_name] ) if f_name in t_engine.keys() \
|
||||
else default_engine ( f_name )
|
||||
|
||||
match_func = mark_safe(t_match_func[f_name]) \
|
||||
if f_name in t_match_func.keys() else default_match_func( f_name )
|
||||
|
||||
update_on = t_update_on[f_name] if f_name in t_update_on.keys() else []
|
||||
|
||||
js_content = (
|
||||
'var choices_{f_name} = {choices};'
|
||||
'var engine_{f_name};'
|
||||
'var setup_{f_name} = function() {{'
|
||||
'engine_{f_name} = {engine};'
|
||||
'$( "#{input_id}" ).typeahead( "destroy" );'
|
||||
'$( "#{input_id}" ).typeahead( {datasets} );'
|
||||
'}};'
|
||||
'$( "#{input_id}" ).bind( "typeahead:select", {updater} );'
|
||||
'$( "#{input_id}" ).bind( "typeahead:change", {change} );'
|
||||
'{updates}'
|
||||
'$( "#{input_id}" ).ready( function() {{'
|
||||
'setup_{f_name}();'
|
||||
'{init_input}'
|
||||
'}} );'
|
||||
).format(
|
||||
f_name = f_name,
|
||||
choices = choices,
|
||||
engine = engine,
|
||||
input_id = input_id( f_name ),
|
||||
datasets = default_datasets( f_name, match_func ),
|
||||
updater = typeahead_updater( f_name ),
|
||||
change = typeahead_change( f_name ),
|
||||
updates = ''.join( [ (
|
||||
'$( "#{u_id}" ).change( function() {{'
|
||||
'setup_{f_name}();'
|
||||
'{reset_input}'
|
||||
'}} );'
|
||||
).format(
|
||||
u_id = u_id,
|
||||
reset_input = reset_input( f_name ),
|
||||
f_name = f_name
|
||||
) for u_id in update_on ]
|
||||
),
|
||||
init_input = init_input( f_name, f_bound ),
|
||||
)
|
||||
|
||||
return render_tag( 'script', content=mark_safe( js_content ) )
|
||||
|
||||
def init_input( f_name, f_bound ) :
|
||||
""" The JS script to init the fields values """
|
||||
init_key = f_bound.value() or '""'
|
||||
return (
|
||||
'$( "#{input_id}" ).typeahead("val", {init_val});'
|
||||
'$( "#{hidden_id}" ).val( {init_key} );'
|
||||
).format(
|
||||
input_id = input_id( f_name ),
|
||||
init_val = '""' if init_key == '""' else
|
||||
'engine_{f_name}.get( {init_key} )[0].value'.format(
|
||||
f_name = f_name,
|
||||
init_key = init_key
|
||||
),
|
||||
init_key = init_key,
|
||||
hidden_id = hidden_id( f_name )
|
||||
)
|
||||
|
||||
def reset_input( f_name ) :
|
||||
""" The JS script to reset the fields values """
|
||||
return (
|
||||
'$( "#{input_id}" ).typeahead("val", "");'
|
||||
'$( "#{hidden_id}" ).val( "" );'
|
||||
).format(
|
||||
input_id = input_id( f_name ),
|
||||
hidden_id = hidden_id( f_name )
|
||||
)
|
||||
|
||||
def default_choices( f_value ) :
|
||||
""" The JS script creating the variable choices_<fieldname> """
|
||||
return '[{objects}]'.format(
|
||||
objects = ','.join(
|
||||
[ '{{key:{k},value:"{v}"}}'.format(
|
||||
k = choice[0] if choice[0] != '' else '""',
|
||||
v = choice[1]
|
||||
) for choice in f_value.choices ]
|
||||
)
|
||||
)
|
||||
|
||||
def default_engine ( f_name ) :
|
||||
""" The JS script creating the variable engine_<field_name> """
|
||||
return (
|
||||
'new Bloodhound({{'
|
||||
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),'
|
||||
'queryTokenizer: Bloodhound.tokenizers.whitespace,'
|
||||
'local: choices_{f_name},'
|
||||
'identify: function(obj) {{ return obj.key; }}'
|
||||
'}})'
|
||||
).format(
|
||||
f_name = f_name
|
||||
)
|
||||
|
||||
def default_datasets( f_name, match_func ) :
|
||||
""" The JS script creating the datasets to use with typeahead """
|
||||
return (
|
||||
'{{'
|
||||
'hint: true,'
|
||||
'highlight: true,'
|
||||
'minLength: 0'
|
||||
'}},'
|
||||
'{{'
|
||||
'display: "value",'
|
||||
'name: "{f_name}",'
|
||||
'source: {match_func}'
|
||||
'}}'
|
||||
).format(
|
||||
f_name = f_name,
|
||||
match_func = match_func
|
||||
)
|
||||
|
||||
def default_match_func ( f_name ) :
|
||||
""" The JS script creating the matching function to use with typeahed """
|
||||
return (
|
||||
'function ( q, sync ) {{'
|
||||
'if ( q === "" ) {{'
|
||||
'var first = choices_{f_name}.slice( 0, 5 ).map('
|
||||
'function ( obj ) {{ return obj.key; }}'
|
||||
');'
|
||||
'sync( engine_{f_name}.get( first ) );'
|
||||
'}} else {{'
|
||||
'engine_{f_name}.search( q, sync );'
|
||||
'}}'
|
||||
'}}'
|
||||
).format(
|
||||
f_name = f_name
|
||||
)
|
||||
|
||||
def typeahead_updater( f_name ):
|
||||
""" The JS script creating the function triggered when an item is
|
||||
selected through typeahead """
|
||||
return (
|
||||
'function(evt, item) {{'
|
||||
'$( "#{hidden_id}" ).val( item.key );'
|
||||
'$( "#{hidden_id}" ).change();'
|
||||
'return item;'
|
||||
'}}'
|
||||
).format(
|
||||
hidden_id = hidden_id( f_name )
|
||||
)
|
||||
|
||||
def typeahead_change( f_name ):
|
||||
""" The JS script creating the function triggered when an item is changed
|
||||
(i.e. looses focus and value has changed since the moment it gained focus
|
||||
"""
|
||||
return (
|
||||
'function(evt) {{'
|
||||
'if ( $( "#{input_id}" ).typeahead( "val" ) === "" ) {{'
|
||||
'$( "#{hidden_id}" ).val( "" );'
|
||||
'$( "#{hidden_id}" ).change();'
|
||||
'}}'
|
||||
'}}'
|
||||
).format(
|
||||
input_id = input_id( f_name ),
|
||||
hidden_id = hidden_id( f_name )
|
||||
)
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
# -*- 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.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- 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.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- 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.
|
||||
|
@ -5,6 +6,7 @@
|
|||
# Copyright © 2017 Gabriel Détraz
|
||||
# Copyright © 2017 Goulven Kermarec
|
||||
# Copyright © 2017 Augustin Lemesle
|
||||
# Copyright © 2017 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
|
||||
|
@ -34,7 +36,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
|||
from django.template import Context, RequestContext, loader
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.db.models import ProtectedError
|
||||
from django.db.models import ProtectedError, F
|
||||
from django.forms import ValidationError, modelformset_factory
|
||||
from django.db import transaction
|
||||
from django.contrib.auth import authenticate, login
|
||||
|
@ -53,6 +55,7 @@ from .models import IpType, Machine, Interface, IpList, MachineType, Extension,
|
|||
from users.models import User
|
||||
from users.models import all_has_access
|
||||
from preferences.models import GeneralOption, OptionalMachine
|
||||
from .templatetags.bootstrap_form_typeahead import hidden_id, input_id
|
||||
|
||||
def all_active_interfaces():
|
||||
"""Renvoie l'ensemble des machines autorisées à sortir sur internet """
|
||||
|
@ -75,8 +78,90 @@ def form(ctx, template, request):
|
|||
c.update(csrf(request))
|
||||
return render(request, template, c)
|
||||
|
||||
def f_type_id( is_type_tt ):
|
||||
""" The id that will be used in HTML to store the value of the field
|
||||
type. Depends on the fact that type is generate using typeahead or not
|
||||
"""
|
||||
return hidden_id('type') if is_type_tt else input_id('type')
|
||||
|
||||
def generate_ipv4_choices( form ) :
|
||||
""" Generate the parameter choices for the bootstrap_form_typeahead tag
|
||||
"""
|
||||
f_ipv4 = form.fields['ipv4']
|
||||
used_mtype_id = []
|
||||
choices = '{"":[{key:"",value:"Choisissez d\'abord un type de machine"},'
|
||||
mtype_id = -1
|
||||
|
||||
for ip in f_ipv4.queryset.annotate(mtype_id=F('ip_type__machinetype__id')).order_by('mtype_id', 'id') :
|
||||
if mtype_id != ip.mtype_id :
|
||||
mtype_id = ip.mtype_id
|
||||
used_mtype_id.append(mtype_id)
|
||||
choices += '],"{t}":[{{key:"",value:"{v}"}},'.format(
|
||||
t = mtype_id,
|
||||
v = f_ipv4.empty_label or '""'
|
||||
)
|
||||
choices += '{{key:{k},value:"{v}"}},'.format(
|
||||
k = ip.id,
|
||||
v = ip.ipv4
|
||||
)
|
||||
|
||||
for t in form.fields['type'].queryset.exclude(id__in=used_mtype_id) :
|
||||
choices += '], "'+str(t.id)+'": ['
|
||||
choices += '{key: "", value: "' + str(f_ipv4.empty_label) + '"},'
|
||||
choices += ']}'
|
||||
return choices
|
||||
|
||||
def generate_ipv4_engine( is_type_tt ) :
|
||||
""" Generate the parameter engine for the bootstrap_form_typeahead tag
|
||||
"""
|
||||
return (
|
||||
'new Bloodhound( {{'
|
||||
'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),'
|
||||
'queryTokenizer: Bloodhound.tokenizers.whitespace,'
|
||||
'local: choices_ipv4[ $( "#{type_id}" ).val() ],'
|
||||
'identify: function( obj ) {{ return obj.key; }}'
|
||||
'}} )'
|
||||
).format(
|
||||
type_id = f_type_id( is_type_tt )
|
||||
)
|
||||
|
||||
def generate_ipv4_match_func( is_type_tt ) :
|
||||
""" Generate the parameter match_func for the bootstrap_form_typeahead tag
|
||||
"""
|
||||
return (
|
||||
'function(q, sync) {{'
|
||||
'if (q === "") {{'
|
||||
'var first = choices_ipv4[$("#{type_id}").val()].slice(0, 5);'
|
||||
'first = first.map( function (obj) {{ return obj.key; }} );'
|
||||
'sync(engine_ipv4.get(first));'
|
||||
'}} else {{'
|
||||
'engine_ipv4.search(q, sync);'
|
||||
'}}'
|
||||
'}}'
|
||||
).format(
|
||||
type_id = f_type_id( is_type_tt )
|
||||
)
|
||||
|
||||
def generate_ipv4_bft_param( form, is_type_tt ):
|
||||
""" Generate all the parameters to use with the bootstrap_form_typeahead
|
||||
tag """
|
||||
i_choices = { 'ipv4': generate_ipv4_choices( form ) }
|
||||
i_engine = { 'ipv4': generate_ipv4_engine( is_type_tt ) }
|
||||
i_match_func = { 'ipv4': generate_ipv4_match_func( is_type_tt ) }
|
||||
i_update_on = { 'ipv4': [f_type_id( is_type_tt )] }
|
||||
i_bft_param = {
|
||||
'choices': i_choices,
|
||||
'engine': i_engine,
|
||||
'match_func': i_match_func,
|
||||
'update_on': i_update_on
|
||||
}
|
||||
return i_bft_param
|
||||
|
||||
@login_required
|
||||
def new_machine(request, userid):
|
||||
""" Fonction de creation d'une machine. Cree l'objet machine, le sous objet interface et l'objet domain
|
||||
à partir de model forms.
|
||||
Trop complexe, devrait être simplifié"""
|
||||
try:
|
||||
user = User.objects.get(pk=userid)
|
||||
except User.DoesNotExist:
|
||||
|
@ -118,10 +203,13 @@ def new_machine(request, userid):
|
|||
reversion.set_comment("Création")
|
||||
messages.success(request, "La machine a été créée")
|
||||
return redirect("/users/profil/" + str(user.id))
|
||||
return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain}, 'machines/machine.html', request)
|
||||
i_bft_param = generate_ipv4_bft_param( interface, False )
|
||||
return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_bft_param': i_bft_param}, 'machines/machine.html', request)
|
||||
|
||||
@login_required
|
||||
def edit_interface(request, interfaceid):
|
||||
""" Edition d'une interface. Distingue suivant les droits les valeurs de interfaces et machines que l'user peut modifier
|
||||
infra permet de modifier le propriétaire"""
|
||||
try:
|
||||
interface = Interface.objects.get(pk=interfaceid)
|
||||
except Interface.DoesNotExist:
|
||||
|
@ -155,10 +243,12 @@ def edit_interface(request, interfaceid):
|
|||
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in domain_form.changed_data))
|
||||
messages.success(request, "La machine a été modifiée")
|
||||
return redirect("/users/profil/" + str(interface.machine.user.id))
|
||||
return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form}, 'machines/machine.html', request)
|
||||
i_bft_param = generate_ipv4_bft_param( interface_form, False )
|
||||
return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request)
|
||||
|
||||
@login_required
|
||||
def del_machine(request, machineid):
|
||||
""" Supprime une machine, interfaces en mode cascade"""
|
||||
try:
|
||||
machine = Machine.objects.get(pk=machineid)
|
||||
except Machine.DoesNotExist:
|
||||
|
@ -178,6 +268,7 @@ def del_machine(request, machineid):
|
|||
|
||||
@login_required
|
||||
def new_interface(request, machineid):
|
||||
""" Ajoute une interface et son domain associé à une machine existante"""
|
||||
try:
|
||||
machine = Machine.objects.get(pk=machineid)
|
||||
except Machine.DoesNotExist:
|
||||
|
@ -211,10 +302,12 @@ def new_interface(request, machineid):
|
|||
reversion.set_comment("Création")
|
||||
messages.success(request, "L'interface a été ajoutée")
|
||||
return redirect("/users/profil/" + str(machine.user.id))
|
||||
return form({'interfaceform': interface_form, 'domainform': domain_form}, 'machines/machine.html', request)
|
||||
i_bft_param = generate_ipv4_bft_param( interface_form, False )
|
||||
return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_bft_param': i_bft_param}, 'machines/machine.html', request)
|
||||
|
||||
@login_required
|
||||
def del_interface(request, interfaceid):
|
||||
""" Supprime une interface. Domain objet en mode cascade"""
|
||||
try:
|
||||
interface = Interface.objects.get(pk=interfaceid)
|
||||
except Interface.DoesNotExist:
|
||||
|
@ -238,6 +331,7 @@ def del_interface(request, interfaceid):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def add_iptype(request):
|
||||
""" Ajoute un range d'ip. Intelligence dans le models, fonction views minimaliste"""
|
||||
iptype = IpTypeForm(request.POST or None)
|
||||
if iptype.is_valid():
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
|
@ -251,6 +345,7 @@ def add_iptype(request):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def edit_iptype(request, iptypeid):
|
||||
""" Edition d'un range. Ne permet pas de le redimensionner pour éviter l'incohérence"""
|
||||
try:
|
||||
iptype_instance = IpType.objects.get(pk=iptypeid)
|
||||
except IpType.DoesNotExist:
|
||||
|
@ -269,6 +364,7 @@ def edit_iptype(request, iptypeid):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def del_iptype(request):
|
||||
""" Suppression d'un range ip. Supprime les objets ip associés"""
|
||||
iptype = DelIpTypeForm(request.POST or None)
|
||||
if iptype.is_valid():
|
||||
iptype_dels = iptype.cleaned_data['iptypes']
|
||||
|
@ -761,13 +857,13 @@ def index(request):
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_iptype(request):
|
||||
iptype_list = IpType.objects.select_related('extension').order_by('type')
|
||||
iptype_list = IpType.objects.select_related('extension').select_related('vlan').order_by('type')
|
||||
return render(request, 'machines/index_iptype.html', {'iptype_list':iptype_list})
|
||||
|
||||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_vlan(request):
|
||||
vlan_list = Vlan.objects.order_by('vlan_id')
|
||||
vlan_list = Vlan.objects.prefetch_related('iptype_set').order_by('vlan_id')
|
||||
return render(request, 'machines/index_vlan.html', {'vlan_list':vlan_list})
|
||||
|
||||
@login_required
|
||||
|
@ -779,7 +875,7 @@ def index_machinetype(request):
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_nas(request):
|
||||
nas_list = Nas.objects.select_related('machine_type').order_by('name')
|
||||
nas_list = Nas.objects.select_related('machine_type').select_related('nas_type').order_by('name')
|
||||
return render(request, 'machines/index_nas.html', {'nas_list':nas_list})
|
||||
|
||||
@login_required
|
||||
|
@ -807,8 +903,8 @@ def index_alias(request, interfaceid):
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_service(request):
|
||||
service_list = Service.objects.all()
|
||||
servers_list = Service_link.objects.all()
|
||||
service_list = Service.objects.prefetch_related('service_link_set__server__domain__extension').all()
|
||||
servers_list = Service_link.objects.select_related('server__domain__extension').select_related('service').all()
|
||||
return render(request, 'machines/index_service.html', {'service_list':service_list, 'servers_list':servers_list})
|
||||
|
||||
@login_required
|
||||
|
@ -916,7 +1012,7 @@ def history(request, object, id):
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_portlist(request):
|
||||
port_list = OuverturePortList.objects.all().order_by('name')
|
||||
port_list = OuverturePortList.objects.prefetch_related('ouvertureport_set').prefetch_related('interface_set').order_by('name')
|
||||
return render(request, "machines/index_portlist.html", {'port_list':port_list})
|
||||
|
||||
@login_required
|
||||
|
|
93
static/css/typeaheadjs.css
Normal file
93
static/css/typeaheadjs.css
Normal file
|
@ -0,0 +1,93 @@
|
|||
span.twitter-typeahead .tt-menu,
|
||||
span.twitter-typeahead .tt-dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
float: left;
|
||||
min-width: 160px;
|
||||
padding: 5px 0;
|
||||
margin: 2px 0 0;
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
background-clip: padding-box;
|
||||
}
|
||||
span.twitter-typeahead .tt-suggestion {
|
||||
display: block;
|
||||
padding: 3px 20px;
|
||||
clear: both;
|
||||
font-weight: normal;
|
||||
line-height: 1.42857143;
|
||||
color: #333333;
|
||||
white-space: nowrap;
|
||||
}
|
||||
span.twitter-typeahead .tt-suggestion.tt-cursor,
|
||||
span.twitter-typeahead .tt-suggestion:hover,
|
||||
span.twitter-typeahead .tt-suggestion:focus {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
outline: 0;
|
||||
background-color: #337ab7;
|
||||
}
|
||||
.input-group.input-group-lg span.twitter-typeahead .form-control {
|
||||
height: 46px;
|
||||
padding: 10px 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.3333333;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.input-group.input-group-sm span.twitter-typeahead .form-control {
|
||||
height: 30px;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
span.twitter-typeahead {
|
||||
width: 100%;
|
||||
}
|
||||
.input-group span.twitter-typeahead {
|
||||
display: block !important;
|
||||
height: 34px;
|
||||
}
|
||||
.input-group span.twitter-typeahead .tt-menu,
|
||||
.input-group span.twitter-typeahead .tt-dropdown-menu {
|
||||
top: 32px !important;
|
||||
}
|
||||
.input-group span.twitter-typeahead:not(:first-child):not(:last-child) .form-control {
|
||||
border-radius: 0;
|
||||
}
|
||||
.input-group span.twitter-typeahead:first-child .form-control {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
.input-group span.twitter-typeahead:last-child .form-control {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.input-group.input-group-sm span.twitter-typeahead {
|
||||
height: 30px;
|
||||
}
|
||||
.input-group.input-group-sm span.twitter-typeahead .tt-menu,
|
||||
.input-group.input-group-sm span.twitter-typeahead .tt-dropdown-menu {
|
||||
top: 30px !important;
|
||||
}
|
||||
.input-group.input-group-lg span.twitter-typeahead {
|
||||
height: 46px;
|
||||
}
|
||||
.input-group.input-group-lg span.twitter-typeahead .tt-menu,
|
||||
.input-group.input-group-lg span.twitter-typeahead .tt-dropdown-menu {
|
||||
top: 46px !important;
|
||||
}
|
4840
static/js/handlebars.js
Normal file
4840
static/js/handlebars.js
Normal file
File diff suppressed because one or more lines are too long
2451
static/js/typeahead.js
Normal file
2451
static/js/typeahead.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -32,8 +32,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<head>
|
||||
{# Load CSS and JavaScript #}
|
||||
{% bootstrap_css %}
|
||||
<link href="/static/css/typeaheadjs.css" rel="stylesheet">
|
||||
|
||||
{% bootstrap_javascript %}
|
||||
<script src="/static/js/typeahead.js"></script>
|
||||
<script src="/static/js/handlebars.js"></script>
|
||||
<link rel="stylesheet" href="{% static "/css/base.css" %}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ site_name }} : {% block title %}Accueil{% endblock %}</title>
|
||||
|
|
|
@ -34,17 +34,11 @@ import reversion
|
|||
|
||||
from machines.models import Vlan
|
||||
|
||||
def make_port_related(port):
|
||||
related_port = port.related
|
||||
related_port.related = port
|
||||
related_port.save()
|
||||
|
||||
def clean_port_related(port):
|
||||
related_port = port.related_port
|
||||
related_port.related = None
|
||||
related_port.save()
|
||||
|
||||
class Stack(models.Model):
|
||||
""" Un objet stack. Regrouppe des switchs en foreign key
|
||||
, contient une id de stack, un switch id min et max dans
|
||||
le stack"""
|
||||
PRETTY_NAME = "Stack de switchs"
|
||||
|
||||
name = models.CharField(max_length=32, blank=True, null=True)
|
||||
|
@ -57,15 +51,25 @@ class Stack(models.Model):
|
|||
return " ".join([self.name, self.stack_id])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
if not self.name:
|
||||
self.name = self.stack_id
|
||||
super(Stack, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
""" Verification que l'id_max < id_min"""
|
||||
if self.member_id_max < self.member_id_min:
|
||||
raise ValidationError({'member_id_max':"L'id maximale est inférieure à l'id minimale"})
|
||||
|
||||
class Switch(models.Model):
|
||||
""" Definition d'un switch. Contient un nombre de ports (number),
|
||||
un emplacement (location), un stack parent (optionnel, stack)
|
||||
et un id de membre dans le stack (stack_member_id)
|
||||
relié en onetoone à une interface
|
||||
Pourquoi ne pas avoir fait hériter switch de interface ?
|
||||
Principalement par méconnaissance de la puissance de cette façon de faire.
|
||||
Ceci étant entendu, django crée en interne un onetoone, ce qui a un
|
||||
effet identique avec ce que l'on fait ici"""
|
||||
PRETTY_NAME = "Switch / Commutateur"
|
||||
|
||||
switch_interface = models.OneToOneField('machines.Interface', on_delete=models.CASCADE)
|
||||
|
@ -82,6 +86,7 @@ class Switch(models.Model):
|
|||
return str(self.location) + ' ' + str(self.switch_interface)
|
||||
|
||||
def clean(self):
|
||||
""" Verifie que l'id stack est dans le bon range"""
|
||||
if self.stack is not None:
|
||||
if self.stack_member_id is not None:
|
||||
if (self.stack_member_id > self.stack.member_id_max) or (self.stack_member_id < self.stack.member_id_min):
|
||||
|
@ -90,6 +95,20 @@ class Switch(models.Model):
|
|||
raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"})
|
||||
|
||||
class Port(models.Model):
|
||||
""" Definition d'un port. Relié à un switch(foreign_key),
|
||||
un port peut etre relié de manière exclusive à :
|
||||
- une chambre (room)
|
||||
- une machine (serveur etc) (machine_interface)
|
||||
- un autre port (uplink) (related)
|
||||
Champs supplémentaires :
|
||||
- RADIUS (mode STRICT : connexion sur port uniquement si machine
|
||||
d'un adhérent à jour de cotisation et que la chambre est également à jour de cotisation
|
||||
mode COMMON : vérification uniquement du statut de la machine
|
||||
mode NO : accepte toute demande venant du port et place sur le vlan normal
|
||||
mode BLOQ : rejet de toute authentification
|
||||
- vlan_force : override la politique générale de placement vlan, permet
|
||||
de forcer un port sur un vlan particulier. S'additionne à la politique
|
||||
RADIUS"""
|
||||
PRETTY_NAME = "Port de switch"
|
||||
STATES = (
|
||||
('NO', 'NO'),
|
||||
|
@ -110,7 +129,26 @@ class Port(models.Model):
|
|||
class Meta:
|
||||
unique_together = ('switch', 'port')
|
||||
|
||||
def make_port_related(self):
|
||||
""" Synchronise le port distant sur self"""
|
||||
related_port = self.related
|
||||
related_port.related = self
|
||||
related_port.save()
|
||||
|
||||
def clean_port_related(self):
|
||||
""" Supprime la relation related sur self"""
|
||||
related_port = self.related_port
|
||||
related_port.related = None
|
||||
related_port.save()
|
||||
|
||||
def clean(self):
|
||||
""" Verifie que un seul de chambre, interface_parent et related_port est rempli.
|
||||
Verifie que le related n'est pas le port lui-même....
|
||||
Verifie que le related n'est pas déjà occupé par une machine ou une chambre. Si
|
||||
ce n'est pas le cas, applique la relation related
|
||||
Si un port related point vers self, on nettoie la relation
|
||||
A priori pas d'autre solution que de faire ça à la main. A priori tout cela est dans
|
||||
un bloc transaction, donc pas de problème de cohérence"""
|
||||
if hasattr(self, 'switch'):
|
||||
if self.port > self.switch.number:
|
||||
raise ValidationError("Ce port ne peut exister, numero trop élevé")
|
||||
|
@ -122,14 +160,15 @@ class Port(models.Model):
|
|||
if self.related.machine_interface or self.related.room:
|
||||
raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation")
|
||||
else:
|
||||
make_port_related(self)
|
||||
self.make_port_related()
|
||||
elif hasattr(self, 'related_port'):
|
||||
clean_port_related(self)
|
||||
self.clean_port_related()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.switch) + " - " + str(self.port)
|
||||
|
||||
class Room(models.Model):
|
||||
""" Une chambre/local contenant une prise murale"""
|
||||
PRETTY_NAME = "Chambre/ Prise murale"
|
||||
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
|
|
|
@ -44,12 +44,14 @@ from preferences.models import AssoOption, GeneralOption
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index(request):
|
||||
switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain')
|
||||
""" Vue d'affichage de tous les swicthes"""
|
||||
switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain').select_related('stack')
|
||||
return render(request, 'topologie/index.html', {'switch_list': switch_list})
|
||||
|
||||
@login_required
|
||||
@permission_required('cableur')
|
||||
def history(request, object, id):
|
||||
""" Vue générique pour afficher l'historique complet d'un objet"""
|
||||
if object == 'switch':
|
||||
try:
|
||||
object_instance = Switch.objects.get(pk=id)
|
||||
|
@ -95,6 +97,7 @@ def history(request, object, id):
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_port(request, switch_id):
|
||||
""" Affichage de l'ensemble des ports reliés à un switch particulier"""
|
||||
try:
|
||||
switch = Switch.objects.get(pk=switch_id)
|
||||
except Switch.DoesNotExist:
|
||||
|
@ -106,6 +109,7 @@ def index_port(request, switch_id):
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_room(request):
|
||||
""" Affichage de l'ensemble des chambres"""
|
||||
room_list = Room.objects.order_by('name')
|
||||
options, created = GeneralOption.objects.get_or_create()
|
||||
pagination_number = options.pagination_number
|
||||
|
@ -124,13 +128,14 @@ def index_room(request):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def index_stack(request):
|
||||
stack_list = Stack.objects.order_by('name')
|
||||
stack_list = Stack.objects.order_by('name').prefetch_related('switch_set__switch_interface__domain__extension')
|
||||
return render(request, 'topologie/index_stack.html', {'stack_list': stack_list})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required('infra')
|
||||
def new_port(request, switch_id):
|
||||
""" Nouveau port"""
|
||||
try:
|
||||
switch = Switch.objects.get(pk=switch_id)
|
||||
except Switch.DoesNotExist:
|
||||
|
@ -154,6 +159,7 @@ def new_port(request, switch_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def edit_port(request, port_id):
|
||||
""" Edition d'un port. Permet de changer le switch parent et l'affectation du port"""
|
||||
try:
|
||||
port_object = Port.objects.select_related('switch__switch_interface__domain__extension').select_related('machine_interface__domain__extension').select_related('machine_interface__switch').select_related('room').select_related('related').get(pk=port_id)
|
||||
except Port.DoesNotExist:
|
||||
|
@ -172,6 +178,7 @@ def edit_port(request, port_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def del_port(request,port_id):
|
||||
""" Supprime le port"""
|
||||
try:
|
||||
port = Port.objects.get(pk=port_id)
|
||||
except Port.DoesNotExist:
|
||||
|
@ -263,6 +270,9 @@ def edit_switchs_stack(request,stack_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def new_switch(request):
|
||||
""" Creation d'un switch. Cree en meme temps l'interface et la machine associée.
|
||||
Vue complexe. Appelle successivement les 4 models forms adaptés : machine,
|
||||
interface, domain et switch"""
|
||||
switch = NewSwitchForm(request.POST or None)
|
||||
machine = NewMachineForm(request.POST or None)
|
||||
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
|
||||
|
@ -304,6 +314,8 @@ def new_switch(request):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def edit_switch(request, switch_id):
|
||||
""" Edition d'un switch. Permet de chambre nombre de ports, place dans le stack,
|
||||
interface et machine associée"""
|
||||
try:
|
||||
switch = Switch.objects.get(pk=switch_id)
|
||||
except Switch.DoesNotExist:
|
||||
|
@ -341,6 +353,7 @@ def edit_switch(request, switch_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def new_room(request):
|
||||
"""Nouvelle chambre """
|
||||
room = EditRoomForm(request.POST or None)
|
||||
if room.is_valid():
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
|
@ -354,6 +367,7 @@ def new_room(request):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def edit_room(request, room_id):
|
||||
""" Edition numero et details de la chambre"""
|
||||
try:
|
||||
room = Room.objects.get(pk=room_id)
|
||||
except Room.DoesNotExist:
|
||||
|
@ -372,6 +386,7 @@ def edit_room(request, room_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def del_room(request, room_id):
|
||||
""" Suppression d'un chambre"""
|
||||
try:
|
||||
room = Room.objects.get(pk=room_id)
|
||||
except Room.DoesNotExist:
|
||||
|
|
|
@ -378,7 +378,7 @@ class User(AbstractBaseUser):
|
|||
def user_interfaces(self, active=True):
|
||||
""" Renvoie toutes les interfaces dont les machines appartiennent à self
|
||||
Par defaut ne prend que les interfaces actives"""
|
||||
return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=active))
|
||||
return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=active)).select_related('domain__extension')
|
||||
|
||||
def assign_ips(self):
|
||||
""" Assign une ipv4 aux machines d'un user """
|
||||
|
|
Loading…
Reference in a new issue