; TTL
+ """
+ return (
+ ' {refresh}; refresh\n'
+ ' {retry}; retry\n'
+ ' {expire}; expire\n'
+ ' {ttl}; TTL'
+ ).format(
+ refresh=str(self.refresh).ljust(12),
+ retry=str(self.retry).ljust(12),
+ expire=str(self.expire).ljust(12),
+ ttl=str(self.ttl).ljust(12)
+ )
+
+ @cached_property
+ def dns_soa_mail(self):
+ """ Renvoie le mail dans l'enregistrement SOA """
+ mail_fields = str(self.mail).split('@')
+ return mail_fields[0].replace('.', '\\.') + '.' + mail_fields[1] + '.'
+
+ @classmethod
+ def new_default_soa(cls):
+ """ Fonction pour créer un SOA par défaut, utile pour les nouvelles
+ extensions .
+ /!\ Ne jamais supprimer ou renommer cette fonction car elle est
+ utilisée dans les migrations de la BDD. """
+ return cls.objects.get_or_create(name="SOA to edit", mail="postmaser@example.com")[0].pk
+
+
+
class Extension(models.Model):
- """ Extension dns type example.org. Précise si tout le monde peut l'utiliser,
- associé à un origin (ip d'origine)"""
+ """ 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)
need_infra = models.BooleanField(default=False)
- origin = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True)
+ origin = models.OneToOneField(
+ 'IpList',
+ on_delete=models.PROTECT,
+ blank=True,
+ null=True
+ )
+ origin_v6 = models.GenericIPAddressField(
+ protocol='IPv6',
+ null=True,
+ blank=True
+ )
+ soa = models.ForeignKey(
+ 'SOA',
+ on_delete=models.CASCADE,
+ default=SOA.new_default_soa
+ )
@cached_property
def dns_entry(self):
- """ Une entrée DNS A"""
- return "@ IN A " + str(self.origin)
+ """ Une entrée DNS A et AAAA sur origin (zone self)"""
+ entry = ""
+ if self.origin:
+ entry += "@ IN A " + str(self.origin)
+ if self.origin_v6:
+ if entry:
+ entry += "\n"
+ entry += "@ IN AAAA " + str(self.origin_v6)
+ return entry
def __str__(self):
return self.name
+
class Mx(models.Model):
- """ Entrées des MX. Enregistre la zone (extension) associée et la priorité
+ """ 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)
- priority = models.IntegerField(unique=True)
+ priority = models.PositiveIntegerField(unique=True)
name = models.OneToOneField('Domain', on_delete=models.PROTECT)
@cached_property
def dns_entry(self):
- return "@ IN MX " + str(self.priority) + " " + str(self.name)
+ """Renvoie l'entrée DNS complète pour un MX à mettre dans les
+ fichiers de zones"""
+ return "@ IN MX " + str(self.priority).ljust(3) + " " + str(self.name)
def __str__(self):
return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)
+
class Ns(models.Model):
+ """Liste des enregistrements name servers par zone considéérée"""
PRETTY_NAME = "Enregistrements NS"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
@@ -222,36 +375,47 @@ class Ns(models.Model):
@cached_property
def dns_entry(self):
- return "@ IN NS " + str(self.ns)
+ """Renvoie un enregistrement NS complet pour les filezones"""
+ return "@ IN NS " + str(self.ns)
def __str__(self):
return str(self.zone) + ' ' + str(self.ns)
+
class Text(models.Model):
""" Un enregistrement TXT associé à une extension"""
- PRETTY_NAME = "Enregistrement text"
+ PRETTY_NAME = "Enregistrement TXT"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
field1 = models.CharField(max_length=255)
field2 = models.CharField(max_length=255)
-
+
def __str__(self):
- return str(self.zone) + " : " + str(self.field1) + " " + str(self.field2)
+ return str(self.zone) + " : " + str(self.field1) + " " +\
+ str(self.field2)
@cached_property
def dns_entry(self):
- return str(self.field1) + " IN TXT " + str(self.field2)
+ """Renvoie l'enregistrement TXT complet pour le fichier de zone"""
+ return str(self.field1).ljust(15) + " 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 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)
+ ipv4 = models.OneToOneField(
+ 'IpList',
+ on_delete=models.PROTECT,
+ blank=True,
+ null=True
+ )
mac_address = MACAddressField(integer=False, unique=True)
machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
@@ -265,12 +429,14 @@ class Interface(models.Model):
user = self.machine.user
return machine.active and user.has_access()
-
@cached_property
def ipv6_object(self):
- """ Renvoie un objet type ipv6 à partir du prefix associé à l'iptype parent"""
+ """ 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)
+ return EUI(self.mac_address).ipv6(
+ IPNetwork(self.type.ip_type.prefix_v6).network
+ )
else:
return None
@@ -284,15 +450,25 @@ class Interface(models.Model):
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"""
+ """ Tente un formatage mac_bare, si échoue, lève une erreur de
+ validation"""
try:
self.mac_address = str(EUI(self.mac_address))
- except :
+ 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"""
+ # If type was an invalid value, django won't create an attribute type
+ # but try clean() as we may be able to create it from another value
+ # so even if the error as yet been detected at this point, django
+ # continues because the error might not prevent us from creating the
+ # instance.
+ # But in our case, it's impossible to create a type value so we raise
+ # the error.
+ if not hasattr(self, 'type') :
+ raise ValidationError("Le type d'ip choisi n'est pas valide")
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:
@@ -305,7 +481,8 @@ class Interface(models.Model):
if free_ips:
self.ipv4 = free_ips[0]
else:
- raise ValidationError("Il n'y a plus d'ip disponibles dans le slash")
+ raise ValidationError("Il n'y a plus d'ip disponibles\
+ dans le slash")
return
def unassign_ipv4(self):
@@ -320,8 +497,10 @@ class Interface(models.Model):
def save(self, *args, **kwargs):
self.filter_macaddress()
# On verifie la cohérence en forçant l'extension par la méthode
- if self.type.ip_type != self.ipv4.ip_type:
- raise ValidationError("L'ipv4 et le type de la machine ne correspondent pas")
+ if self.ipv4:
+ if self.type.ip_type != self.ipv4.ip_type:
+ raise ValidationError("L'ipv4 et le type de la machine ne\
+ correspondent pas")
super(Interface, self).save(*args, **kwargs)
def __str__(self):
@@ -340,18 +519,34 @@ class Interface(models.Model):
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)"""
+ 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"""
+ """ 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)
- name = models.CharField(help_text="Obligatoire et unique, ne doit pas comporter de points", max_length=255)
+ interface_parent = models.OneToOneField(
+ 'Interface',
+ on_delete=models.CASCADE,
+ blank=True,
+ null=True
+ )
+ name = models.CharField(
+ help_text="Obligatoire et unique, ne doit pas comporter de points",
+ max_length=255
+ )
extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
- cname = models.ForeignKey('self', null=True, blank=True, related_name='related_domain')
+ cname = models.ForeignKey(
+ 'self',
+ null=True,
+ blank=True,
+ related_name='related_domain'
+ )
class Meta:
unique_together = (("name", "extension"),)
@@ -361,30 +556,35 @@ class Domain(models.Model):
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'):
+ elif hasattr(self, 'extension'):
return self.extension
else:
return None
def clean(self):
- """ Validation :
+ """ 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 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 """
+ self.extension = self.get_extension()
if self.interface_parent and self.cname:
raise ValidationError("On ne peut créer à la fois A et CNAME")
- if self.cname==self:
+ if self.cname == self:
raise ValidationError("On ne peut créer un cname sur lui même")
- HOSTNAME_LABEL_PATTERN = re.compile("(?!-)[A-Z\d-]+(? 63:
- raise ValidationError("Le nom de domaine %s est trop long (maximum de 63 caractères)." % dns)
+ raise ValidationError("Le nom de domaine %s est trop long\
+ (maximum de 63 caractères)." % dns)
if not HOSTNAME_LABEL_PATTERN.match(dns):
- raise ValidationError("Ce nom de domaine %s contient des carractères interdits." % dns)
+ raise ValidationError("Ce nom de domaine %s contient des\
+ carractères interdits." % dns)
self.validate_unique()
super(Domain, self).clean()
@@ -392,10 +592,11 @@ class Domain(models.Model):
def dns_entry(self):
""" Une entrée DNS"""
if self.cname:
- return str(self.name) + " IN CNAME " + str(self.cname) + "."
+ return str(self.name).ljust(15) + " IN CNAME " + str(self.cname) + "."
def save(self, *args, **kwargs):
- """ Empèche le save sans extension valide. Force à avoir appellé clean avant"""
+ """ Empèche le save sans extension valide.
+ Force à avoir appellé clean avant"""
if not self.get_extension():
raise ValidationError("Extension invalide")
self.full_clean()
@@ -404,6 +605,7 @@ class Domain(models.Model):
def __str__(self):
return str(self.name) + str(self.extension)
+
class IpList(models.Model):
PRETTY_NAME = "Addresses ipv4"
@@ -412,13 +614,15 @@ class IpList(models.Model):
@cached_property
def need_infra(self):
- """ Permet de savoir si un user basique peut assigner cette ip ou non"""
+ """ 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!")
+ raise ValidationError("L'ipv4 et le range de l'iptype ne\
+ correspondent pas!")
return
def save(self, *args, **kwargs):
@@ -428,24 +632,38 @@ class IpList(models.Model):
def __str__(self):
return self.ipv4
+
class Service(models.Model):
""" Definition d'un service (dhcp, dns, etc)"""
+ PRETTY_NAME = "Services à générer (dhcp, dns, etc)"
+
service_type = models.CharField(max_length=255, blank=True, unique=True)
- min_time_regen = models.DurationField(default=timedelta(minutes=1), help_text="Temps minimal avant nouvelle génération du service")
- regular_time_regen = models.DurationField(default=timedelta(hours=1), help_text="Temps maximal avant nouvelle génération du service")
+ min_time_regen = models.DurationField(
+ default=timedelta(minutes=1),
+ help_text="Temps minimal avant nouvelle génération du service"
+ )
+ regular_time_regen = models.DurationField(
+ default=timedelta(hours=1),
+ help_text="Temps maximal avant nouvelle génération du service"
+ )
servers = models.ManyToManyField('Interface', through='Service_link')
def ask_regen(self):
""" Marque à True la demande de régénération pour un service x """
- Service_link.objects.filter(service=self).exclude(asked_regen=True).update(asked_regen=True)
+ Service_link.objects.filter(service=self).exclude(asked_regen=True)\
+ .update(asked_regen=True)
return
def process_link(self, servers):
- """ Django ne peut créer lui meme les relations manytomany avec table intermediaire explicite"""
- for serv in servers.exclude(pk__in=Interface.objects.filter(service=self)):
+ """ Django ne peut créer lui meme les relations manytomany avec table
+ intermediaire explicite"""
+ for serv in servers.exclude(
+ pk__in=Interface.objects.filter(service=self)
+ ):
link = Service_link(service=self, server=serv)
link.save()
- Service_link.objects.filter(service=self).exclude(server__in=servers).delete()
+ Service_link.objects.filter(service=self).exclude(server__in=servers)\
+ .delete()
return
def save(self, *args, **kwargs):
@@ -454,15 +672,20 @@ class Service(models.Model):
def __str__(self):
return str(self.service_type)
+
def regen(service):
- """ Fonction externe pour régérération d'un service, prend un objet service en arg"""
+ """ Fonction externe pour régérération d'un service, prend un objet service
+ en arg"""
obj = Service.objects.filter(service_type=service)
if obj:
obj[0].ask_regen()
return
+
class Service_link(models.Model):
""" Definition du lien entre serveurs et services"""
+ PRETTY_NAME = "Relation entre service et serveur"
+
service = models.ForeignKey('Service', on_delete=models.CASCADE)
server = models.ForeignKey('Interface', on_delete=models.CASCADE)
last_regen = models.DateTimeField(auto_now_add=True)
@@ -475,11 +698,16 @@ class Service_link(models.Model):
self.save()
def need_regen(self):
- """ Décide si le temps minimal écoulé est suffisant pour provoquer une régénération de service"""
- if (self.asked_regen and (self.last_regen + self.service.min_time_regen) < timezone.now()) or (self.last_regen + self.service.regular_time_regen) < timezone.now():
- return True
- else:
- return False
+ """ Décide si le temps minimal écoulé est suffisant pour provoquer une
+ régénération de service"""
+ return bool(
+ (self.asked_regen and (
+ self.last_regen + self.service.min_time_regen
+ ) < timezone.now()
+ ) or (
+ self.last_regen + self.service.regular_time_regen
+ ) < timezone.now()
+ )
def __str__(self):
return str(self.server) + " " + str(self.service)
@@ -487,143 +715,218 @@ class Service_link(models.Model):
class OuverturePortList(models.Model):
"""Liste des ports ouverts sur une interface."""
- name = models.CharField(help_text="Nom de la configuration des ports.", max_length=255)
+ PRETTY_NAME = "Profil d'ouverture de ports"
+
+ name = models.CharField(
+ help_text="Nom de la configuration des ports.",
+ max_length=255
+ )
def __str__(self):
return self.name
def tcp_ports_in(self):
- return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN)
-
+ """Renvoie la liste des ports ouverts en TCP IN pour ce profil"""
+ return self.ouvertureport_set.filter(
+ protocole=OuverturePort.TCP,
+ io=OuverturePort.IN
+ )
+
def udp_ports_in(self):
- return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN)
+ """Renvoie la liste des ports ouverts en UDP IN pour ce profil"""
+ return self.ouvertureport_set.filter(
+ protocole=OuverturePort.UDP,
+ io=OuverturePort.IN
+ )
def tcp_ports_out(self):
- return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT)
-
+ """Renvoie la liste des ports ouverts en TCP OUT pour ce profil"""
+ return self.ouvertureport_set.filter(
+ protocole=OuverturePort.TCP,
+ io=OuverturePort.OUT
+ )
+
def udp_ports_out(self):
- return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT)
+ """Renvoie la liste des ports ouverts en UDP OUT pour ce profil"""
+ return self.ouvertureport_set.filter(
+ protocole=OuverturePort.UDP,
+ io=OuverturePort.OUT
+ )
class OuverturePort(models.Model):
"""
Représente un simple port ou une plage de ports.
-
- Les ports de la plage sont compris entre begin et en inclus.
+
+ Les ports de la plage sont compris entre begin et en inclus.
Si begin == end alors on ne représente qu'un seul port.
+
+ On limite les ports entre 0 et 65535, tels que défini par la RFC
"""
+ PRETTY_NAME = "Plage de port ouverte"
+
TCP = 'T'
UDP = 'U'
IN = 'I'
OUT = 'O'
- begin = models.IntegerField()
- end = models.IntegerField()
- port_list = models.ForeignKey('OuverturePortList', on_delete=models.CASCADE)
+ begin = models.PositiveIntegerField(validators=[MaxValueValidator(65535)])
+ end = models.PositiveIntegerField(validators=[MaxValueValidator(65535)])
+ port_list = models.ForeignKey(
+ 'OuverturePortList',
+ on_delete=models.CASCADE
+ )
protocole = models.CharField(
- max_length=1,
- choices=(
- (TCP, 'TCP'),
- (UDP, 'UDP'),
- ),
- default=TCP,
+ max_length=1,
+ choices=(
+ (TCP, 'TCP'),
+ (UDP, 'UDP'),
+ ),
+ default=TCP,
)
io = models.CharField(
- max_length=1,
- choices=(
- (IN, 'IN'),
- (OUT, 'OUT'),
- ),
- default=OUT,
+ max_length=1,
+ choices=(
+ (IN, 'IN'),
+ (OUT, 'OUT'),
+ ),
+ default=OUT,
)
def __str__(self):
- if self.begin == self.end :
+ if self.begin == self.end:
return str(self.begin)
return '-'.join([str(self.begin), str(self.end)])
def show_port(self):
+ """Formatage plus joli, alias pour str"""
return str(self)
@receiver(post_save, sender=Machine)
def machine_post_save(sender, **kwargs):
+ """Synchronisation ldap et régen parefeu/dhcp lors de la modification
+ d'une machine"""
user = kwargs['instance'].user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
regen('dhcp')
regen('mac_ip_list')
+
@receiver(post_delete, sender=Machine)
def machine_post_delete(sender, **kwargs):
+ """Synchronisation ldap et régen parefeu/dhcp lors de la suppression
+ d'une machine"""
machine = kwargs['instance']
user = machine.user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
regen('dhcp')
regen('mac_ip_list')
+
@receiver(post_save, sender=Interface)
def interface_post_save(sender, **kwargs):
+ """Synchronisation ldap et régen parefeu/dhcp lors de la modification
+ d'une interface"""
interface = kwargs['instance']
user = interface.machine.user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
- if not interface.may_have_port_open() and interface.port_lists.all():
- interface.port_lists.clear()
# Regen services
regen('dhcp')
regen('mac_ip_list')
+
@receiver(post_delete, sender=Interface)
def interface_post_delete(sender, **kwargs):
+ """Synchronisation ldap et régen parefeu/dhcp lors de la suppression
+ d'une interface"""
interface = kwargs['instance']
user = interface.machine.user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
+
@receiver(post_save, sender=IpType)
def iptype_post_save(sender, **kwargs):
+ """Generation des objets ip après modification d'un range ip"""
iptype = kwargs['instance']
iptype.gen_ip_range()
+
@receiver(post_save, sender=MachineType)
def machine_post_save(sender, **kwargs):
+ """Mise à jour des interfaces lorsque changement d'attribution
+ d'une machinetype (changement iptype parent)"""
machinetype = kwargs['instance']
for interface in machinetype.all_interfaces():
interface.update_type()
+
@receiver(post_save, sender=Domain)
def domain_post_save(sender, **kwargs):
+ """Regeneration dns après modification d'un domain object"""
regen('dns')
+
@receiver(post_delete, sender=Domain)
def domain_post_delete(sender, **kwargs):
+ """Regeneration dns après suppression d'un domain object"""
regen('dns')
+
@receiver(post_save, sender=Extension)
def extension_post_save(sender, **kwargs):
+ """Regeneration dns après modification d'une extension"""
regen('dns')
+
@receiver(post_delete, sender=Extension)
def extension_post_selete(sender, **kwargs):
+ """Regeneration dns après suppression d'une extension"""
regen('dns')
+
+@receiver(post_save, sender=SOA)
+def soa_post_save(sender, **kwargs):
+ """Regeneration dns après modification d'un SOA"""
+ regen('dns')
+
+
+@receiver(post_delete, sender=SOA)
+def soa_post_delete(sender, **kwargs):
+ """Regeneration dns après suppresson d'un SOA"""
+ regen('dns')
+
+
@receiver(post_save, sender=Mx)
def mx_post_save(sender, **kwargs):
+ """Regeneration dns après modification d'un MX"""
regen('dns')
+
@receiver(post_delete, sender=Mx)
def mx_post_delete(sender, **kwargs):
+ """Regeneration dns après suppresson d'un MX"""
regen('dns')
+
@receiver(post_save, sender=Ns)
def ns_post_save(sender, **kwargs):
+ """Regeneration dns après modification d'un NS"""
regen('dns')
+
@receiver(post_delete, sender=Ns)
def ns_post_delete(sender, **kwargs):
+ """Regeneration dns après modification d'un NS"""
regen('dns')
+
@receiver(post_save, sender=Text)
def text_post_save(sender, **kwargs):
+ """Regeneration dns après modification d'un TXT"""
regen('dns')
+
@receiver(post_delete, sender=Text)
def text_post_delete(sender, **kwargs):
+ """Regeneration dns après modification d'un TX"""
regen('dns')
diff --git a/machines/serializers.py b/machines/serializers.py
index 2cf3d3e8..7d78ebc3 100644
--- a/machines/serializers.py
+++ b/machines/serializers.py
@@ -24,20 +24,42 @@
#Augustin Lemesle
from rest_framework import serializers
-from machines.models import Interface, IpType, Extension, IpList, MachineType, Domain, Text, Mx, Service_link, Ns
+from machines.models import (
+ Interface,
+ IpType,
+ Extension,
+ IpList,
+ MachineType,
+ Domain,
+ Text,
+ Mx,
+ Service_link,
+ Ns,
+ OuverturePortList,
+ OuverturePort
+)
+
class IpTypeField(serializers.RelatedField):
+ """Serialisation d'une iptype, renvoie son evaluation str"""
def to_representation(self, value):
return value.type
+
class IpListSerializer(serializers.ModelSerializer):
+ """Serialisation d'une iplist, ip_type etant une foreign_key,
+ on evalue sa methode str"""
ip_type = IpTypeField(read_only=True)
class Meta:
model = IpList
fields = ('ipv4', 'ip_type')
+
class InterfaceSerializer(serializers.ModelSerializer):
+ """Serialisation d'une interface, ipv4, domain et extension sont
+ des foreign_key, on les override et on les evalue avec des fonctions
+ get_..."""
ipv4 = IpListSerializer(read_only=True)
mac_address = serializers.SerializerMethodField('get_macaddress')
domain = serializers.SerializerMethodField('get_dns')
@@ -56,7 +78,9 @@ class InterfaceSerializer(serializers.ModelSerializer):
def get_macaddress(self, obj):
return str(obj.mac_address)
+
class FullInterfaceSerializer(serializers.ModelSerializer):
+ """Serialisation complete d'une interface avec l'ipv6 en plus"""
ipv4 = IpListSerializer(read_only=True)
mac_address = serializers.SerializerMethodField('get_macaddress')
domain = serializers.SerializerMethodField('get_dns')
@@ -75,24 +99,70 @@ class FullInterfaceSerializer(serializers.ModelSerializer):
def get_macaddress(self, obj):
return str(obj.mac_address)
+
class ExtensionNameField(serializers.RelatedField):
+ """Evaluation str d'un objet extension (.example.org)"""
def to_representation(self, value):
return value.name
+
class TypeSerializer(serializers.ModelSerializer):
+ """Serialisation d'un iptype : extension et la liste des
+ ouvertures de port son evalués en get_... etant des
+ foreign_key ou des relations manytomany"""
extension = ExtensionNameField(read_only=True)
+ ouverture_ports_tcp_in = serializers\
+ .SerializerMethodField('get_port_policy_input_tcp')
+ ouverture_ports_tcp_out = serializers\
+ .SerializerMethodField('get_port_policy_output_tcp')
+ ouverture_ports_udp_in = serializers\
+ .SerializerMethodField('get_port_policy_input_udp')
+ ouverture_ports_udp_out = serializers\
+ .SerializerMethodField('get_port_policy_output_udp')
class Meta:
model = IpType
- fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop')
+ fields = ('type', 'extension', 'domaine_ip_start', 'domaine_ip_stop',
+ 'ouverture_ports_tcp_in', 'ouverture_ports_tcp_out',
+ 'ouverture_ports_udp_in', 'ouverture_ports_udp_out',)
+
+ def get_port_policy(self, obj, protocole, io):
+ if obj.ouverture_ports is None:
+ return []
+ return map(
+ str,
+ obj.ouverture_ports.ouvertureport_set.filter(
+ protocole=protocole
+ ).filter(io=io)
+ )
+
+ def get_port_policy_input_tcp(self, obj):
+ """Renvoie la liste des ports ouverts en entrée tcp"""
+ return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.IN)
+
+ def get_port_policy_output_tcp(self, obj):
+ """Renvoie la liste des ports ouverts en sortie tcp"""
+ return self.get_port_policy(obj, OuverturePort.TCP, OuverturePort.OUT)
+
+ def get_port_policy_input_udp(self, obj):
+ """Renvoie la liste des ports ouverts en entrée udp"""
+ return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.IN)
+
+ def get_port_policy_output_udp(self, obj):
+ """Renvoie la liste des ports ouverts en sortie udp"""
+ return self.get_port_policy(obj, OuverturePort.UDP, OuverturePort.OUT)
+
class ExtensionSerializer(serializers.ModelSerializer):
+ """Serialisation d'une extension : origin_ip et la zone sont
+ des foreign_key donc evalués en get_..."""
origin = serializers.SerializerMethodField('get_origin_ip')
zone_entry = serializers.SerializerMethodField('get_zone_name')
+ soa = serializers.SerializerMethodField('get_soa_data')
class Meta:
model = Extension
- fields = ('name', 'origin', 'zone_entry')
+ fields = ('name', 'origin', 'origin_v6', 'zone_entry', 'soa')
def get_origin_ip(self, obj):
return obj.origin.ipv4
@@ -100,7 +170,13 @@ class ExtensionSerializer(serializers.ModelSerializer):
def get_zone_name(self, obj):
return str(obj.dns_entry)
+ def get_soa_data(self, obj):
+ return { 'mail': obj.soa.dns_soa_mail, 'param': obj.soa.dns_soa_param }
+
+
class MxSerializer(serializers.ModelSerializer):
+ """Serialisation d'un MX, evaluation du nom, de la zone
+ et du serveur cible, etant des foreign_key"""
name = serializers.SerializerMethodField('get_entry_name')
zone = serializers.SerializerMethodField('get_zone_name')
mx_entry = serializers.SerializerMethodField('get_mx_name')
@@ -118,13 +194,16 @@ class MxSerializer(serializers.ModelSerializer):
def get_mx_name(self, obj):
return str(obj.dns_entry)
+
class TextSerializer(serializers.ModelSerializer):
+ """Serialisation d'un txt : zone cible et l'entrée txt
+ sont evaluées à part"""
zone = serializers.SerializerMethodField('get_zone_name')
text_entry = serializers.SerializerMethodField('get_text_name')
class Meta:
model = Text
- fields = ('zone','text_entry','field1', 'field2')
+ fields = ('zone', 'text_entry', 'field1', 'field2')
def get_zone_name(self, obj):
return str(obj.zone.name)
@@ -132,10 +211,13 @@ class TextSerializer(serializers.ModelSerializer):
def get_text_name(self, obj):
return str(obj.dns_entry)
+
class NsSerializer(serializers.ModelSerializer):
+ """Serialisation d'un NS : la zone, l'entrée ns complète et le serveur
+ ns sont évalués à part"""
zone = serializers.SerializerMethodField('get_zone_name')
ns = serializers.SerializerMethodField('get_domain_name')
- ns_entry = serializers.SerializerMethodField('get_text_name')
+ ns_entry = serializers.SerializerMethodField('get_text_name')
class Meta:
model = Ns
@@ -150,10 +232,13 @@ class NsSerializer(serializers.ModelSerializer):
def get_text_name(self, obj):
return str(obj.dns_entry)
+
class DomainSerializer(serializers.ModelSerializer):
+ """Serialisation d'un domain, extension, cname sont des foreign_key,
+ et l'entrée complète, sont évalués à part"""
extension = serializers.SerializerMethodField('get_zone_name')
cname = serializers.SerializerMethodField('get_alias_name')
- cname_entry = serializers.SerializerMethodField('get_cname_name')
+ cname_entry = serializers.SerializerMethodField('get_cname_name')
class Meta:
model = Domain
@@ -168,7 +253,9 @@ class DomainSerializer(serializers.ModelSerializer):
def get_cname_name(self, obj):
return str(obj.dns_entry)
+
class ServiceServersSerializer(serializers.ModelSerializer):
+ """Evaluation d'un Service, et serialisation"""
server = serializers.SerializerMethodField('get_server_name')
service = serializers.SerializerMethodField('get_service_name')
need_regen = serializers.SerializerMethodField('get_regen_status')
@@ -185,3 +272,31 @@ class ServiceServersSerializer(serializers.ModelSerializer):
def get_regen_status(self, obj):
return obj.need_regen()
+
+
+class OuverturePortsSerializer(serializers.Serializer):
+ """Serialisation de l'ouverture des ports"""
+ ipv4 = serializers.SerializerMethodField()
+ ipv6 = serializers.SerializerMethodField()
+
+ def get_ipv4():
+ return {i.ipv4.ipv4:
+ {
+ "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()],
+ "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()],
+ "udp_in":[j.udp_ports_in() for j in i.port_lists.all()],
+ "udp_out":[j.udp_ports_out() for j in i.port_lists.all()],
+ }
+ for i in Interface.objects.all() if i.ipv4
+ }
+
+ def get_ipv6():
+ return {i.ipv6:
+ {
+ "tcp_in":[j.tcp_ports_in() for j in i.port_lists.all()],
+ "tcp_out":[j.tcp_ports_out()for j in i.port_lists.all()],
+ "udp_in":[j.udp_ports_in() for j in i.port_lists.all()],
+ "udp_out":[j.udp_ports_out() for j in i.port_lists.all()],
+ }
+ for i in Interface.objects.all() if i.ipv6
+ }
diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html
index 18fd7c4b..15a4c637 100644
--- a/machines/templates/machines/aff_extension.html
+++ b/machines/templates/machines/aff_extension.html
@@ -26,17 +26,25 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Extension
- Autorisation infra pour utiliser l'extension
+ Droit infra pour utiliser ?
+ Enregistrement SOA
Enregistrement A origin
-
+ {% if ipv6_enabled %}
+ Enregistrement AAAA origin
+ {% endif %}
+
{% for extension in extension_list %}
{{ extension.name }}
- {{ extension.need_infra }}
+ {{ extension.need_infra }}
+ {{ extension.soa}}
{{ extension.origin }}
-
+ {% if ipv6_enabled %}
+ {{ extension.origin_v6 }}
+ {% endif %}
+
{% if is_infra %}
{% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %}
{% endif %}
diff --git a/machines/templates/machines/aff_iptype.html b/machines/templates/machines/aff_iptype.html
index aafc4c1d..454b169d 100644
--- a/machines/templates/machines/aff_iptype.html
+++ b/machines/templates/machines/aff_iptype.html
@@ -32,6 +32,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Fin
Préfixe v6
Sur vlan
+ Ouverture ports par défault
@@ -45,6 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{{ type.domaine_ip_stop }}
{{ type.prefix_v6 }}
{{ type.vlan }}
+ {{ type.ouverture_ports }}
{% if is_infra %}
{% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %}
diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html
index 80e887d2..b59bdc23 100644
--- a/machines/templates/machines/aff_machines.html
+++ b/machines/templates/machines/aff_machines.html
@@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- Nom DNS
+ {% include "buttons/sort.html" with prefix='machine' col='name' text='Nom DNS' %}
Type
MAC
IP
@@ -44,7 +44,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% for machine in machines_list %}
- {{ machine.name }}
+ {{ machine.name|default:'Pas de nom ' }}
{{ machine.user }}
diff --git a/machines/templates/machines/aff_soa.html b/machines/templates/machines/aff_soa.html
new file mode 100644
index 00000000..3dad11c7
--- /dev/null
+++ b/machines/templates/machines/aff_soa.html
@@ -0,0 +1,56 @@
+{% 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 %}
+
+
+
+
+ Nom
+ Mail
+ Refresh
+ Retry
+ Expire
+ TTL
+
+
+
+
+ {% for soa in soa_list %}
+
+ {{ soa.name }}
+ {{ soa.mail }}
+ {{ soa.refresh }}
+ {{ soa.retry }}
+ {{ soa.expire }}
+ {{ soa.ttl }}
+
+ {% if is_infra %}
+ {% include 'buttons/edit.html' with href='machines:edit-soa' id=soa.id %}
+ {% endif %}
+ {% include 'buttons/history.html' with href='machines:history' name='soa' id=soa.id %}
+
+
+ {% endfor %}
+
+
+
diff --git a/machines/templates/machines/aff_text.html b/machines/templates/machines/aff_txt.html
similarity index 84%
rename from machines/templates/machines/aff_text.html
rename to machines/templates/machines/aff_txt.html
index f3ada132..fd7c5ee6 100644
--- a/machines/templates/machines/aff_text.html
+++ b/machines/templates/machines/aff_txt.html
@@ -25,21 +25,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- Zone concernée
- Enregistrement
-
+ Zone concernée
+ Enregistrement
+
- {% for text in text_list %}
+ {% for txt in txt_list %}
- {{ text.zone }}
- {{ text.dns_entry }}
+ {{ txt.zone }}
+ {{ txt.dns_entry }}
{% if is_infra %}
- {% include 'buttons/edit.html' with href='machines:edit-text' id=text.id %}
+ {% include 'buttons/edit.html' with href='machines:edit-txt' id=txt.id %}
{% endif %}
- {% include 'buttons/history.html' with href='machines:history' name='text' id=text.id %}
+ {% include 'buttons/history.html' with href='machines:history' name='txt' id=txt.id %}
{% endfor %}
diff --git a/machines/templates/machines/index_extension.html b/machines/templates/machines/index_extension.html
index 20587d85..ceae84c6 100644
--- a/machines/templates/machines/index_extension.html
+++ b/machines/templates/machines/index_extension.html
@@ -35,6 +35,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}
{% include "machines/aff_extension.html" with extension_list=extension_list %}
+ Liste des enregistrements SOA
+ {% if is_infra %}
+ Ajouter un enregistrement SOA
+ Supprimer un enregistrement SOA
+ {% endif %}
+ {% include "machines/aff_soa.html" with soa_list=soa_list %}
Liste des enregistrements MX
{% if is_infra %}
Ajouter un enregistrement MX
@@ -47,12 +53,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Supprimer un enregistrement NS
{% endif %}
{% include "machines/aff_ns.html" with ns_list=ns_list %}
- Liste des enregistrements Text
+ Liste des enregistrements TXT
{% if is_infra %}
- Ajouter un enregistrement TXT
- Supprimer un enregistrement TXT
+ Ajouter un enregistrement TXT
+ Supprimer un enregistrement TXT
{% endif %}
- {% include "machines/aff_text.html" with text_list=text_list %}
+ {% include "machines/aff_txt.html" with txt_list=txt_list %}
diff --git a/machines/templates/machines/index_nas.html b/machines/templates/machines/index_nas.html
index 16fb29e2..ac1fe8b4 100644
--- a/machines/templates/machines/index_nas.html
+++ b/machines/templates/machines/index_nas.html
@@ -31,8 +31,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Liste des nas
La correpondance nas-machinetype relie le type de nas à un type de machine.
Elle est utile pour l'autoenregistrement des macs par radius, et permet de choisir le type de machine à affecter aux machines en fonction du type de nas
+ {% if is_infra %}
Ajouter un type de nas
Supprimer un ou plusieurs types nas
+ {% endif %}
{% include "machines/aff_nas.html" with nas_list=nas_list %}
diff --git a/machines/templates/machines/index_vlan.html b/machines/templates/machines/index_vlan.html
index ccbfa753..ec00b0bf 100644
--- a/machines/templates/machines/index_vlan.html
+++ b/machines/templates/machines/index_vlan.html
@@ -29,8 +29,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
Liste des vlans
+ {% if is_infra %}
Ajouter un vlan
Supprimer un ou plusieurs vlan
+ {% endif %}
{% include "machines/aff_vlan.html" with vlan_list=vlan_list %}
diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html
index d34dccb9..636c8e2f 100644
--- a/machines/templates/machines/machine.html
+++ b/machines/templates/machines/machine.html
@@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
-{% load bootstrap_form_typeahead %}
+{% load massive_bootstrap_form %}
{% block title %}Création et modification de machines{% endblock %}
@@ -39,6 +39,36 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if domainform %}
{% bootstrap_form_errors domainform %}
{% endif %}
+{% if iptypeform %}
+{% bootstrap_form_errors iptypeform %}
+{% endif %}
+{% if machinetypeform %}
+{% bootstrap_form_errors machinetypeform %}
+{% endif %}
+{% if extensionform %}
+{% bootstrap_form_errors extensionform %}
+{% endif %}
+{% if mxform %}
+{% bootstrap_form_errors mxform %}
+{% endif %}
+{% if nsform %}
+{% bootstrap_form_errors nsform %}
+{% endif %}
+{% if txtform %}
+{% bootstrap_form_errors txtform %}
+{% endif %}
+{% if aliasform %}
+{% bootstrap_form_errors aliasform %}
+{% endif %}
+{% if serviceform %}
+{% bootstrap_form_errors serviceform %}
+{% endif %}
+{% if vlanform %}
+{% bootstrap_form_errors vlanform %}
+{% endif %}
+{% if nasform %}
+{% bootstrap_form_errors nasform %}
+{% endif %}
diff --git a/machines/templates/machines/sidebar.html b/machines/templates/machines/sidebar.html
index e635d69a..6ca3a07f 100644
--- a/machines/templates/machines/sidebar.html
+++ b/machines/templates/machines/sidebar.html
@@ -58,7 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if is_cableur %}
- Configuration de ports
+ Ouverture de ports
{%endif%}
{% endblock %}
diff --git a/machines/templatetags/bootstrap_form_typeahead.py b/machines/templatetags/bootstrap_form_typeahead.py
deleted file mode 100644
index 05dd3147..00000000
--- a/machines/templatetags/bootstrap_form_typeahead.py
+++ /dev/null
@@ -1,386 +0,0 @@
-# -*- 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_' and 'engine_' 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
- [ '[,[,...]]' ]
- [ {
- [ 'choices': {
- [ '': ''
- [, '': ''
- [, ... ] ] ]
- } ]
- [, 'engine': {
- [ '': ''
- [, '': ''
- [, ... ] ] ]
- } ]
- [, 'match_func': {
- [ '': ''
- [, '': ''
- [, ... ] ] ]
- } ]
- [, 'update_on': {
- [ '': ''
- [, '': ''
- [, ... ] ] ]
- } ]
- } ]
- [ ]
- %}
-
- **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_ """
- 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_ """
- 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 )
- )
-
diff --git a/machines/urls.py b/machines/urls.py
index 886c7c0a..3669b5b9 100644
--- a/machines/urls.py
+++ b/machines/urls.py
@@ -44,12 +44,15 @@ urlpatterns = [
url(r'^add_extension/$', views.add_extension, name='add-extension'),
url(r'^edit_extension/(?P[0-9]+)$', views.edit_extension, name='edit-extension'),
url(r'^del_extension/$', views.del_extension, name='del-extension'),
+ url(r'^add_soa/$', views.add_soa, name='add-soa'),
+ url(r'^edit_soa/(?P[0-9]+)$', views.edit_soa, name='edit-soa'),
+ url(r'^del_soa/$', views.del_soa, name='del-soa'),
url(r'^add_mx/$', views.add_mx, name='add-mx'),
url(r'^edit_mx/(?P[0-9]+)$', views.edit_mx, name='edit-mx'),
url(r'^del_mx/$', views.del_mx, name='del-mx'),
- url(r'^add_text/$', views.add_text, name='add-text'),
- url(r'^edit_text/(?P[0-9]+)$', views.edit_text, name='edit-text'),
- url(r'^del_text/$', views.del_text, name='del-text'),
+ url(r'^add_txt/$', views.add_txt, name='add-txt'),
+ url(r'^edit_txt/(?P[0-9]+)$', views.edit_txt, name='edit-txt'),
+ url(r'^del_txt/$', views.del_txt, name='del-txt'),
url(r'^add_ns/$', views.add_ns, name='add-ns'),
url(r'^edit_ns/(?P[0-9]+)$', views.edit_ns, name='edit-ns'),
url(r'^del_ns/$', views.del_ns, name='del-ns'),
@@ -74,9 +77,10 @@ urlpatterns = [
url(r'^history/(?Pinterface)/(?P[0-9]+)$', views.history, name='history'),
url(r'^history/(?Pmachinetype)/(?P[0-9]+)$', views.history, name='history'),
url(r'^history/(?Pextension)/(?P[0-9]+)$', views.history, name='history'),
+ url(r'^history/(?Psoa)/(?P[0-9]+)$', views.history, name='history'),
url(r'^history/(?Pmx)/(?P[0-9]+)$', views.history, name='history'),
url(r'^history/(?Pns)/(?P[0-9]+)$', views.history, name='history'),
- url(r'^history/(?Ptext)/(?P[0-9]+)$', views.history, name='history'),
+ url(r'^history/(?Ptxt)/(?P[0-9]+)$', views.history, name='history'),
url(r'^history/(?Piptype)/(?P[0-9]+)$', views.history, name='history'),
url(r'^history/(?Palias)/(?P[0-9]+)$', views.history, name='history'),
url(r'^history/(?Pvlan)/(?P[0-9]+)$', views.history, name='history'),
@@ -93,6 +97,7 @@ urlpatterns = [
url(r'^rest/text/$', views.text, name='text'),
url(r'^rest/zones/$', views.zones, name='zones'),
url(r'^rest/service_servers/$', views.service_servers, name='service-servers'),
+ url(r'^rest/ouverture_ports/$', views.ouverture_ports, name='ouverture-ports'),
url(r'index_portlist/$', views.index_portlist, name='index-portlist'),
url(r'^edit_portlist/(?P[0-9]+)$', views.edit_portlist, name='edit-portlist'),
url(r'^del_portlist/(?P[0-9]+)$', views.del_portlist, name='del-portlist'),
diff --git a/machines/views.py b/machines/views.py
index ac37d8c6..83cad204 100644
--- a/machines/views.py
+++ b/machines/views.py
@@ -43,56 +43,101 @@ from django.contrib.auth import authenticate, login
from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer
-from machines.serializers import FullInterfaceSerializer, InterfaceSerializer, TypeSerializer, DomainSerializer, TextSerializer, MxSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer
+from machines.serializers import ( FullInterfaceSerializer,
+ InterfaceSerializer,
+ TypeSerializer,
+ DomainSerializer,
+ TextSerializer,
+ MxSerializer,
+ ExtensionSerializer,
+ ServiceServersSerializer,
+ NsSerializer,
+ OuverturePortsSerializer
+)
from reversion import revisions as reversion
from reversion.models import Version
import re
-from .forms import NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm, MachineTypeForm, DelMachineTypeForm, ExtensionForm, DelExtensionForm, BaseEditInterfaceForm, BaseEditMachineForm
-from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasForm, DelAliasForm, NsForm, DelNsForm, TextForm, DelTextForm, MxForm, DelMxForm, VlanForm, DelVlanForm, ServiceForm, DelServiceForm, NasForm, DelNasForm
+from .forms import (
+ NewMachineForm,
+ EditMachineForm,
+ EditInterfaceForm,
+ AddInterfaceForm,
+ MachineTypeForm,
+ DelMachineTypeForm,
+ ExtensionForm,
+ DelExtensionForm,
+ BaseEditInterfaceForm,
+ BaseEditMachineForm
+)
+from .forms import (
+ EditIpTypeForm,
+ IpTypeForm,
+ DelIpTypeForm,
+ DomainForm,
+ AliasForm,
+ DelAliasForm,
+ SOAForm,
+ DelSOAForm,
+ NsForm,
+ DelNsForm,
+ TxtForm,
+ DelTxtForm,
+ MxForm,
+ DelMxForm,
+ VlanForm,
+ DelVlanForm,
+ ServiceForm,
+ DelServiceForm,
+ NasForm,
+ DelNasForm
+)
from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm
-from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text, OuverturePortList, OuverturePort
+from .models import (
+ IpType,
+ Machine,
+ Interface,
+ IpList,
+ MachineType,
+ Extension,
+ SOA,
+ Mx,
+ Ns,
+ Domain,
+ Service,
+ Service_link,
+ Vlan,
+ Nas,
+ Text,
+ OuverturePortList,
+ OuverturePort
+)
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 """
- return Interface.objects.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True)).select_related('domain').select_related('machine').select_related('type').select_related('ipv4').select_related('domain__extension').select_related('ipv4__ip_type').distinct()
-
-def all_active_assigned_interfaces():
- """ Renvoie l'ensemble des machines qui ont une ipv4 assignées et disposant de l'accès internet"""
- return all_active_interfaces().filter(ipv4__isnull=False)
-
-def all_active_interfaces_count():
- """ Version light seulement pour compter"""
- return Interface.objects.filter(machine__in=Machine.objects.filter(user__in=all_has_access()).filter(active=True))
-
-def all_active_assigned_interfaces_count():
- """ Version light seulement pour compter"""
- return all_active_interfaces_count().filter(ipv4__isnull=False)
-
-def form(ctx, template, request):
- c = ctx
- c.update(csrf(request))
- return render(request, template, c)
+from re2o.utils import (
+ all_active_assigned_interfaces,
+ all_has_access,
+ filter_active_interfaces,
+ SortTable
+)
+from re2o.views import form
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')
+ return 'id_Interface-type_hidden' if is_type_tt else 'id_Interface-type'
def generate_ipv4_choices( form ) :
- """ Generate the parameter choices for the bootstrap_form_typeahead tag
+ """ Generate the parameter choices for the massive_bootstrap_form 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') :
+ 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)
@@ -112,7 +157,7 @@ def generate_ipv4_choices( form ) :
return choices
def generate_ipv4_engine( is_type_tt ) :
- """ Generate the parameter engine for the bootstrap_form_typeahead tag
+ """ Generate the parameter engine for the massive_bootstrap_form tag
"""
return (
'new Bloodhound( {{'
@@ -126,7 +171,7 @@ def generate_ipv4_engine( is_type_tt ) :
)
def generate_ipv4_match_func( is_type_tt ) :
- """ Generate the parameter match_func for the bootstrap_form_typeahead tag
+ """ Generate the parameter match_func for the massive_bootstrap_form tag
"""
return (
'function(q, sync) {{'
@@ -142,25 +187,27 @@ def generate_ipv4_match_func( is_type_tt ) :
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
+def generate_ipv4_mbf_param( form, is_type_tt ):
+ """ Generate all the parameters to use with the massive_bootstrap_form
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 = {
+ i_gen_select = { 'ipv4': False }
+ i_mbf_param = {
'choices': i_choices,
'engine': i_engine,
'match_func': i_match_func,
- 'update_on': i_update_on
+ 'update_on': i_update_on,
+ 'gen_select': i_gen_select
}
- return i_bft_param
+ return i_mbf_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.
+ """ 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)
@@ -171,15 +218,16 @@ def new_machine(request, userid):
max_lambdauser_interfaces = options.max_lambdauser_interfaces
if not request.user.has_perms(('cableur',)):
if user != request.user:
- messages.error(request, "Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit")
+ messages.error(
+ request,
+ "Vous ne pouvez pas ajouter une machine à un autre user que vous sans droit")
return redirect("/users/profil/" + str(request.user.id))
if user.user_interfaces().count() >= max_lambdauser_interfaces:
messages.error(request, "Vous avez atteint le maximum d'interfaces autorisées que vous pouvez créer vous même (%s) " % max_lambdauser_interfaces)
return redirect("/users/profil/" + str(request.user.id))
machine = NewMachineForm(request.POST or None)
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
- nb_machine = Interface.objects.filter(machine__user=userid).count()
- domain = DomainForm(request.POST or None, user=user, nb_machine=nb_machine)
+ domain = DomainForm(request.POST or None, user=user)
if machine.is_valid() and interface.is_valid():
new_machine = machine.save(commit=False)
new_machine.user = user
@@ -203,8 +251,8 @@ 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))
- 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)
+ i_mbf_param = generate_ipv4_mbf_param( interface, False )
+ return form({'machineform': machine, 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request)
@login_required
def edit_interface(request, interfaceid):
@@ -243,8 +291,8 @@ 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))
- 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)
+ i_mbf_param = generate_ipv4_mbf_param( interface_form, False )
+ return form({'machineform': machine_form, 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request)
@login_required
def del_machine(request, machineid):
@@ -302,8 +350,8 @@ 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))
- 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)
+ i_mbf_param = generate_ipv4_mbf_param( interface_form, False )
+ return form({'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param}, 'machines/machine.html', request)
@login_required
def del_interface(request, interfaceid):
@@ -340,7 +388,7 @@ def add_iptype(request):
reversion.set_comment("Création")
messages.success(request, "Ce type d'ip a été ajouté")
return redirect("/machines/index_iptype")
- return form({'machineform': iptype, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'iptypeform': iptype}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -359,7 +407,7 @@ def edit_iptype(request, iptypeid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in iptype.changed_data))
messages.success(request, "Type d'ip modifié")
return redirect("/machines/index_iptype/")
- return form({'machineform': iptype}, 'machines/machine.html', request)
+ return form({'iptypeform': iptype}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -377,7 +425,7 @@ def del_iptype(request):
except ProtectedError:
messages.error(request, "Le type d'ip %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % iptype_del)
return redirect("/machines/index_iptype")
- return form({'machineform': iptype, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'iptypeform': iptype}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -390,7 +438,7 @@ def add_machinetype(request):
reversion.set_comment("Création")
messages.success(request, "Ce type de machine a été ajouté")
return redirect("/machines/index_machinetype")
- return form({'machineform': machinetype, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'machinetypeform': machinetype}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -408,7 +456,7 @@ def edit_machinetype(request, machinetypeid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in machinetype.changed_data))
messages.success(request, "Type de machine modifié")
return redirect("/machines/index_machinetype/")
- return form({'machineform': machinetype}, 'machines/machine.html', request)
+ return form({'machinetypeform': machinetype}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -425,7 +473,7 @@ def del_machinetype(request):
except ProtectedError:
messages.error(request, "Le type de machine %s est affectée à au moins une machine, vous ne pouvez pas le supprimer" % machinetype_del)
return redirect("/machines/index_machinetype")
- return form({'machineform': machinetype, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'machinetypeform': machinetype}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -438,7 +486,7 @@ def add_extension(request):
reversion.set_comment("Création")
messages.success(request, "Cette extension a été ajoutée")
return redirect("/machines/index_extension")
- return form({'machineform': extension, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'extensionform': extension}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -456,7 +504,7 @@ def edit_extension(request, extensionid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in extension.changed_data))
messages.success(request, "Extension modifiée")
return redirect("/machines/index_extension/")
- return form({'machineform': extension}, 'machines/machine.html', request)
+ return form({'extensionform': extension}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -473,7 +521,55 @@ def del_extension(request):
except ProtectedError:
messages.error(request, "L'extension %s est affectée à au moins un type de machine, vous ne pouvez pas la supprimer" % extension_del)
return redirect("/machines/index_extension")
- return form({'machineform': extension, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'extensionform': extension}, 'machines/machine.html', request)
+
+@login_required
+@permission_required('infra')
+def add_soa(request):
+ soa = SOAForm(request.POST or None)
+ if soa.is_valid():
+ with transaction.atomic(), reversion.create_revision():
+ soa.save()
+ reversion.set_user(request.user)
+ reversion.set_comment("Création")
+ messages.success(request, "Cet enregistrement SOA a été ajouté")
+ return redirect("/machines/index_extension")
+ return form({'soaform': soa}, 'machines/machine.html', request)
+
+@login_required
+@permission_required('infra')
+def edit_soa(request, soaid):
+ try:
+ soa_instance = SOA.objects.get(pk=soaid)
+ except SOA.DoesNotExist:
+ messages.error(request, u"Entrée inexistante" )
+ return redirect("/machines/index_extension/")
+ soa = SOAForm(request.POST or None, instance=soa_instance)
+ if soa.is_valid():
+ with transaction.atomic(), reversion.create_revision():
+ soa.save()
+ reversion.set_user(request.user)
+ reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in soa.changed_data))
+ messages.success(request, "SOA modifié")
+ return redirect("/machines/index_extension/")
+ return form({'soaform': soa}, 'machines/machine.html', request)
+
+@login_required
+@permission_required('infra')
+def del_soa(request):
+ soa = DelSOAForm(request.POST or None)
+ if soa.is_valid():
+ soa_dels = soa.cleaned_data['soa']
+ for soa_del in soa_dels:
+ try:
+ with transaction.atomic(), reversion.create_revision():
+ soa_del.delete()
+ reversion.set_user(request.user)
+ messages.success(request, "Le SOA a été supprimée")
+ except ProtectedError:
+ messages.error(request, "Erreur le SOA suivant %s ne peut être supprimé" % soa_del)
+ return redirect("/machines/index_extension")
+ return form({'soaform': soa}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -486,7 +582,7 @@ def add_mx(request):
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement mx a été ajouté")
return redirect("/machines/index_extension")
- return form({'machineform': mx, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'mxform': mx}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -504,7 +600,7 @@ def edit_mx(request, mxid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in mx.changed_data))
messages.success(request, "Mx modifié")
return redirect("/machines/index_extension/")
- return form({'machineform': mx}, 'machines/machine.html', request)
+ return form({'mxform': mx}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -521,7 +617,7 @@ def del_mx(request):
except ProtectedError:
messages.error(request, "Erreur le Mx suivant %s ne peut être supprimé" % mx_del)
return redirect("/machines/index_extension")
- return form({'machineform': mx, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'mxform': mx}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -534,7 +630,7 @@ def add_ns(request):
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement ns a été ajouté")
return redirect("/machines/index_extension")
- return form({'machineform': ns, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'nsform': ns}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -552,7 +648,7 @@ def edit_ns(request, nsid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in ns.changed_data))
messages.success(request, "Ns modifié")
return redirect("/machines/index_extension/")
- return form({'machineform': ns}, 'machines/machine.html', request)
+ return form({'nsform': ns}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -569,55 +665,55 @@ def del_ns(request):
except ProtectedError:
messages.error(request, "Erreur le Ns suivant %s ne peut être supprimé" % ns_del)
return redirect("/machines/index_extension")
- return form({'machineform': ns, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'nsform': ns}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
-def add_text(request):
- text = TextForm(request.POST or None)
- if text.is_valid():
+def add_txt(request):
+ txt = TxtForm(request.POST or None)
+ if txt.is_valid():
with transaction.atomic(), reversion.create_revision():
- text.save()
+ txt.save()
reversion.set_user(request.user)
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement text a été ajouté")
return redirect("/machines/index_extension")
- return form({'machineform': text, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'txtform': txt}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
-def edit_text(request, textid):
+def edit_txt(request, txtid):
try:
- text_instance = Text.objects.get(pk=textid)
+ txt_instance = Text.objects.get(pk=txtid)
except Text.DoesNotExist:
messages.error(request, u"Entrée inexistante" )
return redirect("/machines/index_extension/")
- text = TextForm(request.POST or None, instance=text_instance)
- if text.is_valid():
+ txt = TxtForm(request.POST or None, instance=txt_instance)
+ if txt.is_valid():
with transaction.atomic(), reversion.create_revision():
- text.save()
+ txt.save()
reversion.set_user(request.user)
- reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in text.changed_data))
- messages.success(request, "Text modifié")
+ reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in txt.changed_data))
+ messages.success(request, "Txt modifié")
return redirect("/machines/index_extension/")
- return form({'machineform': text}, 'machines/machine.html', request)
+ return form({'txtform': txt}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
-def del_text(request):
- text = DelTextForm(request.POST or None)
- if text.is_valid():
- text_dels = text.cleaned_data['text']
- for text_del in text_dels:
+def del_txt(request):
+ txt = DelTxtForm(request.POST or None)
+ if txt.is_valid():
+ txt_dels = txt.cleaned_data['txt']
+ for txt_del in txt_dels:
try:
with transaction.atomic(), reversion.create_revision():
- text_del.delete()
+ txt_del.delete()
reversion.set_user(request.user)
- messages.success(request, "Le text a été supprimé")
+ messages.success(request, "Le txt a été supprimé")
except ProtectedError:
- messages.error(request, "Erreur le Text suivant %s ne peut être supprimé" % text_del)
+ messages.error(request, "Erreur le Txt suivant %s ne peut être supprimé" % txt_del)
return redirect("/machines/index_extension")
- return form({'machineform': text, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'txtform': txt}, 'machines/machine.html', request)
@login_required
def add_alias(request, interfaceid):
@@ -645,7 +741,7 @@ def add_alias(request, interfaceid):
reversion.set_comment("Création")
messages.success(request, "Cet alias a été ajouté")
return redirect("/machines/index_alias/" + str(interfaceid))
- return form({'machineform': alias, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'aliasform': alias}, 'machines/machine.html', request)
@login_required
def edit_alias(request, aliasid):
@@ -665,7 +761,7 @@ def edit_alias(request, aliasid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in alias.changed_data))
messages.success(request, "Alias modifié")
return redirect("/machines/index_alias/" + str(alias_instance.cname.interface_parent.id))
- return form({'machineform': alias}, 'machines/machine.html', request)
+ return form({'aliasform': alias}, 'machines/machine.html', request)
@login_required
def del_alias(request, interfaceid):
@@ -689,7 +785,7 @@ def del_alias(request, interfaceid):
except ProtectedError:
messages.error(request, "Erreur l'alias suivant %s ne peut être supprimé" % alias_del)
return redirect("/machines/index_alias/" + str(interfaceid))
- return form({'machineform': alias, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'aliasform': alias}, 'machines/machine.html', request)
@login_required
@@ -703,7 +799,7 @@ def add_service(request):
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement service a été ajouté")
return redirect("/machines/index_service")
- return form({'machineform': service}, 'machines/machine.html', request)
+ return form({'serviceform': service}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -721,7 +817,7 @@ def edit_service(request, serviceid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in service.changed_data))
messages.success(request, "Service modifié")
return redirect("/machines/index_service/")
- return form({'machineform': service}, 'machines/machine.html', request)
+ return form({'serviceform': service}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -738,7 +834,7 @@ def del_service(request):
except ProtectedError:
messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % service_del)
return redirect("/machines/index_service")
- return form({'machineform': service}, 'machines/machine.html', request)
+ return form({'serviceform': service}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -751,7 +847,7 @@ def add_vlan(request):
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement vlan a été ajouté")
return redirect("/machines/index_vlan")
- return form({'machineform': vlan, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'vlanform': vlan}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -769,7 +865,7 @@ def edit_vlan(request, vlanid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in vlan.changed_data))
messages.success(request, "Vlan modifié")
return redirect("/machines/index_vlan/")
- return form({'machineform': vlan}, 'machines/machine.html', request)
+ return form({'vlanform': vlan}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -786,7 +882,7 @@ def del_vlan(request):
except ProtectedError:
messages.error(request, "Erreur le Vlan suivant %s ne peut être supprimé" % vlan_del)
return redirect("/machines/index_vlan")
- return form({'machineform': vlan, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'vlanform': vlan}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -799,7 +895,7 @@ def add_nas(request):
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement nas a été ajouté")
return redirect("/machines/index_nas")
- return form({'machineform': nas, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'nasform': nas}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -817,7 +913,7 @@ def edit_nas(request, nasid):
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in nas.changed_data))
messages.success(request, "Nas modifié")
return redirect("/machines/index_nas/")
- return form({'machineform': nas}, 'machines/machine.html', request)
+ return form({'nasform': nas}, 'machines/machine.html', request)
@login_required
@permission_required('infra')
@@ -834,14 +930,20 @@ def del_nas(request):
except ProtectedError:
messages.error(request, "Erreur le Nas suivant %s ne peut être supprimé" % nas_del)
return redirect("/machines/index_nas")
- return form({'machineform': nas, 'interfaceform': None}, 'machines/machine.html', request)
+ return form({'nasform': nas}, 'machines/machine.html', request)
@login_required
@permission_required('cableur')
def index(request):
options, created = GeneralOption.objects.get_or_create()
pagination_large_number = options.pagination_large_number
- machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension').order_by('pk')
+ machines_list = Machine.objects.select_related('user').prefetch_related('interface_set__domain__extension').prefetch_related('interface_set__ipv4__ip_type').prefetch_related('interface_set__type__ip_type__extension').prefetch_related('interface_set__domain__related_domain__extension')
+ machines_list = SortTable.sort(
+ machines_list,
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.MACHINES_INDEX
+ )
paginator = Paginator(machines_list, pagination_large_number)
page = request.GET.get('page')
try:
@@ -857,13 +959,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
@@ -875,17 +977,18 @@ 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
@permission_required('cableur')
def index_extension(request):
- extension_list = Extension.objects.select_related('origin').order_by('name')
+ extension_list = Extension.objects.select_related('origin').select_related('soa').order_by('name')
+ soa_list = SOA.objects.order_by('name')
mx_list = Mx.objects.order_by('zone').select_related('zone').select_related('name__extension')
ns_list = Ns.objects.order_by('zone').select_related('zone').select_related('ns__extension')
text_list = Text.objects.all().select_related('zone')
- return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'mx_list': mx_list, 'ns_list': ns_list, 'text_list' : text_list})
+ return render(request, 'machines/index_extension.html', {'extension_list':extension_list, 'soa_list': soa_list, 'mx_list': mx_list, 'ns_list': ns_list, 'text_list' : text_list})
@login_required
def index_alias(request, interfaceid):
@@ -903,8 +1006,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
@@ -954,17 +1057,23 @@ def history(request, object, id):
except Extension.DoesNotExist:
messages.error(request, "Extension inexistante")
return redirect("/machines/")
+ elif object == 'soa' and request.user.has_perms(('cableur',)):
+ try:
+ object_instance = SOA.objects.get(pk=id)
+ except SOA.DoesNotExist:
+ messages.error(request, "SOA inexistant")
+ return redirect("/machines/")
elif object == 'mx' and request.user.has_perms(('cableur',)):
try:
object_instance = Mx.objects.get(pk=id)
except Mx.DoesNotExist:
messages.error(request, "Mx inexistant")
return redirect("/machines/")
- elif object == 'text' and request.user.has_perms(('cableur',)):
+ elif object == 'txt' and request.user.has_perms(('cableur',)):
try:
object_instance = Text.objects.get(pk=id)
except Text.DoesNotExist:
- messages.error(request, "Text inexistant")
+ messages.error(request, "Txt inexistant")
return redirect("/machines/")
elif object == 'ns' and request.user.has_perms(('cableur',)):
try:
@@ -1012,7 +1121,9 @@ 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__domain__extension')\
+ .prefetch_related('interface_set__machine__user').order_by('name')
return render(request, "machines/index_portlist.html", {'port_list':port_list})
@login_required
@@ -1203,6 +1314,34 @@ def service_servers(request):
@csrf_exempt
@login_required
@permission_required('serveur')
+def ouverture_ports(request):
+ r = {'ipv4':{}, 'ipv6':{}}
+ for o in OuverturePortList.objects.all().prefetch_related('ouvertureport_set').prefetch_related('interface_set', 'interface_set__ipv4'):
+ pl = {
+ "tcp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN))),
+ "tcp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT))),
+ "udp_in":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN))),
+ "udp_out":set(map(str,o.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT))),
+ }
+ for i in filter_active_interfaces(o.interface_set):
+ if i.may_have_port_open():
+ d = r['ipv4'].get(i.ipv4.ipv4, {})
+ d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"])
+ d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"])
+ d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"])
+ d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"])
+ r['ipv4'][i.ipv4.ipv4] = d
+ if i.ipv6_object:
+ d = r['ipv6'].get(i.ipv6, {})
+ d["tcp_in"] = d.get("tcp_in",set()).union(pl["tcp_in"])
+ d["tcp_out"] = d.get("tcp_out",set()).union(pl["tcp_out"])
+ d["udp_in"] = d.get("udp_in",set()).union(pl["udp_in"])
+ d["udp_out"] = d.get("udp_out",set()).union(pl["udp_out"])
+ r['ipv6'][i.ipv6] = d
+ return JSONResponse(r)
+@csrf_exempt
+@login_required
+@permission_required('serveur')
def regen_achieved(request):
obj = Service_link.objects.filter(service__in=Service.objects.filter(service_type=request.POST['service']), server__in=Interface.objects.filter(domain__in=Domain.objects.filter(name=request.POST['server'])))
if obj:
diff --git a/preferences/admin.py b/preferences/admin.py
index a8ce9335..96b4d9e1 100644
--- a/preferences/admin.py
+++ b/preferences/admin.py
@@ -20,35 +20,53 @@
# 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.
-
+"""
+Classes admin pour les models de preferences
+"""
from __future__ import unicode_literals
from django.contrib import admin
from reversion.admin import VersionAdmin
-from .models import OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, Service, AssoOption, MailMessageOption
+from .models import OptionalUser, OptionalMachine, OptionalTopologie
+from .models import GeneralOption, Service, AssoOption, MailMessageOption
+
class OptionalUserAdmin(VersionAdmin):
+ """Class admin options user"""
pass
+
class OptionalTopologieAdmin(VersionAdmin):
+ """Class admin options topologie"""
pass
+
class OptionalMachineAdmin(VersionAdmin):
+ """Class admin options machines"""
pass
+
class GeneralOptionAdmin(VersionAdmin):
+ """Class admin options générales"""
pass
+
class ServiceAdmin(VersionAdmin):
+ """Class admin gestion des services de la page d'accueil"""
pass
+
class AssoOptionAdmin(VersionAdmin):
+ """Class admin options de l'asso"""
pass
+
class MailMessageOptionAdmin(VersionAdmin):
+ """Class admin options mail"""
pass
+
admin.site.register(OptionalUser, OptionalUserAdmin)
admin.site.register(OptionalMachine, OptionalMachineAdmin)
admin.site.register(OptionalTopologie, OptionalTopologieAdmin)
diff --git a/preferences/forms.py b/preferences/forms.py
index 20c7ca95..51cbb885 100644
--- a/preferences/forms.py
+++ b/preferences/forms.py
@@ -19,66 +19,116 @@
# 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.
+"""
+Formulaire d'edition des réglages : user, machine, topologie, asso...
+"""
from __future__ import unicode_literals
-from django.forms import ModelForm, Form, ValidationError
+from django.forms import ModelForm, Form
from django import forms
-from .models import OptionalUser, OptionalMachine, OptionalTopologie, GeneralOption, AssoOption, MailMessageOption, Service
-from django.db.models import Q
+from .models import OptionalUser, OptionalMachine, OptionalTopologie
+from .models import GeneralOption, AssoOption, MailMessageOption, Service
+
class EditOptionalUserForm(ModelForm):
+ """Formulaire d'édition des options de l'user. (solde, telephone..)"""
class Meta:
model = OptionalUser
fields = '__all__'
def __init__(self, *args, **kwargs):
- super(EditOptionalUserForm, self).__init__(*args, **kwargs)
- self.fields['is_tel_mandatory'].label = 'Exiger un numéro de téléphone'
- self.fields['user_solde'].label = 'Activation du solde pour les utilisateurs'
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(EditOptionalUserForm, self).__init__(
+ *args,
+ prefix=prefix,
+ **kwargs
+ )
+ self.fields['is_tel_mandatory'].label = 'Exiger un numéro de\
+ téléphone'
+ self.fields['user_solde'].label = 'Activation du solde pour\
+ les utilisateurs'
+
class EditOptionalMachineForm(ModelForm):
+ """Options machines (max de machines, etc)"""
class Meta:
model = OptionalMachine
fields = '__all__'
def __init__(self, *args, **kwargs):
- super(EditOptionalMachineForm, self).__init__(*args, **kwargs)
- self.fields['password_machine'].label = "Possibilité d'attribuer un mot de passe par interface"
- self.fields['max_lambdauser_interfaces'].label = "Maximum d'interfaces autorisées pour un user normal"
- self.fields['max_lambdauser_aliases'].label = "Maximum d'alias dns autorisés pour un user normal"
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(EditOptionalMachineForm, self).__init__(
+ *args,
+ prefix=prefix,
+ **kwargs
+ )
+ self.fields['password_machine'].label = "Possibilité d'attribuer\
+ un mot de passe par interface"
+ self.fields['max_lambdauser_interfaces'].label = "Maximum\
+ d'interfaces autorisées pour un user normal"
+ self.fields['max_lambdauser_aliases'].label = "Maximum d'alias\
+ dns autorisés pour un user normal"
+
class EditOptionalTopologieForm(ModelForm):
+ """Options de topologie, formulaire d'edition (vlan par default etc)"""
class Meta:
model = OptionalTopologie
fields = '__all__'
def __init__(self, *args, **kwargs):
- super(EditOptionalTopologieForm, self).__init__(*args, **kwargs)
- self.fields['vlan_decision_ok'].label = "Vlan où placer les machines après acceptation RADIUS"
- self.fields['vlan_decision_nok'].label = "Vlan où placer les machines après rejet RADIUS"
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(EditOptionalTopologieForm, self).__init__(
+ *args,
+ prefix=prefix,
+ **kwargs
+ )
+ self.fields['vlan_decision_ok'].label = "Vlan où placer les\
+ machines après acceptation RADIUS"
+ self.fields['vlan_decision_nok'].label = "Vlan où placer les\
+ machines après rejet RADIUS"
+
class EditGeneralOptionForm(ModelForm):
+ """Options générales (affichages de résultats de recherche, etc)"""
class Meta:
model = GeneralOption
fields = '__all__'
def __init__(self, *args, **kwargs):
- super(EditGeneralOptionForm, self).__init__(*args, **kwargs)
- self.fields['search_display_page'].label = 'Resultats affichés dans une recherche'
- self.fields['pagination_number'].label = 'Items par page, taille normale (ex users)'
- self.fields['pagination_large_number'].label = 'Items par page, taille élevée (machines)'
- self.fields['req_expire_hrs'].label = 'Temps avant expiration du lien de reinitialisation de mot de passe (en heures)'
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(EditGeneralOptionForm, self).__init__(
+ *args,
+ prefix=prefix,
+ **kwargs
+ )
+ self.fields['search_display_page'].label = 'Resultats\
+ affichés dans une recherche'
+ self.fields['pagination_number'].label = 'Items par page,\
+ taille normale (ex users)'
+ self.fields['pagination_large_number'].label = 'Items par page,\
+ taille élevée (machines)'
+ self.fields['req_expire_hrs'].label = 'Temps avant expiration du lien\
+ de reinitialisation de mot de passe (en heures)'
self.fields['site_name'].label = 'Nom du site web'
- self.fields['email_from'].label = 'Adresse mail d\'expedition automatique'
+ self.fields['email_from'].label = "Adresse mail d\
+ 'expedition automatique"
+
class EditAssoOptionForm(ModelForm):
+ """Options de l'asso (addresse, telephone, etc)"""
class Meta:
model = AssoOption
fields = '__all__'
def __init__(self, *args, **kwargs):
- super(EditAssoOptionForm, self).__init__(*args, **kwargs)
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(EditAssoOptionForm, self).__init__(
+ *args,
+ prefix=prefix,
+ **kwargs
+ )
self.fields['name'].label = 'Nom de l\'asso'
self.fields['siret'].label = 'SIRET'
self.fields['adresse1'].label = 'Adresse (ligne 1)'
@@ -86,22 +136,44 @@ class EditAssoOptionForm(ModelForm):
self.fields['contact'].label = 'Email de contact'
self.fields['telephone'].label = 'Numéro de téléphone'
self.fields['pseudo'].label = 'Pseudo d\'usage'
- self.fields['utilisateur_asso'].label = 'Compte utilisé pour faire les modifications depuis /admin'
+ self.fields['utilisateur_asso'].label = 'Compte utilisé pour\
+ faire les modifications depuis /admin'
+
class EditMailMessageOptionForm(ModelForm):
+ """Formulaire d'edition des messages de bienvenue personnalisés"""
class Meta:
model = MailMessageOption
fields = '__all__'
def __init__(self, *args, **kwargs):
- super(EditMailMessageOptionForm, self).__init__(*args, **kwargs)
- self.fields['welcome_mail_fr'].label = 'Message dans le mail de bienvenue en français'
- self.fields['welcome_mail_en'].label = 'Message dans le mail de bienvenue en anglais'
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(EditMailMessageOptionForm, self).__init__(
+ *args,
+ prefix=prefix,
+ **kwargs
+ )
+ self.fields['welcome_mail_fr'].label = 'Message dans le\
+ mail de bienvenue en français'
+ self.fields['welcome_mail_en'].label = 'Message dans le\
+ mail de bienvenue en anglais'
+
class ServiceForm(ModelForm):
+ """Edition, ajout de services sur la page d'accueil"""
class Meta:
model = Service
fields = '__all__'
+ def __init__(self, *args, **kwargs):
+ prefix = kwargs.pop('prefix', self.Meta.model.__name__)
+ super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs)
+
+
class DelServiceForm(Form):
- services = forms.ModelMultipleChoiceField(queryset=Service.objects.all(), label="Enregistrements service actuels", widget=forms.CheckboxSelectMultiple)
+ """Suppression de services sur la page d'accueil"""
+ services = forms.ModelMultipleChoiceField(
+ queryset=Service.objects.all(),
+ label="Enregistrements service actuels",
+ widget=forms.CheckboxSelectMultiple
+ )
diff --git a/preferences/migrations/0021_auto_20171015_1741.py b/preferences/migrations/0021_auto_20171015_1741.py
new file mode 100644
index 00000000..cc94720a
--- /dev/null
+++ b/preferences/migrations/0021_auto_20171015_1741.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-10-15 15:41
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('preferences', '0020_optionalmachine_ipv6'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='optionaltopologie',
+ name='radius_general_policy',
+ field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32),
+ ),
+ ]
diff --git a/preferences/migrations/0022_auto_20171015_1758.py b/preferences/migrations/0022_auto_20171015_1758.py
new file mode 100644
index 00000000..ea389a32
--- /dev/null
+++ b/preferences/migrations/0022_auto_20171015_1758.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-10-15 15:58
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('preferences', '0021_auto_20171015_1741'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='optionaltopologie',
+ name='radius_general_policy',
+ field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32),
+ ),
+ ]
diff --git a/preferences/migrations/0023_auto_20171015_2033.py b/preferences/migrations/0023_auto_20171015_2033.py
new file mode 100644
index 00000000..3235e49f
--- /dev/null
+++ b/preferences/migrations/0023_auto_20171015_2033.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.7 on 2017-10-15 18:33
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('preferences', '0022_auto_20171015_1758'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='optionaltopologie',
+ name='radius_general_policy',
+ field=models.CharField(choices=[('MACHINE', 'Sur le vlan de la plage ip machine'), ('DEFINED', 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"')], default='DEFINED', max_length=32),
+ ),
+ ]
diff --git a/preferences/models.py b/preferences/models.py
index 34c4c0b1..dc1412e7 100644
--- a/preferences/models.py
+++ b/preferences/models.py
@@ -20,26 +20,38 @@
# 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.
-
+"""
+Reglages généraux, machines, utilisateurs, mail, general pour l'application.
+"""
from __future__ import unicode_literals
from django.db import models
from cotisations.models import Paiement
-from machines.models import Vlan
+
class OptionalUser(models.Model):
+ """Options pour l'user : obligation ou nom du telephone,
+ activation ou non du solde, autorisation du negatif, fingerprint etc"""
PRETTY_NAME = "Options utilisateur"
is_tel_mandatory = models.BooleanField(default=True)
user_solde = models.BooleanField(default=False)
- solde_negatif = models.DecimalField(max_digits=5, decimal_places=2, default=0)
+ solde_negatif = models.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ default=0
+ )
gpg_fingerprint = models.BooleanField(default=True)
def clean(self):
+ """Creation du mode de paiement par solde"""
if self.user_solde:
Paiement.objects.get_or_create(moyen="Solde")
+
class OptionalMachine(models.Model):
+ """Options pour les machines : maximum de machines ou d'alias par user
+ sans droit, activation de l'ipv6"""
PRETTY_NAME = "Options machines"
password_machine = models.BooleanField(default=False)
@@ -47,21 +59,43 @@ class OptionalMachine(models.Model):
max_lambdauser_aliases = models.IntegerField(default=10)
ipv6 = models.BooleanField(default=False)
+
class OptionalTopologie(models.Model):
+ """Reglages pour la topologie : mode d'accès radius, vlan où placer
+ les machines en accept ou reject"""
PRETTY_NAME = "Options topologie"
MACHINE = 'MACHINE'
DEFINED = 'DEFINED'
CHOICE_RADIUS = (
- (MACHINE, 'Sur le vlan de la plage ip machine'),
- (DEFINED, 'Prédéfini dans "Vlan où placer les machines après acceptation RADIUS"'),
+ (MACHINE, 'Sur le vlan de la plage ip machine'),
+ (DEFINED, 'Prédéfini dans "Vlan où placer les machines\
+ après acceptation RADIUS"'),
+ )
+
+ radius_general_policy = models.CharField(
+ max_length=32,
+ choices=CHOICE_RADIUS,
+ default='DEFINED'
+ )
+ vlan_decision_ok = models.OneToOneField(
+ 'machines.Vlan',
+ on_delete=models.PROTECT,
+ related_name='decision_ok',
+ blank=True,
+ null=True
+ )
+ vlan_decision_nok = models.OneToOneField(
+ 'machines.Vlan',
+ on_delete=models.PROTECT,
+ related_name='decision_nok',
+ blank=True,
+ null=True
)
- radius_general_policy = models.CharField(max_length=32, choices=CHOICE_RADIUS, default='DEFINED')
- vlan_decision_ok = models.OneToOneField('machines.Vlan', on_delete=models.PROTECT, related_name='decision_ok', blank=True, null=True)
- vlan_decision_nok = models.OneToOneField('machines.Vlan', on_delete=models.PROTECT, related_name='decision_nok', blank=True, null=True)
-
class GeneralOption(models.Model):
+ """Options générales : nombre de resultats par page, nom du site,
+ temps où les liens sont valides"""
PRETTY_NAME = "Options générales"
search_display_page = models.IntegerField(default=15)
@@ -71,30 +105,44 @@ class GeneralOption(models.Model):
site_name = models.CharField(max_length=32, default="Re2o")
email_from = models.EmailField(default="www-data@serveur.net")
+
class Service(models.Model):
+ """Liste des services affichés sur la page d'accueil : url, description,
+ image et nom"""
name = models.CharField(max_length=32)
url = models.URLField()
description = models.TextField()
- image = models.ImageField(upload_to='logo', blank=True)
+ image = models.ImageField(upload_to='logo', blank=True)
def __str__(self):
return str(self.name)
+
class AssoOption(models.Model):
+ """Options générales de l'asso : siret, addresse, nom, etc"""
PRETTY_NAME = "Options de l'association"
- name = models.CharField(default="Association réseau école machin", max_length=256)
+ name = models.CharField(
+ default="Association réseau école machin",
+ max_length=256
+ )
siret = models.CharField(default="00000000000000", max_length=32)
adresse1 = models.CharField(default="1 Rue de exemple", max_length=128)
adresse2 = models.CharField(default="94230 Cachan", max_length=128)
contact = models.EmailField(default="contact@example.org")
telephone = models.CharField(max_length=15, default="0000000000")
pseudo = models.CharField(default="Asso", max_length=32)
- utilisateur_asso = models.OneToOneField('users.User', on_delete=models.PROTECT, blank=True, null=True)
+ utilisateur_asso = models.OneToOneField(
+ 'users.User',
+ on_delete=models.PROTECT,
+ blank=True,
+ null=True
+ )
+
class MailMessageOption(models.Model):
+ """Reglages, mail de bienvenue et autre"""
PRETTY_NAME = "Options de corps de mail"
welcome_mail_fr = models.TextField(default="")
welcome_mail_en = models.TextField(default="")
-
diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html
index 25fa4c02..02f006c1 100644
--- a/preferences/templates/preferences/edit_preferences.html
+++ b/preferences/templates/preferences/edit_preferences.html
@@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endcomment %}
{% load bootstrap3 %}
+{% load massive_bootstrap_form %}
{% block title %}Création et modification des préférences{% endblock %}
@@ -34,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
diff --git a/preferences/urls.py b/preferences/urls.py
index 624d2e75..f10d25a0 100644
--- a/preferences/urls.py
+++ b/preferences/urls.py
@@ -19,6 +19,9 @@
# 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.
+"""
+Urls de l'application preferences, pointant vers les fonctions de views
+"""
from __future__ import unicode_literals
@@ -28,15 +31,47 @@ from . import views
urlpatterns = [
- url(r'^edit_options/(?POptionalUser)$', views.edit_options, name='edit-options'),
- url(r'^edit_options/(?POptionalMachine)$', views.edit_options, name='edit-options'),
- url(r'^edit_options/(?POptionalTopologie)$', views.edit_options, name='edit-options'),
- url(r'^edit_options/(?PGeneralOption)$', views.edit_options, name='edit-options'),
- url(r'^edit_options/(?PAssoOption)$', views.edit_options, name='edit-options'),
- url(r'^edit_options/(?PMailMessageOption)$', views.edit_options, name='edit-options'),
+ url(
+ r'^edit_options/(?POptionalUser)$',
+ views.edit_options,
+ name='edit-options'
+ ),
+ url(
+ r'^edit_options/(?POptionalMachine)$',
+ views.edit_options,
+ name='edit-options'
+ ),
+ url(
+ r'^edit_options/(?POptionalTopologie)$',
+ views.edit_options,
+ name='edit-options'
+ ),
+ url(
+ r'^edit_options/(?PGeneralOption)$',
+ views.edit_options,
+ name='edit-options'
+ ),
+ url(
+ r'^edit_options/(?PAssoOption)$',
+ views.edit_options,
+ name='edit-options'
+ ),
+ url(
+ r'^edit_options/(?PMailMessageOption)$',
+ views.edit_options,
+ name='edit-options'
+ ),
url(r'^add_services/$', views.add_services, name='add-services'),
- url(r'^edit_services/(?P[0-9]+)$', views.edit_services, name='edit-services'),
+ url(
+ r'^edit_services/(?P[0-9]+)$',
+ views.edit_services,
+ name='edit-services'
+ ),
url(r'^del_services/$', views.del_services, name='del-services'),
- url(r'^history/(?Pservice)/(?P[0-9]+)$', views.history, name='history'),
+ url(
+ r'^history/(?Pservice)/(?P[0-9]+)$',
+ views.history,
+ name='history'
+ ),
url(r'^$', views.display_options, name='display-options'),
]
diff --git a/preferences/views.py b/preferences/views.py
index 5fe1cff5..1e2c433e 100644
--- a/preferences/views.py
+++ b/preferences/views.py
@@ -23,48 +23,53 @@
# App de gestion des machines pour re2o
# Gabriel Détraz, Augustin Lemesle
# Gplv2
+"""
+Vue d'affichage, et de modification des réglages (réglages machine,
+topologie, users, service...)
+"""
from __future__ import unicode_literals
-from django.shortcuts import render
-from django.shortcuts import get_object_or_404, render, redirect
-from django.template.context_processors import csrf
+from django.shortcuts import render, redirect
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 Max, ProtectedError
-from django.db import IntegrityError
-from django.core.mail import send_mail
-from django.utils import timezone
-from django.core.urlresolvers import reverse
+from django.db.models import ProtectedError
from django.db import transaction
from reversion.models import Version
from reversion import revisions as reversion
+from re2o.views import form
from .forms import ServiceForm, DelServiceForm
-from .models import Service, OptionalUser, OptionalMachine, AssoOption, MailMessageOption, GeneralOption, OptionalTopologie
+from .models import Service, OptionalUser, OptionalMachine, AssoOption
+from .models import MailMessageOption, GeneralOption, OptionalTopologie
from . import models
from . import forms
-def form(ctx, template, request):
- c = ctx
- c.update(csrf(request))
- return render(request, template, c)
-
@login_required
@permission_required('cableur')
def display_options(request):
- useroptions, created = OptionalUser.objects.get_or_create()
- machineoptions, created = OptionalMachine.objects.get_or_create()
- topologieoptions, created = OptionalTopologie.objects.get_or_create()
- generaloptions, created = GeneralOption.objects.get_or_create()
- assooptions, created = AssoOption.objects.get_or_create()
- mailmessageoptions, created = MailMessageOption.objects.get_or_create()
+ """Vue pour affichage des options (en vrac) classé selon les models
+ correspondants dans un tableau"""
+ useroptions, _created = OptionalUser.objects.get_or_create()
+ machineoptions, _created = OptionalMachine.objects.get_or_create()
+ topologieoptions, _created = OptionalTopologie.objects.get_or_create()
+ generaloptions, _created = GeneralOption.objects.get_or_create()
+ assooptions, _created = AssoOption.objects.get_or_create()
+ mailmessageoptions, _created = MailMessageOption.objects.get_or_create()
service_list = Service.objects.all()
- return form({'useroptions': useroptions, 'machineoptions': machineoptions, 'topologieoptions': topologieoptions, 'generaloptions': generaloptions, 'assooptions' : assooptions, 'mailmessageoptions' : mailmessageoptions, 'service_list':service_list}, 'preferences/display_preferences.html', request)
+ return form({
+ 'useroptions': useroptions,
+ 'machineoptions': machineoptions,
+ 'topologieoptions': topologieoptions,
+ 'generaloptions': generaloptions,
+ 'assooptions': assooptions,
+ 'mailmessageoptions': mailmessageoptions,
+ 'service_list': service_list
+ }, 'preferences/display_preferences.html', request)
+
@login_required
@permission_required('admin')
@@ -73,23 +78,36 @@ def edit_options(request, section):
model = getattr(models, section, None)
form_instance = getattr(forms, 'Edit' + section + 'Form', None)
if model and form:
- options_instance, created = model.objects.get_or_create()
- options = form_instance(request.POST or None, instance=options_instance)
+ options_instance, _created = model.objects.get_or_create()
+ options = form_instance(
+ request.POST or None,
+ instance=options_instance
+ )
if options.is_valid():
with transaction.atomic(), reversion.create_revision():
options.save()
reversion.set_user(request.user)
- reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in options.changed_data))
+ reversion.set_comment(
+ "Champs modifié(s) : %s" % ', '.join(
+ field for field in options.changed_data
+ )
+ )
messages.success(request, "Préférences modifiées")
return redirect("/preferences/")
- return form({'options': options}, 'preferences/edit_preferences.html', request)
+ return form(
+ {'options': options},
+ 'preferences/edit_preferences.html',
+ request
+ )
else:
messages.error(request, "Objet inconnu")
return redirect("/preferences/")
+
@login_required
@permission_required('admin')
def add_services(request):
+ """Ajout d'un service de la page d'accueil"""
services = ServiceForm(request.POST or None)
if services.is_valid():
with transaction.atomic(), reversion.create_revision():
@@ -98,29 +116,45 @@ def add_services(request):
reversion.set_comment("Création")
messages.success(request, "Cet enregistrement ns a été ajouté")
return redirect("/preferences/")
- return form({'preferenceform': services}, 'preferences/preferences.html', request)
+ return form(
+ {'preferenceform': services},
+ 'preferences/preferences.html',
+ request
+ )
+
@login_required
@permission_required('admin')
def edit_services(request, servicesid):
+ """Edition des services affichés sur la page d'accueil"""
try:
services_instance = Service.objects.get(pk=servicesid)
except Service.DoesNotExist:
- messages.error(request, u"Entrée inexistante" )
+ messages.error(request, u"Entrée inexistante")
return redirect("/preferences/")
services = ServiceForm(request.POST or None, instance=services_instance)
if services.is_valid():
with transaction.atomic(), reversion.create_revision():
services.save()
reversion.set_user(request.user)
- reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in services.changed_data))
+ reversion.set_comment(
+ "Champs modifié(s) : %s" % ', '.join(
+ field for field in services.changed_data
+ )
+ )
messages.success(request, "Service modifié")
return redirect("/preferences/")
- return form({'preferenceform': services}, 'preferences/preferences.html', request)
+ return form(
+ {'preferenceform': services},
+ 'preferences/preferences.html',
+ request
+ )
+
@login_required
@permission_required('admin')
def del_services(request):
+ """Suppression d'un service de la page d'accueil"""
services = DelServiceForm(request.POST or None)
if services.is_valid():
services_dels = services.cleaned_data['services']
@@ -131,20 +165,28 @@ def del_services(request):
reversion.set_user(request.user)
messages.success(request, "Le services a été supprimée")
except ProtectedError:
- messages.error(request, "Erreur le service suivant %s ne peut être supprimé" % services_del)
+ messages.error(request, "Erreur le service\
+ suivant %s ne peut être supprimé" % services_del)
return redirect("/preferences/")
- return form({'preferenceform': services}, 'preferences/preferences.html', request)
+ return form(
+ {'preferenceform': services},
+ 'preferences/preferences.html',
+ request
+ )
+
@login_required
@permission_required('cableur')
-def history(request, object, id):
- if object == 'service':
+def history(request, object_name, object_id):
+ """Historique de creation et de modification d'un service affiché sur
+ la page d'accueil"""
+ if object_name == 'service':
try:
- object_instance = Service.objects.get(pk=id)
+ object_instance = Service.objects.get(pk=object_id)
except Service.DoesNotExist:
- messages.error(request, "Service inexistant")
- return redirect("/preferences/")
- options, created = GeneralOption.objects.get_or_create()
+ messages.error(request, "Service inexistant")
+ return redirect("/preferences/")
+ options, _created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number
reversions = Version.objects.get_for_object(object_instance)
paginator = Paginator(reversions, pagination_number)
@@ -157,4 +199,7 @@ def history(request, object, id):
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
reversions = paginator.page(paginator.num_pages)
- return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance})
+ return render(request, 're2o/history.html', {
+ 'reversions': reversions,
+ 'object': object_instance
+ })
diff --git a/re2o/context_processors.py b/re2o/context_processors.py
index ed4769b5..e562a347 100644
--- a/re2o/context_processors.py
+++ b/re2o/context_processors.py
@@ -19,15 +19,19 @@
# 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.
+"""Fonction de context, variables renvoyées à toutes les vues"""
+
from __future__ import unicode_literals
-from machines.models import Interface, Machine
from preferences.models import GeneralOption, OptionalMachine
+
def context_user(request):
- general_options, created = GeneralOption.objects.get_or_create()
- machine_options, created = OptionalMachine.objects.get_or_create()
+ """Fonction de context lorsqu'un user est logué (ou non),
+ renvoie les infos sur l'user, la liste de ses droits, ses machines"""
+ general_options, _created = GeneralOption.objects.get_or_create()
+ machine_options, _created = OptionalMachine.objects.get_or_create()
user = request.user
if user.is_authenticated():
interfaces = user.user_interfaces()
@@ -52,8 +56,8 @@ def context_user(request):
'is_bofh': is_bofh,
'is_trez': is_trez,
'is_infra': is_infra,
- 'is_admin' : is_admin,
+ 'is_admin': is_admin,
'interfaces': interfaces,
'site_name': general_options.site_name,
- 'ipv6_enabled' : machine_options.ipv6,
+ 'ipv6_enabled': machine_options.ipv6,
}
diff --git a/machines/templatetags/__init__.py b/re2o/templatetags/__init__.py
similarity index 100%
rename from machines/templatetags/__init__.py
rename to re2o/templatetags/__init__.py
diff --git a/re2o/templatetags/massive_bootstrap_form.py b/re2o/templatetags/massive_bootstrap_form.py
new file mode 100644
index 00000000..26a9bcc8
--- /dev/null
+++ b/re2o/templatetags/massive_bootstrap_form.py
@@ -0,0 +1,809 @@
+# -*- 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.
+
+""" Templatetag used to render massive django form selects into bootstrap
+forms that can still be manipulating even if there is multiple tens of
+thousands of elements in the select. It's made possible using JS libaries
+Twitter Typeahead and Splitree's Tokenfield.
+See docstring of massive_bootstrap_form for a detailed explaantion on how
+to use this templatetag.
+"""
+
+from django import template
+from django.utils.safestring import mark_safe
+from django.forms import TextInput
+from django.forms.widgets import Select
+from bootstrap3.utils import render_tag
+from bootstrap3.forms import render_field
+
+register = template.Library()
+
+@register.simple_tag
+def massive_bootstrap_form(form, mbf_fields, *args, **kwargs):
+ """
+ Render a form where some specific fields are rendered using Twitter
+ Typeahead and/or splitree's Bootstrap Tokenfield to improve the performance, the
+ speed and UX when dealing with very large datasets (select with 50k+ elts
+ for instance).
+ When the fields specified should normally be rendered as a select with
+ single selectable option, Twitter Typeahead is used for a better display
+ and the matching query engine. When dealing with multiple selectable
+ options, sliptree's Bootstrap Tokenfield in addition with Typeahead.
+ For convenience, it accepts the same parameters as a standard bootstrap
+ can accept.
+
+ **Tag name**::
+
+ massive_bootstrap_form
+
+ **Parameters**:
+
+ form (required)
+ The form that is to be rendered
+
+ mbf_fields (optional)
+ A list of field names (comma separated) that should be rendered
+ with Typeahead/Tokenfield instead of the default bootstrap
+ renderer.
+ If not specified, all fields will be rendered as a normal bootstrap
+ field.
+
+ mbf_param (optional)
+ A dict of parameters for the massive_bootstrap_form tag. The
+ possible parameters are the following.
+
+ choices (optional)
+ 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 (optional)
+ 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 (optional)
+ 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_' and 'engine_' 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 (optional)
+ A dict of list of ids that the values depends on. The engine
+ and the typeahead properties are recalculated and reapplied.
+ Example :
+ 'update_on' : {
+ 'field_A' : [ 'id0', 'id1', ... ] ,
+ 'field_B' : ... ,
+ ...
+ }
+
+ gen_select (optional)
+ A dict of boolean telling if the form should either generate
+ the normal select (set to true) and then use it to generate
+ the possible choices and then remove it or either (set to
+ false) generate the choices variable in this tag and do not
+ send any select.
+ Sending the select before can be usefull to permit the use
+ without any JS enabled but it will execute more code locally
+ for the client so the loading might be slower.
+ If not specified, this variable is set to true for each field
+ Example :
+ 'gen_select' : {
+ 'field_A': True ,
+ 'field_B': ... ,
+ ...
+ }
+
+ See boostrap_form_ for other arguments
+
+ **Usage**::
+
+ {% massive_bootstrap_form
+ form
+ [ '[,[,...]]' ]
+ [ mbf_param = {
+ [ 'choices': {
+ [ '': ''
+ [, '': ''
+ [, ... ] ] ]
+ } ]
+ [, 'engine': {
+ [ '': ''
+ [, '': ''
+ [, ... ] ] ]
+ } ]
+ [, 'match_func': {
+ [ '': ''
+ [, '': ''
+ [, ... ] ] ]
+ } ]
+ [, 'update_on': {
+ [ '': ''
+ [, '': ''
+ [, ... ] ] ]
+ } ],
+ [, 'gen_select': {
+ [ '': ''
+ [, '': ''
+ [, ... ] ] ]
+ } ]
+ } ]
+ [ ]
+ %}
+
+ **Example**:
+
+ {% massive_bootstrap_form form 'ipv4' choices='[...]' %}
+ """
+
+ mbf_form = MBFForm(form, mbf_fields.split(','), *args, **kwargs)
+ return mbf_form.render()
+
+
+
+
+class MBFForm():
+ """ An object to hold all the information and useful methods needed to
+ create and render a massive django form into an actual HTML and JS
+ code able to handle it correctly.
+ Every field that is not listed is rendered as a normal bootstrap_field.
+ """
+
+
+ def __init__(self, form, mbf_fields, *args, **kwargs):
+ # The django form object
+ self.form = form
+ # The fields on which to use JS
+ self.fields = mbf_fields
+
+ # Other bootstrap_form arguments to render the fields
+ self.args = args
+ self.kwargs = kwargs
+
+ # Fields to exclude form the form rendering
+ self.exclude = self.kwargs.get('exclude', '').split(',')
+
+ # All the mbf parameters specified byt the user
+ param = kwargs.pop('mbf_param', {})
+ self.choices = param.get('choices', {})
+ self.engine = param.get('engine', {})
+ self.match_func = param.get('match_func', {})
+ self.update_on = param.get('update_on', {})
+ self.gen_select = param.get('gen_select', {})
+ self.hidden_fields = [h.name for h in self.form.hidden_fields()]
+
+ # HTML code to insert inside a template
+ self.html = ""
+
+
+ def render(self):
+ """ HTML code for the fully rendered form with all the necessary form
+ """
+ for name, field in self.form.fields.items():
+ if not name in self.exclude:
+
+ if name in self.fields and not name in self.hidden_fields:
+ mbf_field = MBFField(
+ name,
+ field,
+ field.get_bound_field(self.form, name),
+ self.choices.get(name, None),
+ self.engine.get(name, None),
+ self.match_func.get(name, None),
+ self.update_on.get(name, None),
+ self.gen_select.get(name, True),
+ *self.args,
+ **self.kwargs
+ )
+ self.html += mbf_field.render()
+
+ else:
+ self.html += render_field(
+ field.get_bound_field(self.form, name),
+ *self.args,
+ **self.kwargs
+ )
+
+ return mark_safe(self.html)
+
+
+
+
+
+class MBFField():
+ """ An object to hold all the information and useful methods needed to
+ create and render a massive django form field into an actual HTML and JS
+ code able to handle it correctly.
+ Twitter Typeahead is used for the display and the matching of queries and
+ in case of a MultipleSelect, Sliptree's Tokenfield is also used to manage
+ multiple values.
+ A div with only non visible elements is created after the div containing
+ the displayed input. It's used to store the actual data that will be sent
+ to the server """
+
+
+ def __init__(self, name_, field_, bound_, choices_, engine_, match_func_,
+ update_on_, gen_select_, *args_, **kwargs_):
+
+ # Verify this field is a Select (or MultipleSelect) (only supported)
+ if not isinstance(field_.widget, Select):
+ raise ValueError(
+ ('Field named {f_name} is not a Select and'
+ 'can\'t be rendered with massive_bootstrap_form.'
+ ).format(
+ f_name=name_
+ )
+ )
+
+ # Name of the field
+ self.name = name_
+ # Django field object
+ self.field = field_
+ # Bound Django field associated with field
+ self.bound = bound_
+
+ # Id for the main visible input
+ self.input_id = self.bound.auto_id
+ # Id for a hidden input used to store the value
+ self.hidden_id = self.input_id + '_hidden'
+ # Id for another div containing hidden inputs and script
+ self.div2_id = self.input_id + '_div'
+
+ # Should the standard select should be generated
+ self.gen_select = gen_select_
+ # Is it select with multiple values possible (use of tokenfield)
+ self.multiple = self.field.widget.allow_multiple_selected
+ # JS for the choices variable (user specified or default)
+ self.choices = choices_ or self.default_choices()
+ # JS for the engine variable (typeahead) (user specified or default)
+ self.engine = engine_ or self.default_engine()
+ # JS for the matching function (typeahead) (user specified or default)
+ self.match_func = match_func_ or self.default_match_func()
+ # JS for the datasets variable (typeahead) (user specified or default)
+ self.datasets = self.default_datasets()
+ # Ids of other fields to bind a reset/reload with when changed
+ self.update_on = update_on_ or []
+
+ # Whole HTML code to insert in the template
+ self.html = ""
+ # JS code in the script tag
+ self.js_script = ""
+ # Input tag to display instead of select
+ self.replace_input = None
+
+ # Other bootstrap_form arguments to render the fields
+ self.args = args_
+ self.kwargs = kwargs_
+
+
+ def default_choices(self):
+ """ JS code of the variable choices_ """
+
+ if self.gen_select:
+ return (
+ 'function plop(o) {{'
+ 'var c = [];'
+ 'for( let i=0 ; i """
+ return (
+ 'new Bloodhound({{'
+ ' datumTokenizer: Bloodhound.tokenizers.obj.whitespace("value"),'
+ ' queryTokenizer: Bloodhound.tokenizers.whitespace,'
+ ' local: choices_{name},'
+ ' identify: function(obj) {{ return obj.key; }}'
+ '}})'
+ ).format(
+ name=self.name
+ )
+
+
+ def default_datasets(self):
+ """ Default JS script of the datasets to use with typeahead """
+ return (
+ '{{'
+ ' hint: true,'
+ ' highlight: true,'
+ ' minLength: 0'
+ '}},'
+ '{{'
+ ' display: "value",'
+ ' name: "{name}",'
+ ' source: {match_func}'
+ '}}'
+ ).format(
+ name=self.name,
+ match_func=self.match_func
+ )
+
+
+ def default_match_func(self):
+ """ Default JS code of the matching function to use with typeahed """
+ return (
+ 'function ( q, sync ) {{'
+ ' if ( q === "" ) {{'
+ ' var first = choices_{name}.slice( 0, 5 ).map('
+ ' function ( obj ) {{ return obj.key; }}'
+ ' );'
+ ' sync( engine_{name}.get( first ) );'
+ ' }} else {{'
+ ' engine_{name}.search( q, sync );'
+ ' }}'
+ '}}'
+ ).format(
+ name=self.name
+ )
+
+
+ def render(self):
+ """ HTML code for the fully rendered field """
+ self.gen_displayed_div()
+ self.gen_hidden_div()
+ return mark_safe(self.html)
+
+
+ def gen_displayed_div(self):
+ """ Generate HTML code for the div that contains displayed tags """
+ if self.gen_select:
+ self.html += render_field(
+ self.bound,
+ *self.args,
+ **self.kwargs
+ )
+
+ self.field.widget = TextInput(
+ attrs={
+ 'name': 'mbf_'+self.name,
+ 'placeholder': self.field.empty_label
+ }
+ )
+ self.replace_input = render_field(
+ self.bound,
+ *self.args,
+ **self.kwargs
+ )
+
+ if not self.gen_select:
+ self.html += self.replace_input
+
+
+ def gen_hidden_div(self):
+ """ Generate HTML code for the div that contains hidden tags """
+ self.gen_full_js()
+
+ content = self.js_script
+ if not self.multiple and not self.gen_select:
+ content += self.hidden_input()
+
+ self.html += render_tag(
+ 'div',
+ content=content,
+ attrs={'id': self.div2_id}
+ )
+
+
+ def hidden_input(self):
+ """ HTML for the hidden input element """
+ return render_tag(
+ 'input',
+ attrs={
+ 'id': self.hidden_id,
+ 'name': self.bound.html_name,
+ 'type': 'hidden',
+ 'value': self.bound.value() or ""
+ }
+ )
+
+
+ def gen_full_js(self):
+ """ Generate the full script tag containing the JS code """
+ self.create_js()
+ self.fill_js()
+ self.get_script()
+
+
+ def create_js(self):
+ """ Generate a template for the whole script to use depending on
+ gen_select and multiple """
+ if self.gen_select:
+ if self.multiple:
+ self.js_script = (
+ '$( "#{input_id}" ).ready( function() {{'
+ ' var choices_{f_name} = {choices};'
+ ' {del_select}'
+ ' var engine_{f_name};'
+ ' var setup_{f_name} = function() {{'
+ ' engine_{f_name} = {engine};'
+ ' $( "#{input_id}" ).tokenfield( "destroy" );'
+ ' $( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});'
+ ' }};'
+ ' $( "#{input_id}" ).bind( "tokenfield:createtoken", {tok_create} );'
+ ' $( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );'
+ ' $( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );'
+ ' {tok_updates}'
+ ' setup_{f_name}();'
+ ' {tok_init_input}'
+ '}} );'
+ )
+ else:
+ self.js_script = (
+ '$( "#{input_id}" ).ready( function() {{'
+ ' var choices_{f_name} = {choices};'
+ ' {del_select}'
+ ' {gen_hidden}'
+ ' 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", {typ_select} );'
+ ' $( "#{input_id}" ).bind( "typeahead:change", {typ_change} );'
+ ' {typ_updates}'
+ ' setup_{f_name}();'
+ ' {typ_init_input}'
+ '}} );'
+ )
+ else:
+ if self.multiple:
+ self.js_script = (
+ 'var choices_{f_name} = {choices};'
+ 'var engine_{f_name};'
+ 'var setup_{f_name} = function() {{'
+ ' engine_{f_name} = {engine};'
+ ' $( "#{input_id}" ).tokenfield( "destroy" );'
+ ' $( "#{input_id}" ).tokenfield({{typeahead: [ {datasets} ] }});'
+ '}};'
+ '$( "#{input_id}" ).bind( "tokenfield:createtoken", {tok_create} );'
+ '$( "#{input_id}" ).bind( "tokenfield:edittoken", {tok_edit} );'
+ '$( "#{input_id}" ).bind( "tokenfield:removetoken", {tok_remove} );'
+ '{tok_updates}'
+ '$( "#{input_id}" ).ready( function() {{'
+ ' setup_{f_name}();'
+ ' {tok_init_input}'
+ '}} );'
+ )
+ else:
+ self.js_script = (
+ '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", {typ_select} );'
+ '$( "#{input_id}" ).bind( "typeahead:change", {typ_change} );'
+ '{typ_updates}'
+ '$( "#{input_id}" ).ready( function() {{'
+ ' setup_{f_name}();'
+ ' {typ_init_input}'
+ '}} );'
+ )
+
+
+ def fill_js(self):
+ """ Fill the template with the correct values """
+ self.js_script = self.js_script.format(
+ f_name=self.name,
+ choices=self.choices,
+ del_select=self.del_select(),
+ gen_hidden=self.gen_hidden(),
+ engine=self.engine,
+ input_id=self.input_id,
+ datasets=self.datasets,
+ typ_select=self.typeahead_select(),
+ typ_change=self.typeahead_change(),
+ tok_create=self.tokenfield_create(),
+ tok_edit=self.tokenfield_edit(),
+ tok_remove=self.tokenfield_remove(),
+ typ_updates=self.typeahead_updates(),
+ tok_updates=self.tokenfield_updates(),
+ tok_init_input=self.tokenfield_init_input(),
+ typ_init_input=self.typeahead_init_input()
+ )
+
+
+ def get_script(self):
+ """ Insert the JS code inside a script tag """
+ self.js_script = render_tag('script', content=mark_safe(self.js_script))
+
+
+ def del_select(self):
+ """ JS code to delete the select if it has been generated and replace
+ it with an input. """
+ return (
+ 'var p = $("#{select_id}").parent()[0];'
+ 'var new_input = `{replace_input}`;'
+ 'p.innerHTML = new_input;'
+ ).format(
+ select_id=self.input_id,
+ replace_input=self.replace_input
+ )
+
+
+ def gen_hidden(self):
+ """ JS code to add a hidden tag to store the value. """
+ return (
+ 'var d = $("#{div2_id}")[0];'
+ 'var i = document.createElement("input");'
+ 'i.id = "{hidden_id}";'
+ 'i.name = "{html_name}";'
+ 'i.value = "";'
+ 'i.type = "hidden";'
+ 'd.appendChild(i);'
+ ).format(
+ div2_id=self.div2_id,
+ hidden_id=self.hidden_id,
+ html_name=self.bound.html_name
+ )
+
+
+ def typeahead_init_input(self):
+ """ JS code to init the fields values """
+ init_key = self.bound.value() or '""'
+ return (
+ '$( "#{input_id}" ).typeahead("val", {init_val});'
+ '$( "#{hidden_id}" ).val( {init_key} );'
+ ).format(
+ input_id=self.input_id,
+ init_val='""' if init_key == '""' else
+ 'engine_{name}.get( {init_key} )[0].value'.format(
+ name=self.name,
+ init_key=init_key
+ ),
+ init_key=init_key,
+ hidden_id=self.hidden_id
+ )
+
+
+ def typeahead_reset_input(self):
+ """ JS code to reset the fields values """
+ return (
+ '$( "#{input_id}" ).typeahead("val", "");'
+ '$( "#{hidden_id}" ).val( "" );'
+ ).format(
+ input_id=self.input_id,
+ hidden_id=self.hidden_id
+ )
+
+
+ def typeahead_select(self):
+ """ JS code to create 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=self.hidden_id
+ )
+
+
+ def typeahead_change(self):
+ """ JS code of 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=self.input_id,
+ hidden_id=self.hidden_id
+ )
+
+
+ def typeahead_updates(self):
+ """ JS code for binding external fields changes with a reset """
+ reset_input = self.typeahead_reset_input()
+ updates = [
+ (
+ '$( "#{u_id}" ).change( function() {{'
+ ' setup_{name}();'
+ ' {reset_input}'
+ '}} );'
+ ).format(
+ u_id=u_id,
+ name=self.name,
+ reset_input=reset_input
+ ) for u_id in self.update_on]
+ return ''.join(updates)
+
+
+ def tokenfield_init_input(self):
+ """ JS code to init the fields values """
+ init_key = self.bound.value() or '""'
+ return (
+ '$( "#{input_id}" ).tokenfield("setTokens", {init_val});'
+ ).format(
+ input_id=self.input_id,
+ init_val='""' if init_key == '""' else (
+ 'engine_{name}.get( {init_key} ).map('
+ ' function(o) {{ return o.value; }}'
+ ')').format(
+ name=self.name,
+ init_key=init_key
+ )
+ )
+
+
+ def tokenfield_reset_input(self):
+ """ JS code to reset the fields values """
+ return (
+ '$( "#{input_id}" ).tokenfield("setTokens", "");'
+ ).format(
+ input_id=self.input_id
+ )
+
+
+ def tokenfield_create(self):
+ """ JS code triggered when a new token is created in tokenfield. """
+ return (
+ 'function(evt) {{'
+ ' var k = evt.attrs.key;'
+ ' if (!k) {{'
+ ' var data = evt.attrs.value;'
+ ' var i = 0;'
+ ' while ( i&q=foo&search=bar&name=johnDoe
+
+ **Usage**::
+
+ {% url_insert_param [URL] [param1=val1 [param2=val2 [...]]] %}
+
+ **Example**::
+
+ {% url_insert_param a=0 b="bar" %}
+ return "?a=0&b=bar"
+
+ {% url_insert_param "url.net/foo.html" a=0 b="bar" %}
+ return "url.net/foo.html?a=0&b=bar"
+
+ {% url_insert_param "url.net/foo.html?c=keep" a=0 b="bar" %}
+ return "url.net/foo.html?c=keep&a=0&b=bar"
+
+ {% url_insert_param "url.net/foo.html?a=del" a=0 b="bar" %}
+ return "url.net/foo.html?a=0&b=bar"
+
+ {% url_insert_param "url.net/foo.html?a=del&c=keep" a=0 b="bar" %}
+ return "url.net/foo.hmtl?a=0&c=keep&b=bar"
+ """
+
+ # Get existing parameters in the url
+ params = {}
+ if '?' in url:
+ url, parameters = url.split('?', maxsplit=1)
+ for parameter in parameters.split('&'):
+ p_name, p_value = parameter.split('=', maxsplit=1)
+ if p_name not in params:
+ params[p_name] = []
+ params[p_name].append(p_value)
+
+ # Add the request parameters to the list of parameters
+ for key, value in kwargs.items():
+ params[key] = [value]
+
+ # Write the url
+ url += '?'
+ for param, value_list in params.items():
+ for value in value_list:
+ url += str(param) + '=' + str(value) + '&'
+
+ # Remove the last '&' (or '?' if no parameters)
+ return url[:-1]
diff --git a/re2o/urls.py b/re2o/urls.py
index 5fd45f85..775b87ec 100644
--- a/re2o/urls.py
+++ b/re2o/urls.py
@@ -49,10 +49,16 @@ urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^users/', include('users.urls', namespace='users')),
url(r'^search/', include('search.urls', namespace='search')),
- url(r'^cotisations/', include('cotisations.urls', namespace='cotisations')),
+ url(
+ r'^cotisations/',
+ include('cotisations.urls', namespace='cotisations')
+ ),
url(r'^machines/', include('machines.urls', namespace='machines')),
url(r'^topologie/', include('topologie.urls', namespace='topologie')),
url(r'^logs/', include('logs.urls', namespace='logs')),
- url(r'^preferences/', include('preferences.urls', namespace='preferences')),
+ url(
+ r'^preferences/',
+ include('preferences.urls', namespace='preferences')
+ ),
]
diff --git a/re2o/utils.py b/re2o/utils.py
new file mode 100644
index 00000000..a6e5c851
--- /dev/null
+++ b/re2o/utils.py
@@ -0,0 +1,264 @@
+# -*- mode: python; coding: utf-8 -*-
+# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
+# se veut agnostique au réseau considéré, de manière à être installable en
+# quelques clics.
+#
+# Copyright © 2017 Gabriel Détraz
+# Copyright © 2017 Goulven Kermarec
+# Copyright © 2017 Augustin Lemesle
+#
+# 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.
+
+# -*- coding: utf-8 -*-
+# David Sinquin, Gabriel Détraz, Goulven Kermarec
+"""
+Regroupe les fonctions transversales utiles
+
+Fonction :
+ - récupérer tous les utilisateurs actifs
+ - récupérer toutes les machines
+ - récupérer tous les bans
+ etc
+"""
+
+
+from __future__ import unicode_literals
+
+
+from django.utils import timezone
+from django.db.models import Q
+
+from cotisations.models import Cotisation, Facture, Paiement, Vente
+from machines.models import Domain, Interface, Machine
+from users.models import Adherent, User, Ban, Whitelist
+from preferences.models import Service
+
+DT_NOW = timezone.now()
+
+
+def all_adherent(search_time=DT_NOW):
+ """ Fonction renvoyant tous les users adherents. Optimisee pour n'est
+ qu'une seule requete sql
+ Inspecte les factures de l'user et ses cotisation, regarde si elles
+ sont posterieur à now (end_time)"""
+ return User.objects.filter(
+ facture__in=Facture.objects.filter(
+ vente__in=Vente.objects.filter(
+ Q(type_cotisation='All') | Q(type_cotisation='Adhesion'),
+ cotisation__in=Cotisation.objects.filter(
+ vente__in=Vente.objects.filter(
+ facture__in=Facture.objects.all().exclude(valid=False)
+ )
+ ).filter(date_end__gt=search_time)
+ )
+ )
+ ).distinct()
+
+
+def all_baned(search_time=DT_NOW):
+ """ Fonction renvoyant tous les users bannis """
+ return User.objects.filter(
+ ban__in=Ban.objects.filter(
+ date_end__gt=search_time
+ )
+ ).distinct()
+
+
+def all_whitelisted(search_time=DT_NOW):
+ """ Fonction renvoyant tous les users whitelistes """
+ return User.objects.filter(
+ whitelist__in=Whitelist.objects.filter(
+ date_end__gt=search_time
+ )
+ ).distinct()
+
+
+def all_has_access(search_time=DT_NOW):
+ """ Renvoie tous les users beneficiant d'une connexion
+ : user adherent ou whiteliste et non banni """
+ return User.objects.filter(
+ Q(state=User.STATE_ACTIVE) &
+ ~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) &
+ (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) |
+ Q(facture__in=Facture.objects.filter(
+ vente__in=Vente.objects.filter(
+ cotisation__in=Cotisation.objects.filter(
+ Q(type_cotisation='All') | Q(type_cotisation='Connexion'),
+ vente__in=Vente.objects.filter(
+ facture__in=Facture.objects.all()
+ .exclude(valid=False)
+ )
+ ).filter(date_end__gt=search_time)
+ )
+ )))
+ ).distinct()
+
+
+def filter_active_interfaces(interface_set):
+ """Filtre les machines autorisées à sortir sur internet dans une requête"""
+ return interface_set.filter(
+ machine__in=Machine.objects.filter(
+ user__in=all_has_access()
+ ).filter(active=True)
+ ).select_related('domain').select_related('machine')\
+ .select_related('type').select_related('ipv4')\
+ .select_related('domain__extension').select_related('ipv4__ip_type')\
+ .distinct()
+
+
+def all_active_interfaces():
+ """Renvoie l'ensemble des machines autorisées à sortir sur internet """
+ return filter_active_interfaces(Interface.objects)
+
+
+def all_active_assigned_interfaces():
+ """ Renvoie l'ensemble des machines qui ont une ipv4 assignées et
+ disposant de l'accès internet"""
+ return all_active_interfaces().filter(ipv4__isnull=False)
+
+
+def all_active_interfaces_count():
+ """ Version light seulement pour compter"""
+ return Interface.objects.filter(
+ machine__in=Machine.objects.filter(
+ user__in=all_has_access()
+ ).filter(active=True)
+ )
+
+
+def all_active_assigned_interfaces_count():
+ """ Version light seulement pour compter"""
+ return all_active_interfaces_count().filter(ipv4__isnull=False)
+
+class SortTable:
+ """ Class gathering uselful stuff to sort the colums of a table, according
+ to the column and order requested. It's used with a dict of possible
+ values and associated model_fields """
+
+ # All the possible possible values
+ # The naming convention is based on the URL or the views function
+ # The syntax to describe the sort to apply is a dict where the keys are
+ # the url value and the values are a list of model field name to use to
+ # order the request. They are applied in the order they are given.
+ # A 'default' might be provided to specify what to do if the requested col
+ # doesn't match any keys.
+ USERS_INDEX = {
+ 'user_name': ['name'],
+ 'user_surname': ['surname'],
+ 'user_pseudo': ['pseudo'],
+ 'user_room': ['room'],
+ 'default': ['state', 'pseudo']
+ }
+ USERS_INDEX_BAN = {
+ 'ban_user': ['user__pseudo'],
+ 'ban_start': ['date_start'],
+ 'ban_end': ['date_end'],
+ 'default': ['-date_end']
+ }
+ USERS_INDEX_WHITE = {
+ 'white_user': ['user__pseudo'],
+ 'white_start': ['date_start'],
+ 'white_end': ['date_end'],
+ 'default': ['-date_end']
+ }
+ MACHINES_INDEX = {
+ 'machine_name': ['name'],
+ 'default': ['pk']
+ }
+ COTISATIONS_INDEX = {
+ 'cotis_user': ['user__pseudo'],
+ 'cotis_paiement': ['paiement__moyen'],
+ 'cotis_date': ['date'],
+ 'cotis_id': ['id'],
+ 'default': ['-date']
+ }
+ COTISATIONS_CONTROL = {
+ 'control_name': ['user__adherent__name'],
+ 'control_surname': ['user__surname'],
+ 'control_paiement': ['paiement'],
+ 'control_date': ['date'],
+ 'control_valid': ['valid'],
+ 'control_control': ['control'],
+ 'control_id': ['id'],
+ 'control_user-id': ['user__id'],
+ 'default': ['-date']
+ }
+ TOPOLOGIE_INDEX = {
+ 'switch_dns': ['switch_interface__domain__name'],
+ 'switch_ip': ['switch_interface__ipv4__ipv4'],
+ 'switch_loc': ['location'],
+ 'switch_ports': ['number'],
+ 'switch_stack': ['stack__name'],
+ 'default': ['location', 'stack', 'stack_member_id']
+ }
+ TOPOLOGIE_INDEX_PORT = {
+ 'port_port': ['port'],
+ 'port_room': ['room__name'],
+ 'port_interface': ['machine_interface__domain__name'],
+ 'port_related': ['related__switch__name'],
+ 'port_radius': ['radius'],
+ 'port_vlan': ['vlan_force__name'],
+ 'default': ['port']
+ }
+ TOPOLOGIE_INDEX_ROOM = {
+ 'room_name': ['name'],
+ 'default': ['name']
+ }
+ TOPOLOGIE_INDEX_STACK = {
+ 'stack_name': ['name'],
+ 'stack_id': ['stack_id'],
+ 'default': ['stack_id'],
+ }
+ TOPOLOGIE_INDEX_MODEL_SWITCH = {
+ 'model_switch_name': ['reference'],
+ 'model_switch__contructor' : ['constructor__name'],
+ 'default': ['reference'],
+ }
+ TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = {
+ 'room_name': ['name'],
+ 'default': ['name'],
+ }
+ LOGS_INDEX = {
+ 'sum_date': ['revision__date_created'],
+ 'default': ['-revision__date_created'],
+ }
+ LOGS_STATS_LOGS = {
+ 'logs_author': ['user__name'],
+ 'logs_date': ['date_created'],
+ 'default': ['-date_created']
+ }
+
+ @staticmethod
+ def sort(request, col, order, values):
+ """ Check if the given values are possible and add .order_by() and
+ a .reverse() as specified according to those values """
+ fields = values.get(col, None)
+ if not fields:
+ fields = values.get('default', [])
+ request = request.order_by(*fields)
+ if values.get(col, None) and order == 'desc':
+ return request.reverse()
+ else:
+ return request
+
+
+def remove_user_room(room):
+ """ Déménage de force l'ancien locataire de la chambre """
+ try:
+ user = Adherent.objects.get(room=room)
+ except Adherent.DoesNotExist:
+ return
+ user.room = None
+ user.save()
diff --git a/re2o/views.py b/re2o/views.py
index bd8077e1..9cab6273 100644
--- a/re2o/views.py
+++ b/re2o/views.py
@@ -19,25 +19,28 @@
# 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.
+"""
+Fonctions de la page d'accueil et diverses fonctions utiles pour tous
+les views
+"""
from __future__ import unicode_literals
from django.shortcuts import render
-from django.shortcuts import get_object_or_404
from django.template.context_processors import csrf
-from django.template import Context, RequestContext, loader
from preferences.models import Service
+
def form(ctx, template, request):
- c = ctx
- c.update(csrf(request))
- return render(request, template, c)
+ """Form générique, raccourci importé par les fonctions views du site"""
+ context = ctx
+ context.update(csrf(request))
+ return render(request, template, context)
def index(request):
- i = 0
+ """Affiche la liste des services sur la page d'accueil de re2o"""
services = [[], [], []]
for indice, serv in enumerate(Service.objects.all()):
services[indice % 3].append(serv)
-
return form({'services_urls': services}, 're2o/index.html', request)
diff --git a/re2o/wsgi.py b/re2o/wsgi.py
index 70108566..deb6b330 100644
--- a/re2o/wsgi.py
+++ b/re2o/wsgi.py
@@ -32,9 +32,10 @@ https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
from __future__ import unicode_literals
import os
-from django.core.wsgi import get_wsgi_application
-from os.path import dirname
import sys
+from os.path import dirname
+from django.core.wsgi import get_wsgi_application
+
sys.path.append(dirname(dirname(__file__)))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "re2o.settings")
diff --git a/search/admin.py b/search/admin.py
index bcdc4f1d..decd096a 100644
--- a/search/admin.py
+++ b/search/admin.py
@@ -21,8 +21,8 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+"""The field used in the admin view for the search app"""
+
from __future__ import unicode_literals
-from django.contrib import admin
-
# Register your models here.
diff --git a/search/forms.py b/search/forms.py
index fa43be55..90f7407f 100644
--- a/search/forms.py
+++ b/search/forms.py
@@ -20,21 +20,72 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+"""The forms used by the search app"""
+
from __future__ import unicode_literals
-from django.db.models import Q
-from simple_search import BaseSearchForm
+from django import forms
+from django.forms import Form
-from users.models import User, School
+CHOICES_USER = (
+ ('0', 'Actifs'),
+ ('1', 'Désactivés'),
+ ('2', 'Archivés'),
+)
-class UserSearchForm(BaseSearchForm):
- class Meta:
- base_qs = User.objects
- search_fields = ('^name', 'description', 'specifications', '=id')
+CHOICES_AFF = (
+ ('0', 'Utilisateurs'),
+ ('1', 'Machines'),
+ ('2', 'Factures'),
+ ('3', 'Bannissements'),
+ ('4', 'Accès à titre gracieux'),
+ ('5', 'Chambres'),
+ ('6', 'Ports'),
+ ('7', 'Switchs'),
+)
- # assumes a fulltext index has been defined on the fields
- # 'name,description,specifications,id'
- fulltext_indexes = (
- ('name', 2), # name matches are weighted higher
- ('name,description,specifications,id', 1),
- )
+
+def initial_choices(c):
+ """Return the choices that should be activated by default for a
+ given set of choices"""
+ return [i[0] for i in c]
+
+
+class SearchForm(Form):
+ """The form for a simple search"""
+ q = forms.CharField(label='Search', max_length=100)
+
+
+class SearchFormPlus(Form):
+ """The form for an advanced search (with filters)"""
+ q = forms.CharField(
+ label='Search',
+ max_length=100,
+ required=False
+ )
+ u = forms.MultipleChoiceField(
+ label="Filtre utilisateurs",
+ required=False,
+ widget=forms.CheckboxSelectMultiple,
+ choices=CHOICES_USER,
+ initial=initial_choices(CHOICES_USER)
+ )
+ a = forms.MultipleChoiceField(
+ label="Filtre affichage",
+ required=False,
+ widget=forms.CheckboxSelectMultiple,
+ choices=CHOICES_AFF,
+ initial=initial_choices(CHOICES_AFF)
+ )
+ s = forms.DateField(
+ required=False,
+ label="Date de début",
+ help_text='DD/MM/YYYY',
+ input_formats=['%d/%m/%Y']
+ )
+ e = forms.DateField(
+ required=False,
+ help_text='DD/MM/YYYY',
+ input_formats=['%d/%m/%Y'],
+ label="Date de fin"
+ )
diff --git a/search/models.py b/search/models.py
deleted file mode 100644
index 8d1fa0e4..00000000
--- a/search/models.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# -*- mode: python; coding: utf-8 -*-
-# Re2o est un logiciel d'administration développé initiallement au rezometz. Il
-# se veut agnostique au réseau considéré, de manière à être installable en
-# quelques clics.
-#
-# Copyright © 2017 Gabriel Détraz
-# Copyright © 2017 Goulven Kermarec
-# Copyright © 2017 Augustin Lemesle
-#
-# 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 __future__ import unicode_literals
-
-from django.db import models
-from django import forms
-from django.forms import Form
-from django.forms import ModelForm
-
-CHOICES = (
- ('0', 'Actifs'),
- ('1', 'Désactivés'),
- ('2', 'Archivés'),
-)
-
-CHOICES2 = (
- (1, 'Active'),
- ("", 'Désactivée'),
-)
-
-CHOICES3 = (
- ('0', 'Utilisateurs'),
- ('1', 'Machines'),
- ('2', 'Factures'),
- ('3', 'Bannissements'),
- ('4', 'Accès à titre gracieux'),
- ('6', 'Switchs'),
- ('5', 'Ports'),
-)
-
-
-class SearchForm(Form):
- search_field = forms.CharField(label = 'Search', max_length = 100)
-
-class SearchFormPlus(Form):
- search_field = forms.CharField(label = 'Search', max_length = 100, required=False)
- filtre = forms.MultipleChoiceField(label="Filtre utilisateurs", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES)
- connexion = forms.MultipleChoiceField(label="Filtre connexion", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES2)
- affichage = forms.MultipleChoiceField(label="Filtre affichage", required=False, widget =forms.CheckboxSelectMultiple,choices=CHOICES3)
- date_deb = forms.DateField(required=False, label="Date de début", help_text='DD/MM/YYYY', input_formats=['%d/%m/%Y'])
- date_fin = forms.DateField(required=False, help_text='DD/MM/YYYY', input_formats=['%d/%m/%Y'], label="Date de fin")
diff --git a/search/templates/search/index.html b/search/templates/search/index.html
index 859193fb..c043a22b 100644
--- a/search/templates/search/index.html
+++ b/search/templates/search/index.html
@@ -36,30 +36,35 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Résultats dans les machines :
{% include "machines/aff_machines.html" with machines_list=machines_list %}
{% endif %}
- {% if facture_list %}
+ {% if factures_list %}
Résultats dans les factures :
- {% include "cotisations/aff_cotisations.html" with facture_list=facture_list %}
+ {% include "cotisations/aff_cotisations.html" with facture_list=factures_list %}
{% endif %}
- {% if white_list %}
+ {% if whitelists_list %}
Résultats dans les accès à titre gracieux :
- {% include "users/aff_whitelists.html" with white_list=white_list %}
+ {% include "users/aff_whitelists.html" with white_list=whitelists_list %}
{% endif %}
- {% if ban_list %}
+ {% if bans_list %}
Résultats dans les banissements :
- {% include "users/aff_bans.html" with ban_list=ban_list %}
+ {% include "users/aff_bans.html" with ban_list=bans_list %}
{% endif %}
- {% if switch_list %}
- Résultats dans les switchs :
- {% include "topologie/aff_switch.html" with switch_list=switch_list %}
+ {% if rooms_list %}
+ Résultats dans les chambres :
+ {% include "topologie/aff_chambres.html" with room_list=rooms_list %}
{% endif %}
- {% if port_list %}
+ {% if switch_ports_list %}
Résultats dans les ports :
- {% include "topologie/aff_port.html" with port_list=port_list %}
+ {% include "topologie/aff_port.html" with port_list=switch_ports_list %}
{% endif %}
- {% if not ban_list and not interfaces_list and not users_list and not facture_list and not white_list and not port_list and not switch_list%}
+ {% if switches_list %}
+ Résultats dans les switchs :
+ {% include "topologie/aff_switch.html" with switch_list=switches_list %}
+ {% endif %}
+ {% if not users_list and not machines_list and not factures_list and not whitelists_list and not bans_list and not rooms_list and not switch_ports_list and not switches_list %}
Aucun résultat
+ {% else %}
+ (Seulement les {{ max_result }} premiers résultats sont affichés dans chaque catégorie)
{% endif %}
- (Seulement les {{ max_result }} premiers résultats sont affichés dans chaque catégorie)
diff --git a/search/templates/search/search.html b/search/templates/search/search.html
index adb5dd92..2c7fd0b6 100644
--- a/search/templates/search/search.html
+++ b/search/templates/search/search.html
@@ -28,11 +28,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block title %}Recherche{% endblock %}
{% block content %}
-{% bootstrap_form_errors searchform %}
+{% bootstrap_form_errors search_form %}
-
diff --git a/search/urls.py b/search/urls.py
index 3b16fcd1..dc1490e5 100644
--- a/search/urls.py
+++ b/search/urls.py
@@ -20,6 +20,8 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+"""The urls used by the search app"""
+
from __future__ import unicode_literals
from django.conf.urls import url
@@ -28,5 +30,5 @@ from . import views
urlpatterns = [
url(r'^$', views.search, name='search'),
- url(r'^avance/$', views.searchp, name='searchp'),
+ url(r'^advanced/$', views.searchp, name='searchp'),
]
diff --git a/search/views.py b/search/views.py
index fa9a43e8..4c28de63 100644
--- a/search/views.py
+++ b/search/views.py
@@ -20,115 +20,326 @@
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-# App de recherche pour re2o
-# Augustin lemesle, Gabriel Détraz, Goulven Kermarec
-# Gplv2
+"""The views for the search app, responsible for finding the matches
+Augustin lemesle, Gabriel Détraz, Goulven Kermarec, Maël Kervella
+Gplv2"""
+
from __future__ import unicode_literals
from django.shortcuts import render
-from django.shortcuts import get_object_or_404
-from django.template.context_processors import csrf
-from django.template import Context, RequestContext, loader
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from users.models import User, Ban, Whitelist
-from machines.models import Machine, Interface
-from topologie.models import Port, Switch
+from machines.models import Machine
+from topologie.models import Port, Switch, Room
from cotisations.models import Facture
-from search.models import SearchForm, SearchFormPlus
from preferences.models import GeneralOption
+from search.forms import (
+ SearchForm,
+ SearchFormPlus,
+ CHOICES_USER,
+ CHOICES_AFF,
+ initial_choices
+)
+from re2o.utils import SortTable
-def form(ctx, template, request):
- c = ctx
- c.update(csrf(request))
- return render(request, template, c)
-def search_result(search, type, request):
- date_deb = None
- date_fin = None
- states=[]
- co=[]
- aff=[]
- if(type):
- aff = search.cleaned_data['affichage']
- co = search.cleaned_data['connexion']
- states = search.cleaned_data['filtre']
- date_deb = search.cleaned_data['date_deb']
- date_fin = search.cleaned_data['date_fin']
- date_query = Q()
- if aff==[]:
- aff = ['0','1','2','3','4','5','6']
- if date_deb != None:
- date_query = date_query & Q(date__gte=date_deb)
- if date_fin != None:
- date_query = date_query & Q(date__lte=date_fin)
- search = search.cleaned_data['search_field']
- query1 = Q()
- for s in states:
- query1 = query1 | Q(state = s)
-
- connexion = []
-
- recherche = {'users_list': None, 'machines_list' : [], 'facture_list' : None, 'ban_list' : None, 'white_list': None, 'port_list': None, 'switch_list': None}
+def is_int(variable):
+ """ Check if the variable can be casted to an integer """
- if request.user.has_perms(('cableur',)):
- query = Q(user__pseudo__icontains = search) | Q(user__name__icontains = search) | Q(user__surname__icontains = search)
+ try:
+ int(variable)
+ except ValueError:
+ return False
else:
- query = (Q(user__pseudo__icontains = search) | Q(user__name__icontains = search) | Q(user__surname__icontains = search)) & Q(user = request.user)
+ return True
- for i in aff:
- if i == '0':
- query_user_list = Q(room__name__icontains = search) | Q(pseudo__icontains = search) | Q(name__icontains = search) | Q(surname__icontains = search) & query1
- if request.user.has_perms(('cableur',)):
- recherche['users_list'] = User.objects.filter(query_user_list).order_by('state', 'surname').distinct()
- else :
- recherche['users_list'] = User.objects.filter(query_user_list & Q(id=request.user.id)).order_by('state', 'surname').distinct()
- if i == '1':
- query_machine_list = Q(machine__user__pseudo__icontains = search) | Q(machine__user__name__icontains = search) | Q(machine__user__surname__icontains = search) | Q(mac_address__icontains = search) | Q(ipv4__ipv4__icontains = search) | Q(domain__name__icontains = search) | Q(domain__related_domain__name__icontains = search)
- if request.user.has_perms(('cableur',)):
- data = Interface.objects.filter(query_machine_list).distinct()
- else:
- data = Interface.objects.filter(query_machine_list & Q(machine__user__id = request.user.id)).distinct()
- for d in data:
- recherche['machines_list'].append(d.machine)
- if i == '2':
- recherche['facture_list'] = Facture.objects.filter(query & date_query).distinct()
- if i == '3':
- recherche['ban_list'] = Ban.objects.filter(query).distinct()
- if i == '4':
- recherche['white_list'] = Whitelist.objects.filter(query).distinct()
- if i == '5':
- recherche['port_list'] = Port.objects.filter(details__icontains = search).distinct()
- if not request.user.has_perms(('cableur',)):
- recherche['port_list'] = None
- if i == '6':
- recherche['switch_list'] = Switch.objects.filter(details__icontains = search).distinct()
- if not request.user.has_perms(('cableur',)):
- recherche['switch_list'] = None
- options, created = GeneralOption.objects.get_or_create()
- search_display_page = options.search_display_page
+def get_results(query, request, filters={}):
+ """ Construct the correct filters to match differents fields of some models
+ with the given query according to the given filters.
+ The match field are either CharField or IntegerField that will be displayed
+ on the results page (else, one might not see why a result has matched the
+ query). IntegerField are matched against the query only if it can be casted
+ to an int."""
- for r in recherche:
- if recherche[r] != None:
- recherche[r] = recherche[r][:search_display_page]
+ start = filters.get('s', None)
+ end = filters.get('e', None)
+ user_state = filters.get('u', initial_choices(CHOICES_USER))
+ aff = filters.get('a', initial_choices(CHOICES_AFF))
- recherche.update({'max_result': search_display_page})
+ options, _ = GeneralOption.objects.get_or_create()
+ max_result = options.search_display_page
+
+ results = {
+ 'users_list': User.objects.none(),
+ 'machines_list': Machine.objects.none(),
+ 'factures_list': Facture.objects.none(),
+ 'bans_list': Ban.objects.none(),
+ 'whitelists_list': Whitelist.objects.none(),
+ 'rooms_list': Room.objects.none(),
+ 'switch_ports_list': Port.objects.none(),
+ 'switches_list': Switch.objects.none()
+ }
+
+ # Users
+ if '0' in aff:
+ filter_user_list = (
+ Q(
+ surname__icontains=query
+ ) | Q(
+ adherent__name__icontains=query
+ ) | Q(
+ pseudo__icontains=query
+ ) | Q(
+ club__room__name__icontains=query
+ ) | Q(
+ adherent__room__name__icontains=query
+ )
+ ) & Q(state__in=user_state)
+ if not request.user.has_perms(('cableur',)):
+ filter_user_list &= Q(id=request.user.id)
+ results['users_list'] = User.objects.filter(filter_user_list)
+ results['users_list'] = SortTable.sort(
+ results['users_list'],
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.USERS_INDEX
+ )
+
+ # Machines
+ if '1' in aff:
+ filter_machine_list = Q(
+ name__icontains=query
+ ) | (
+ Q(
+ user__pseudo__icontains=query
+ ) & Q(
+ user__state__in=user_state
+ )
+ ) | Q(
+ interface__domain__name__icontains=query
+ ) | Q(
+ interface__domain__related_domain__name__icontains=query
+ ) | Q(
+ interface__mac_address__icontains=query
+ ) | Q(
+ interface__ipv4__ipv4__icontains=query
+ )
+ if not request.user.has_perms(('cableur',)):
+ filter_machine_list &= Q(user__id=request.user.id)
+ results['machines_list'] = Machine.objects.filter(filter_machine_list)
+ results['machines_list'] = SortTable.sort(
+ results['machines_list'],
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.MACHINES_INDEX
+ )
+
+ # Factures
+ if '2' in aff:
+ filter_facture_list = Q(
+ user__pseudo__icontains=query
+ ) & Q(
+ user__state__in=user_state
+ )
+ if start is not None:
+ filter_facture_list &= Q(date__gte=start)
+ if end is not None:
+ filter_facture_list &= Q(date__lte=end)
+ results['factures_list'] = Facture.objects.filter(filter_facture_list)
+ results['factures_list'] = SortTable.sort(
+ results['factures_list'],
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.COTISATIONS_INDEX
+ )
+
+ # Bans
+ if '3' in aff:
+ date_filter = (
+ Q(
+ user__pseudo__icontains=query
+ ) & Q(
+ user__state__in=user_state
+ )
+ ) | Q(
+ raison__icontains=query
+ )
+ if start is not None:
+ date_filter &= (
+ Q(date_start__gte=start) & Q(date_end__gte=start)
+ ) | (
+ Q(date_start__lte=start) & Q(date_end__gte=start)
+ ) | (
+ Q(date_start__gte=start) & Q(date_end__lte=start)
+ )
+ if end is not None:
+ date_filter &= (
+ Q(date_start__lte=end) & Q(date_end__lte=end)
+ ) | (
+ Q(date_start__lte=end) & Q(date_end__gte=end)
+ ) | (
+ Q(date_start__gte=end) & Q(date_end__lte=end)
+ )
+ results['bans_list'] = Ban.objects.filter(date_filter)
+ results['bans_list'] = SortTable.sort(
+ results['bans_list'],
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.USERS_INDEX_BAN
+ )
+
+ # Whitelists
+ if '4' in aff:
+ date_filter = (
+ Q(
+ user__pseudo__icontains=query
+ ) & Q(
+ user__state__in=user_state
+ )
+ ) | Q(
+ raison__icontains=query
+ )
+ if start is not None:
+ date_filter &= (
+ Q(date_start__gte=start) & Q(date_end__gte=start)
+ ) | (
+ Q(date_start__lte=start) & Q(date_end__gte=start)
+ ) | (
+ Q(date_start__gte=start) & Q(date_end__lte=start)
+ )
+ if end is not None:
+ date_filter &= (
+ Q(date_start__lte=end) & Q(date_end__lte=end)
+ ) | (
+ Q(date_start__lte=end) & Q(date_end__gte=end)
+ ) | (
+ Q(date_start__gte=end) & Q(date_end__lte=end)
+ )
+ results['whitelists_list'] = Whitelist.objects.filter(date_filter)
+ results['whitelists_list'] = SortTable.sort(
+ results['whitelists_list'],
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.USERS_INDEX_WHITE
+ )
+
+ # Rooms
+ if '5' in aff and request.user.has_perms(('cableur',)):
+ filter_rooms_list = Q(
+ details__icontains=query
+ ) | Q(
+ name__icontains=query
+ ) | Q(
+ port__details=query
+ )
+ results['rooms_list'] = Room.objects.filter(filter_rooms_list)
+ results['rooms_list'] = SortTable.sort(
+ results['rooms_list'],
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.TOPOLOGIE_INDEX_ROOM
+ )
+
+ # Switch ports
+ if '6' in aff and request.user.has_perms(('cableur',)):
+ filter_ports_list = Q(
+ room__name__icontains=query
+ ) | Q(
+ machine_interface__domain__name__icontains=query
+ ) | Q(
+ related__switch__switch_interface__domain__name__icontains=query
+ ) | Q(
+ radius__icontains=query
+ ) | Q(
+ vlan_force__name__icontains=query
+ ) | Q(
+ details__icontains=query
+ )
+ if is_int(query):
+ filter_ports_list |= Q(
+ port=query
+ )
+ results['switch_ports_list'] = Port.objects.filter(filter_ports_list)
+ results['switch_ports_list'] = SortTable.sort(
+ results['switch_ports_list'],
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.TOPOLOGIE_INDEX_PORT
+ )
+
+ # Switches
+ if '7' in aff and request.user.has_perms(('cableur',)):
+ filter_switches_list = Q(
+ switch_interface__domain__name__icontains=query
+ ) | Q(
+ switch_interface__ipv4__ipv4__icontains=query
+ ) | Q(
+ location__icontains=query
+ ) | Q(
+ stack__name__icontains=query
+ ) | Q(
+ model__reference__icontains=query
+ ) | Q(
+ model__constructor__name__icontains=query
+ ) | Q(
+ details__icontains=query
+ )
+ if is_int(query):
+ filter_switches_list |= Q(
+ number=query
+ ) | Q(
+ stack_member_id=query
+ )
+ results['switches_list'] = Switch.objects.filter(filter_switches_list)
+ results['switches_list'] = SortTable.sort(
+ results['switches_list'],
+ request.GET.get('col'),
+ request.GET.get('order'),
+ SortTable.TOPOLOGIE_INDEX
+ )
+
+ for name, val in results.items():
+ results[name] = val.distinct()[:max_result]
+
+ results.update({'max_result': max_result})
+ results.update({'search_term': query})
+
+ return results
- return recherche
@login_required
def search(request):
- search = SearchForm(request.POST or None)
- if search.is_valid():
- return form(search_result(search, False, request), 'search/index.html',request)
- return form({'searchform' : search}, 'search/search.html', request)
+ """ La page de recherche standard """
+ search_form = SearchForm(request.GET or None)
+ if search_form.is_valid():
+ return render(
+ request,
+ 'search/index.html',
+ get_results(
+ search_form.cleaned_data.get('q', ''),
+ request,
+ search_form.cleaned_data
+ )
+ )
+ return render(request, 'search/search.html', {'search_form': search_form})
+
@login_required
def searchp(request):
- search = SearchFormPlus(request.POST or None)
- if search.is_valid():
- return form(search_result(search, True, request), 'search/index.html',request)
- return form({'searchform' : search}, 'search/search.html', request)
+ """ La page de recherche avancée """
+ search_form = SearchFormPlus(request.GET or None)
+ if search_form.is_valid():
+ return render(
+ request,
+ 'search/index.html',
+ get_results(
+ search_form.cleaned_data.get('q', ''),
+ request,
+ search_form.cleaned_data
+ )
+ )
+ return render(request, 'search/search.html', {'search_form': search_form})
diff --git a/static/css/bootstrap-tokenfield.css b/static/css/bootstrap-tokenfield.css
new file mode 100644
index 00000000..ae12c1b7
--- /dev/null
+++ b/static/css/bootstrap-tokenfield.css
@@ -0,0 +1,210 @@
+/*!
+ * bootstrap-tokenfield
+ * https://github.com/sliptree/bootstrap-tokenfield
+ * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
+ */
+@-webkit-keyframes blink {
+ 0% {
+ border-color: #ededed;
+ }
+ 100% {
+ border-color: #b94a48;
+ }
+}
+@-moz-keyframes blink {
+ 0% {
+ border-color: #ededed;
+ }
+ 100% {
+ border-color: #b94a48;
+ }
+}
+@keyframes blink {
+ 0% {
+ border-color: #ededed;
+ }
+ 100% {
+ border-color: #b94a48;
+ }
+}
+.tokenfield {
+ height: auto;
+ min-height: 34px;
+ padding-bottom: 0px;
+}
+.tokenfield.focus {
+ border-color: #66afe9;
+ outline: 0;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, 0.6);
+}
+.tokenfield .token {
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ display: inline-block;
+ border: 1px solid #d9d9d9;
+ background-color: #ededed;
+ white-space: nowrap;
+ margin: -1px 5px 5px 0;
+ height: 22px;
+ vertical-align: top;
+ cursor: default;
+}
+.tokenfield .token:hover {
+ border-color: #b9b9b9;
+}
+.tokenfield .token.active {
+ border-color: #52a8ec;
+ border-color: rgba(82, 168, 236, 0.8);
+}
+.tokenfield .token.duplicate {
+ border-color: #ebccd1;
+ -webkit-animation-name: blink;
+ animation-name: blink;
+ -webkit-animation-duration: 0.1s;
+ animation-duration: 0.1s;
+ -webkit-animation-direction: normal;
+ animation-direction: normal;
+ -webkit-animation-timing-function: ease;
+ animation-timing-function: ease;
+ -webkit-animation-iteration-count: infinite;
+ animation-iteration-count: infinite;
+}
+.tokenfield .token.invalid {
+ background: none;
+ border: 1px solid transparent;
+ -webkit-border-radius: 0;
+ -moz-border-radius: 0;
+ border-radius: 0;
+ border-bottom: 1px dotted #d9534f;
+}
+.tokenfield .token.invalid.active {
+ background: #ededed;
+ border: 1px solid #ededed;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+.tokenfield .token .token-label {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ padding-left: 4px;
+ vertical-align: top;
+}
+.tokenfield .token .close {
+ font-family: Arial;
+ display: inline-block;
+ line-height: 100%;
+ font-size: 1.1em;
+ line-height: 1.49em;
+ margin-left: 5px;
+ float: none;
+ height: 100%;
+ vertical-align: top;
+ padding-right: 4px;
+}
+.tokenfield .token-input {
+ background: none;
+ width: 60px;
+ min-width: 60px;
+ border: 0;
+ height: 20px;
+ padding: 0;
+ margin-bottom: 6px;
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+.tokenfield .token-input:focus {
+ border-color: transparent;
+ outline: 0;
+ /* IE6-9 */
+ -webkit-box-shadow: none;
+ box-shadow: none;
+}
+.tokenfield.disabled {
+ cursor: not-allowed;
+ background-color: #eeeeee;
+}
+.tokenfield.disabled .token-input {
+ cursor: not-allowed;
+}
+.tokenfield.disabled .token:hover {
+ cursor: not-allowed;
+ border-color: #d9d9d9;
+}
+.tokenfield.disabled .token:hover .close {
+ cursor: not-allowed;
+ opacity: 0.2;
+ filter: alpha(opacity=20);
+}
+.has-warning .tokenfield.focus {
+ border-color: #66512c;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
+}
+.has-error .tokenfield.focus {
+ border-color: #843534;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
+}
+.has-success .tokenfield.focus {
+ border-color: #2b542c;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
+}
+.tokenfield.input-sm,
+.input-group-sm .tokenfield {
+ min-height: 30px;
+ padding-bottom: 0px;
+}
+.input-group-sm .token,
+.tokenfield.input-sm .token {
+ height: 20px;
+ margin-bottom: 4px;
+}
+.input-group-sm .token-input,
+.tokenfield.input-sm .token-input {
+ height: 18px;
+ margin-bottom: 5px;
+}
+.tokenfield.input-lg,
+.input-group-lg .tokenfield {
+ height: auto;
+ min-height: 45px;
+ padding-bottom: 4px;
+}
+.input-group-lg .token,
+.tokenfield.input-lg .token {
+ height: 25px;
+}
+.input-group-lg .token-label,
+.tokenfield.input-lg .token-label {
+ line-height: 23px;
+}
+.input-group-lg .token .close,
+.tokenfield.input-lg .token .close {
+ line-height: 1.3em;
+}
+.input-group-lg .token-input,
+.tokenfield.input-lg .token-input {
+ height: 23px;
+ line-height: 23px;
+ margin-bottom: 6px;
+ vertical-align: top;
+}
+.tokenfield.rtl {
+ direction: rtl;
+ text-align: right;
+}
+.tokenfield.rtl .token {
+ margin: -1px 0 5px 5px;
+}
+.tokenfield.rtl .token .token-label {
+ padding-left: 0px;
+ padding-right: 4px;
+}
diff --git a/static/js/bootstrap-tokenfield/LICENSE.md b/static/js/bootstrap-tokenfield/LICENSE.md
new file mode 100644
index 00000000..2449b356
--- /dev/null
+++ b/static/js/bootstrap-tokenfield/LICENSE.md
@@ -0,0 +1,23 @@
+#### Sliptree
+- by Illimar Tambek for [Sliptree](http://sliptree.com)
+- Copyright (c) 2013 by Sliptree
+
+Available for use under the [MIT License](http://en.wikipedia.org/wiki/MIT_License)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js b/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js
new file mode 100644
index 00000000..5b2759d4
--- /dev/null
+++ b/static/js/bootstrap-tokenfield/bootstrap-tokenfield.js
@@ -0,0 +1,1042 @@
+/*!
+ * bootstrap-tokenfield
+ * https://github.com/sliptree/bootstrap-tokenfield
+ * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT
+ */
+
+(function (factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module.
+ define(['jquery'], factory);
+ } else if (typeof exports === 'object') {
+ // For CommonJS and CommonJS-like environments where a window with jQuery
+ // is present, execute the factory with the jQuery instance from the window object
+ // For environments that do not inherently posses a window with a document
+ // (such as Node.js), expose a Tokenfield-making factory as module.exports
+ // This accentuates the need for the creation of a real window or passing in a jQuery instance
+ // e.g. require("bootstrap-tokenfield")(window); or require("bootstrap-tokenfield")($);
+ module.exports = global.window && global.window.$ ?
+ factory( global.window.$ ) :
+ function( input ) {
+ if ( !input.$ && !input.fn ) {
+ throw new Error( "Tokenfield requires a window object with jQuery or a jQuery instance" );
+ }
+ return factory( input.$ || input );
+ };
+ } else {
+ // Browser globals
+ factory(jQuery, window);
+ }
+}(function ($, window) {
+
+ "use strict"; // jshint ;_;
+
+ /* TOKENFIELD PUBLIC CLASS DEFINITION
+ * ============================== */
+
+ var Tokenfield = function (element, options) {
+ var _self = this
+
+ this.$element = $(element)
+ this.textDirection = this.$element.css('direction');
+
+ // Extend options
+ this.options = $.extend(true, {}, $.fn.tokenfield.defaults, { tokens: this.$element.val() }, this.$element.data(), options)
+
+ // Setup delimiters and trigger keys
+ this._delimiters = (typeof this.options.delimiter === 'string') ? [this.options.delimiter] : this.options.delimiter
+ this._triggerKeys = $.map(this._delimiters, function (delimiter) {
+ return delimiter.charCodeAt(0);
+ });
+ this._firstDelimiter = this._delimiters[0];
+
+ // Check for whitespace, dash and special characters
+ var whitespace = $.inArray(' ', this._delimiters)
+ , dash = $.inArray('-', this._delimiters)
+
+ if (whitespace >= 0)
+ this._delimiters[whitespace] = '\\s'
+
+ if (dash >= 0) {
+ delete this._delimiters[dash]
+ this._delimiters.unshift('-')
+ }
+
+ var specialCharacters = ['\\', '$', '[', '{', '^', '.', '|', '?', '*', '+', '(', ')']
+ $.each(this._delimiters, function (index, character) {
+ var pos = $.inArray(character, specialCharacters)
+ if (pos >= 0) _self._delimiters[index] = '\\' + character;
+ });
+
+ // Store original input width
+ var elRules = (window && typeof window.getMatchedCSSRules === 'function') ? window.getMatchedCSSRules( element ) : null
+ , elStyleWidth = element.style.width
+ , elCSSWidth
+ , elWidth = this.$element.width()
+
+ if (elRules) {
+ $.each( elRules, function (i, rule) {
+ if (rule.style.width) {
+ elCSSWidth = rule.style.width;
+ }
+ });
+ }
+
+ // Move original input out of the way
+ var hidingPosition = $('body').css('direction') === 'rtl' ? 'right' : 'left',
+ originalStyles = { position: this.$element.css('position') };
+ originalStyles[hidingPosition] = this.$element.css(hidingPosition);
+
+ this.$element
+ .data('original-styles', originalStyles)
+ .data('original-tabindex', this.$element.prop('tabindex'))
+ .css('position', 'absolute')
+ .css(hidingPosition, '-10000px')
+ .prop('tabindex', -1)
+
+ // Create a wrapper
+ this.$wrapper = $('
')
+ if (this.$element.hasClass('input-lg')) this.$wrapper.addClass('input-lg')
+ if (this.$element.hasClass('input-sm')) this.$wrapper.addClass('input-sm')
+ if (this.textDirection === 'rtl') this.$wrapper.addClass('rtl')
+
+ // Create a new input
+ var id = this.$element.prop('id') || new Date().getTime() + '' + Math.floor((1 + Math.random()) * 100)
+ this.$input = $(' ')
+ .appendTo( this.$wrapper )
+ .prop( 'placeholder', this.$element.prop('placeholder') )
+ .prop( 'id', id + '-tokenfield' )
+ .prop( 'tabindex', this.$element.data('original-tabindex') )
+
+ // Re-route original input label to new input
+ var $label = $( 'label[for="' + this.$element.prop('id') + '"]' )
+ if ( $label.length ) {
+ $label.prop( 'for', this.$input.prop('id') )
+ }
+
+ // Set up a copy helper to handle copy & paste
+ this.$copyHelper = $(' ').css('position', 'absolute').css(hidingPosition, '-10000px').prop('tabindex', -1).prependTo( this.$wrapper )
+
+ // Set wrapper width
+ if (elStyleWidth) {
+ this.$wrapper.css('width', elStyleWidth);
+ }
+ else if (elCSSWidth) {
+ this.$wrapper.css('width', elCSSWidth);
+ }
+ // If input is inside inline-form with no width set, set fixed width
+ else if (this.$element.parents('.form-inline').length) {
+ this.$wrapper.width( elWidth )
+ }
+
+ // Set tokenfield disabled, if original or fieldset input is disabled
+ if (this.$element.prop('disabled') || this.$element.parents('fieldset[disabled]').length) {
+ this.disable();
+ }
+
+ // Set tokenfield readonly, if original input is readonly
+ if (this.$element.prop('readonly')) {
+ this.readonly();
+ }
+
+ // Set up mirror for input auto-sizing
+ this.$mirror = $(' ');
+ this.$input.css('min-width', this.options.minWidth + 'px')
+ $.each([
+ 'fontFamily',
+ 'fontSize',
+ 'fontWeight',
+ 'fontStyle',
+ 'letterSpacing',
+ 'textTransform',
+ 'wordSpacing',
+ 'textIndent'
+ ], function (i, val) {
+ _self.$mirror[0].style[val] = _self.$input.css(val);
+ });
+ this.$mirror.appendTo( 'body' )
+
+ // Insert tokenfield to HTML
+ this.$wrapper.insertBefore( this.$element )
+ this.$element.prependTo( this.$wrapper )
+
+ // Calculate inner input width
+ this.update()
+
+ // Create initial tokens, if any
+ this.setTokens(this.options.tokens, false, ! this.$element.val() && this.options.tokens )
+
+ // Start listening to events
+ this.listen()
+
+ // Initialize autocomplete, if necessary
+ if ( ! $.isEmptyObject( this.options.autocomplete ) ) {
+ var side = this.textDirection === 'rtl' ? 'right' : 'left'
+ , autocompleteOptions = $.extend({
+ minLength: this.options.showAutocompleteOnFocus ? 0 : null,
+ position: { my: side + " top", at: side + " bottom", of: this.$wrapper }
+ }, this.options.autocomplete )
+
+ this.$input.autocomplete( autocompleteOptions )
+ }
+
+ // Initialize typeahead, if necessary
+ if ( ! $.isEmptyObject( this.options.typeahead ) ) {
+
+ var typeaheadOptions = this.options.typeahead
+ , defaults = {
+ minLength: this.options.showAutocompleteOnFocus ? 0 : null
+ }
+ , args = $.isArray( typeaheadOptions ) ? typeaheadOptions : [typeaheadOptions, typeaheadOptions]
+
+ args[0] = $.extend( {}, defaults, args[0] )
+
+ this.$input.typeahead.apply( this.$input, args )
+ this.typeahead = true
+ }
+ }
+
+ Tokenfield.prototype = {
+
+ constructor: Tokenfield
+
+ , createToken: function (attrs, triggerChange) {
+ var _self = this
+
+ if (typeof attrs === 'string') {
+ attrs = { value: attrs, label: attrs }
+ } else {
+ // Copy objects to prevent contamination of data sources.
+ attrs = $.extend( {}, attrs )
+ }
+
+ if (typeof triggerChange === 'undefined') {
+ triggerChange = true
+ }
+
+ // Normalize label and value
+ attrs.value = $.trim(attrs.value.toString());
+ attrs.label = attrs.label && attrs.label.length ? $.trim(attrs.label) : attrs.value
+
+ // Bail out if has no value or label, or label is too short
+ if (!attrs.value.length || !attrs.label.length || attrs.label.length <= this.options.minLength) return
+
+ // Bail out if maximum number of tokens is reached
+ if (this.options.limit && this.getTokens().length >= this.options.limit) return
+
+ // Allow changing token data before creating it
+ var createEvent = $.Event('tokenfield:createtoken', { attrs: attrs })
+ this.$element.trigger(createEvent)
+
+ // Bail out if there if attributes are empty or event was defaultPrevented
+ if (!createEvent.attrs || createEvent.isDefaultPrevented()) return
+
+ var $token = $('
')
+ .append(' ')
+ .append('× ')
+ .data('attrs', attrs)
+
+ // Insert token into HTML
+ if (this.$input.hasClass('tt-input')) {
+ // If the input has typeahead enabled, insert token before it's parent
+ this.$input.parent().before( $token )
+ } else {
+ this.$input.before( $token )
+ }
+
+ // Temporarily set input width to minimum
+ this.$input.css('width', this.options.minWidth + 'px')
+
+ var $tokenLabel = $token.find('.token-label')
+ , $closeButton = $token.find('.close')
+
+ // Determine maximum possible token label width
+ if (!this.maxTokenWidth) {
+ this.maxTokenWidth =
+ this.$wrapper.width() - $closeButton.outerWidth() -
+ parseInt($closeButton.css('margin-left'), 10) -
+ parseInt($closeButton.css('margin-right'), 10) -
+ parseInt($token.css('border-left-width'), 10) -
+ parseInt($token.css('border-right-width'), 10) -
+ parseInt($token.css('padding-left'), 10) -
+ parseInt($token.css('padding-right'), 10)
+ parseInt($tokenLabel.css('border-left-width'), 10) -
+ parseInt($tokenLabel.css('border-right-width'), 10) -
+ parseInt($tokenLabel.css('padding-left'), 10) -
+ parseInt($tokenLabel.css('padding-right'), 10)
+ parseInt($tokenLabel.css('margin-left'), 10) -
+ parseInt($tokenLabel.css('margin-right'), 10)
+ }
+
+ $tokenLabel.css('max-width', this.maxTokenWidth)
+ if (this.options.html)
+ $tokenLabel.html(attrs.label)
+ else
+ $tokenLabel.text(attrs.label)
+
+ // Listen to events on token
+ $token
+ .on('mousedown', function (e) {
+ if (_self._disabled || _self._readonly) return false
+ _self.preventDeactivation = true
+ })
+ .on('click', function (e) {
+ if (_self._disabled || _self._readonly) return false
+ _self.preventDeactivation = false
+
+ if (e.ctrlKey || e.metaKey) {
+ e.preventDefault()
+ return _self.toggle( $token )
+ }
+
+ _self.activate( $token, e.shiftKey, e.shiftKey )
+ })
+ .on('dblclick', function (e) {
+ if (_self._disabled || _self._readonly || !_self.options.allowEditing ) return false
+ _self.edit( $token )
+ })
+
+ $closeButton
+ .on('click', $.proxy(this.remove, this))
+
+ // Trigger createdtoken event on the original field
+ // indicating that the token is now in the DOM
+ this.$element.trigger($.Event('tokenfield:createdtoken', {
+ attrs: attrs,
+ relatedTarget: $token.get(0)
+ }))
+
+ // Trigger change event on the original field
+ if (triggerChange) {
+ this.$element.val( this.getTokensList() ).trigger( $.Event('change', { initiator: 'tokenfield' }) )
+ }
+
+ // Update tokenfield dimensions
+ var _self = this
+ setTimeout(function () {
+ _self.update()
+ }, 0)
+
+ // Return original element
+ return this.$element.get(0)
+ }
+
+ , setTokens: function (tokens, add, triggerChange) {
+ if (!add) this.$wrapper.find('.token').remove()
+
+ if (!tokens) return
+
+ if (typeof triggerChange === 'undefined') {
+ triggerChange = true
+ }
+
+ if (typeof tokens === 'string') {
+ if (this._delimiters.length) {
+ // Split based on delimiters
+ tokens = tokens.split( new RegExp( '[' + this._delimiters.join('') + ']' ) )
+ } else {
+ tokens = [tokens];
+ }
+ }
+
+ var _self = this
+ $.each(tokens, function (i, attrs) {
+ _self.createToken(attrs, triggerChange)
+ })
+
+ return this.$element.get(0)
+ }
+
+ , getTokenData: function($token) {
+ var data = $token.map(function() {
+ var $token = $(this);
+ return $token.data('attrs')
+ }).get();
+
+ if (data.length == 1) {
+ data = data[0];
+ }
+
+ return data;
+ }
+
+ , getTokens: function(active) {
+ var self = this
+ , tokens = []
+ , activeClass = active ? '.active' : '' // get active tokens only
+ this.$wrapper.find( '.token' + activeClass ).each( function() {
+ tokens.push( self.getTokenData( $(this) ) )
+ })
+ return tokens
+ }
+
+ , getTokensList: function(delimiter, beautify, active) {
+ delimiter = delimiter || this._firstDelimiter
+ beautify = ( typeof beautify !== 'undefined' && beautify !== null ) ? beautify : this.options.beautify
+
+ var separator = delimiter + ( beautify && delimiter !== ' ' ? ' ' : '')
+ return $.map( this.getTokens(active), function (token) {
+ return token.value
+ }).join(separator)
+ }
+
+ , getInput: function() {
+ return this.$input.val()
+ }
+
+ , setInput: function (val) {
+ if (this.$input.hasClass('tt-input')) {
+ // Typeahead acts weird when simply setting input value to empty,
+ // so we set the query to empty instead
+ this.$input.typeahead('val', val)
+ } else {
+ this.$input.val(val)
+ }
+ }
+
+ , listen: function () {
+ var _self = this
+
+ this.$element
+ .on('change', $.proxy(this.change, this))
+
+ this.$wrapper
+ .on('mousedown',$.proxy(this.focusInput, this))
+
+ this.$input
+ .on('focus', $.proxy(this.focus, this))
+ .on('blur', $.proxy(this.blur, this))
+ .on('paste', $.proxy(this.paste, this))
+ .on('keydown', $.proxy(this.keydown, this))
+ .on('keypress', $.proxy(this.keypress, this))
+ .on('keyup', $.proxy(this.keyup, this))
+
+ this.$copyHelper
+ .on('focus', $.proxy(this.focus, this))
+ .on('blur', $.proxy(this.blur, this))
+ .on('keydown', $.proxy(this.keydown, this))
+ .on('keyup', $.proxy(this.keyup, this))
+
+ // Secondary listeners for input width calculation
+ this.$input
+ .on('keypress', $.proxy(this.update, this))
+ .on('keyup', $.proxy(this.update, this))
+
+ this.$input
+ .on('autocompletecreate', function() {
+ // Set minimum autocomplete menu width
+ var $_menuElement = $(this).data('ui-autocomplete').menu.element
+
+ var minWidth = _self.$wrapper.outerWidth() -
+ parseInt( $_menuElement.css('border-left-width'), 10 ) -
+ parseInt( $_menuElement.css('border-right-width'), 10 )
+
+ $_menuElement.css( 'min-width', minWidth + 'px' )
+ })
+ .on('autocompleteselect', function (e, ui) {
+ if (_self.createToken( ui.item )) {
+ _self.$input.val('')
+ if (_self.$input.data( 'edit' )) {
+ _self.unedit(true)
+ }
+ }
+ return false
+ })
+ .on('typeahead:selected typeahead:autocompleted', function (e, datum, dataset) {
+ // Create token
+ if (_self.createToken( datum )) {
+ _self.$input.typeahead('val', '')
+ if (_self.$input.data( 'edit' )) {
+ _self.unedit(true)
+ }
+ }
+ })
+
+ // Listen to window resize
+ $(window).on('resize', $.proxy(this.update, this ))
+
+ }
+
+ , keydown: function (e) {
+
+ if (!this.focused) return
+
+ var _self = this
+
+ switch(e.keyCode) {
+ case 8: // backspace
+ if (!this.$input.is(document.activeElement)) break
+ this.lastInputValue = this.$input.val()
+ break
+
+ case 37: // left arrow
+ leftRight( this.textDirection === 'rtl' ? 'next': 'prev' )
+ break
+
+ case 38: // up arrow
+ upDown('prev')
+ break
+
+ case 39: // right arrow
+ leftRight( this.textDirection === 'rtl' ? 'prev': 'next' )
+ break
+
+ case 40: // down arrow
+ upDown('next')
+ break
+
+ case 65: // a (to handle ctrl + a)
+ if (this.$input.val().length > 0 || !(e.ctrlKey || e.metaKey)) break
+ this.activateAll()
+ e.preventDefault()
+ break
+
+ case 9: // tab
+ case 13: // enter
+
+ // We will handle creating tokens from autocomplete in autocomplete events
+ if (this.$input.data('ui-autocomplete') && this.$input.data('ui-autocomplete').menu.element.find("li:has(a.ui-state-focus), li.ui-state-focus").length) break
+
+ // We will handle creating tokens from typeahead in typeahead events
+ if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-cursor').length ) break
+ if (this.$input.hasClass('tt-input') && this.$wrapper.find('.tt-hint').val() && this.$wrapper.find('.tt-hint').val().length) break
+
+ // Create token
+ if (this.$input.is(document.activeElement) && this.$input.val().length || this.$input.data('edit')) {
+ return this.createTokensFromInput(e, this.$input.data('edit'));
+ }
+
+ // Edit token
+ if (e.keyCode === 13) {
+ if (!this.$copyHelper.is(document.activeElement) || this.$wrapper.find('.token.active').length !== 1) break
+ if (!_self.options.allowEditing) break
+ this.edit( this.$wrapper.find('.token.active') )
+ }
+ }
+
+ function leftRight(direction) {
+ if (_self.$input.is(document.activeElement)) {
+ if (_self.$input.val().length > 0) return
+
+ direction += 'All'
+ var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction]('.token:first') : _self.$input[direction]('.token:first')
+ if (!$token.length) return
+
+ _self.preventInputFocus = true
+ _self.preventDeactivation = true
+
+ _self.activate( $token )
+ e.preventDefault()
+
+ } else {
+ _self[direction]( e.shiftKey )
+ e.preventDefault()
+ }
+ }
+
+ function upDown(direction) {
+ if (!e.shiftKey) return
+
+ if (_self.$input.is(document.activeElement)) {
+ if (_self.$input.val().length > 0) return
+
+ var $token = _self.$input.hasClass('tt-input') ? _self.$input.parent()[direction + 'All']('.token:first') : _self.$input[direction + 'All']('.token:first')
+ if (!$token.length) return
+
+ _self.activate( $token )
+ }
+
+ var opposite = direction === 'prev' ? 'next' : 'prev'
+ , position = direction === 'prev' ? 'first' : 'last'
+
+ _self.$firstActiveToken[opposite + 'All']('.token').each(function() {
+ _self.deactivate( $(this) )
+ })
+
+ _self.activate( _self.$wrapper.find('.token:' + position), true, true )
+ e.preventDefault()
+ }
+
+ this.lastKeyDown = e.keyCode
+ }
+
+ , keypress: function(e) {
+
+ // Comma
+ if ($.inArray( e.which, this._triggerKeys) !== -1 && this.$input.is(document.activeElement)) {
+ if (this.$input.val()) {
+ this.createTokensFromInput(e)
+ }
+ return false;
+ }
+ }
+
+ , keyup: function (e) {
+ this.preventInputFocus = false
+
+ if (!this.focused) return
+
+ switch(e.keyCode) {
+ case 8: // backspace
+ if (this.$input.is(document.activeElement)) {
+ if (this.$input.val().length || this.lastInputValue.length && this.lastKeyDown === 8) break
+
+ this.preventDeactivation = true
+ var $prevToken = this.$input.hasClass('tt-input') ? this.$input.parent().prevAll('.token:first') : this.$input.prevAll('.token:first')
+
+ if (!$prevToken.length) break
+
+ this.activate( $prevToken )
+ } else {
+ this.remove(e)
+ }
+ break
+
+ case 46: // delete
+ this.remove(e, 'next')
+ break
+ }
+ this.lastKeyUp = e.keyCode
+ }
+
+ , focus: function (e) {
+ this.focused = true
+ this.$wrapper.addClass('focus')
+
+ if (this.$input.is(document.activeElement)) {
+ this.$wrapper.find('.active').removeClass('active')
+ this.$firstActiveToken = null
+
+ if (this.options.showAutocompleteOnFocus) {
+ this.search()
+ }
+ }
+ }
+
+ , blur: function (e) {
+
+ this.focused = false
+ this.$wrapper.removeClass('focus')
+
+ if (!this.preventDeactivation && !this.$element.is(document.activeElement)) {
+ this.$wrapper.find('.active').removeClass('active')
+ this.$firstActiveToken = null
+ }
+
+ if (!this.preventCreateTokens && (this.$input.data('edit') && !this.$input.is(document.activeElement) || this.options.createTokensOnBlur )) {
+ this.createTokensFromInput(e)
+ }
+
+ this.preventDeactivation = false
+ this.preventCreateTokens = false
+ }
+
+ , paste: function (e) {
+ var _self = this
+
+ // Add tokens to existing ones
+ if (_self.options.allowPasting) {
+ setTimeout(function () {
+ _self.createTokensFromInput(e)
+ }, 1)
+ }
+ }
+
+ , change: function (e) {
+ if ( e.initiator === 'tokenfield' ) return // Prevent loops
+
+ this.setTokens( this.$element.val() )
+ }
+
+ , createTokensFromInput: function (e, focus) {
+ if (this.$input.val().length < this.options.minLength)
+ return // No input, simply return
+
+ var tokensBefore = this.getTokensList()
+ this.setTokens( this.$input.val(), true )
+
+ if (tokensBefore == this.getTokensList() && this.$input.val().length)
+ return false // No tokens were added, do nothing (prevent form submit)
+
+ this.setInput('')
+
+ if (this.$input.data( 'edit' )) {
+ this.unedit(focus)
+ }
+
+ return false // Prevent form being submitted
+ }
+
+ , next: function (add) {
+ if (add) {
+ var $firstActiveToken = this.$wrapper.find('.active:first')
+ , deactivate = $firstActiveToken && this.$firstActiveToken ? $firstActiveToken.index() < this.$firstActiveToken.index() : false
+
+ if (deactivate) return this.deactivate( $firstActiveToken )
+ }
+
+ var $lastActiveToken = this.$wrapper.find('.active:last')
+ , $nextToken = $lastActiveToken.nextAll('.token:first')
+
+ if (!$nextToken.length) {
+ this.$input.focus()
+ return
+ }
+
+ this.activate($nextToken, add)
+ }
+
+ , prev: function (add) {
+
+ if (add) {
+ var $lastActiveToken = this.$wrapper.find('.active:last')
+ , deactivate = $lastActiveToken && this.$firstActiveToken ? $lastActiveToken.index() > this.$firstActiveToken.index() : false
+
+ if (deactivate) return this.deactivate( $lastActiveToken )
+ }
+
+ var $firstActiveToken = this.$wrapper.find('.active:first')
+ , $prevToken = $firstActiveToken.prevAll('.token:first')
+
+ if (!$prevToken.length) {
+ $prevToken = this.$wrapper.find('.token:first')
+ }
+
+ if (!$prevToken.length && !add) {
+ this.$input.focus()
+ return
+ }
+
+ this.activate( $prevToken, add )
+ }
+
+ , activate: function ($token, add, multi, remember) {
+
+ if (!$token) return
+
+ if (typeof remember === 'undefined') var remember = true
+
+ if (multi) var add = true
+
+ this.$copyHelper.focus()
+
+ if (!add) {
+ this.$wrapper.find('.active').removeClass('active')
+ if (remember) {
+ this.$firstActiveToken = $token
+ } else {
+ delete this.$firstActiveToken
+ }
+ }
+
+ if (multi && this.$firstActiveToken) {
+ // Determine first active token and the current tokens indicies
+ // Account for the 1 hidden textarea by subtracting 1 from both
+ var i = this.$firstActiveToken.index() - 2
+ , a = $token.index() - 2
+ , _self = this
+
+ this.$wrapper.find('.token').slice( Math.min(i, a) + 1, Math.max(i, a) ).each( function() {
+ _self.activate( $(this), true )
+ })
+ }
+
+ $token.addClass('active')
+ this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
+ }
+
+ , activateAll: function() {
+ var _self = this
+
+ this.$wrapper.find('.token').each( function (i) {
+ _self.activate($(this), i !== 0, false, false)
+ })
+ }
+
+ , deactivate: function($token) {
+ if (!$token) return
+
+ $token.removeClass('active')
+ this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
+ }
+
+ , toggle: function($token) {
+ if (!$token) return
+
+ $token.toggleClass('active')
+ this.$copyHelper.val( this.getTokensList( null, null, true ) ).select()
+ }
+
+ , edit: function ($token) {
+ if (!$token) return
+
+ var attrs = $token.data('attrs')
+
+ // Allow changing input value before editing
+ var options = { attrs: attrs, relatedTarget: $token.get(0) }
+ var editEvent = $.Event('tokenfield:edittoken', options)
+ this.$element.trigger( editEvent )
+
+ // Edit event can be cancelled if default is prevented
+ if (editEvent.isDefaultPrevented()) return
+
+ $token.find('.token-label').text(attrs.value)
+ var tokenWidth = $token.outerWidth()
+
+ var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
+
+ $token.replaceWith( $_input )
+
+ this.preventCreateTokens = true
+
+ this.$input.val( attrs.value )
+ .select()
+ .data( 'edit', true )
+ .width( tokenWidth )
+
+ this.update();
+
+ // Indicate that token is now being edited, and is replaced with an input field in the DOM
+ this.$element.trigger($.Event('tokenfield:editedtoken', options ))
+ }
+
+ , unedit: function (focus) {
+ var $_input = this.$input.hasClass('tt-input') ? this.$input.parent() : this.$input
+ $_input.appendTo( this.$wrapper )
+
+ this.$input.data('edit', false)
+ this.$mirror.text('')
+
+ this.update()
+
+ // Because moving the input element around in DOM
+ // will cause it to lose focus, we provide an option
+ // to re-focus the input after appending it to the wrapper
+ if (focus) {
+ var _self = this
+ setTimeout(function () {
+ _self.$input.focus()
+ }, 1)
+ }
+ }
+
+ , remove: function (e, direction) {
+ if (this.$input.is(document.activeElement) || this._disabled || this._readonly) return
+
+ var $token = (e.type === 'click') ? $(e.target).closest('.token') : this.$wrapper.find('.token.active')
+
+ if (e.type !== 'click') {
+ if (!direction) var direction = 'prev'
+ this[direction]()
+
+ // Was it the first token?
+ if (direction === 'prev') var firstToken = $token.first().prevAll('.token:first').length === 0
+ }
+
+ // Prepare events and their options
+ var options = { attrs: this.getTokenData( $token ), relatedTarget: $token.get(0) }
+ , removeEvent = $.Event('tokenfield:removetoken', options)
+
+ this.$element.trigger(removeEvent);
+
+ // Remove event can be intercepted and cancelled
+ if (removeEvent.isDefaultPrevented()) return
+
+ var removedEvent = $.Event('tokenfield:removedtoken', options)
+ , changeEvent = $.Event('change', { initiator: 'tokenfield' })
+
+ // Remove token from DOM
+ $token.remove()
+
+ // Trigger events
+ this.$element.val( this.getTokensList() ).trigger( removedEvent ).trigger( changeEvent )
+
+ // Focus, when necessary:
+ // When there are no more tokens, or if this was the first token
+ // and it was removed with backspace or it was clicked on
+ if (!this.$wrapper.find('.token').length || e.type === 'click' || firstToken) this.$input.focus()
+
+ // Adjust input width
+ this.$input.css('width', this.options.minWidth + 'px')
+ this.update()
+
+ // Cancel original event handlers
+ e.preventDefault()
+ e.stopPropagation()
+ }
+
+ /**
+ * Update tokenfield dimensions
+ */
+ , update: function (e) {
+ var value = this.$input.val()
+ , inputPaddingLeft = parseInt(this.$input.css('padding-left'), 10)
+ , inputPaddingRight = parseInt(this.$input.css('padding-right'), 10)
+ , inputPadding = inputPaddingLeft + inputPaddingRight
+
+ if (this.$input.data('edit')) {
+
+ if (!value) {
+ value = this.$input.prop("placeholder")
+ }
+ if (value === this.$mirror.text()) return
+
+ this.$mirror.text(value)
+
+ var mirrorWidth = this.$mirror.width() + 10;
+ if ( mirrorWidth > this.$wrapper.width() ) {
+ return this.$input.width( this.$wrapper.width() )
+ }
+
+ this.$input.width( mirrorWidth )
+ }
+ else {
+ //temporary reset width to minimal value to get proper results
+ this.$input.width(this.options.minWidth);
+
+ var w = (this.textDirection === 'rtl')
+ ? this.$input.offset().left + this.$input.outerWidth() - this.$wrapper.offset().left - parseInt(this.$wrapper.css('padding-left'), 10) - inputPadding - 1
+ : this.$wrapper.offset().left + this.$wrapper.width() + parseInt(this.$wrapper.css('padding-left'), 10) - this.$input.offset().left - inputPadding;
+ //
+ // some usecases pre-render widget before attaching to DOM,
+ // dimensions returned by jquery will be NaN -> we default to 100%
+ // so placeholder won't be cut off.
+ isNaN(w) ? this.$input.width('100%') : this.$input.width(w);
+ }
+ }
+
+ , focusInput: function (e) {
+ if ( $(e.target).closest('.token').length || $(e.target).closest('.token-input').length || $(e.target).closest('.tt-dropdown-menu').length ) return
+ // Focus only after the current call stack has cleared,
+ // otherwise has no effect.
+ // Reason: mousedown is too early - input will lose focus
+ // after mousedown. However, since the input may be moved
+ // in DOM, there may be no click or mouseup event triggered.
+ var _self = this
+ setTimeout(function() {
+ _self.$input.focus()
+ }, 0)
+ }
+
+ , search: function () {
+ if ( this.$input.data('ui-autocomplete') ) {
+ this.$input.autocomplete('search')
+ }
+ }
+
+ , disable: function () {
+ this.setProperty('disabled', true);
+ }
+
+ , enable: function () {
+ this.setProperty('disabled', false);
+ }
+
+ , readonly: function () {
+ this.setProperty('readonly', true);
+ }
+
+ , writeable: function () {
+ this.setProperty('readonly', false);
+ }
+
+ , setProperty: function(property, value) {
+ this['_' + property] = value;
+ this.$input.prop(property, value);
+ this.$element.prop(property, value);
+ this.$wrapper[ value ? 'addClass' : 'removeClass' ](property);
+ }
+
+ , destroy: function() {
+ // Set field value
+ this.$element.val( this.getTokensList() );
+ // Restore styles and properties
+ this.$element.css( this.$element.data('original-styles') );
+ this.$element.prop( 'tabindex', this.$element.data('original-tabindex') );
+
+ // Re-route tokenfield label to original input
+ var $label = $( 'label[for="' + this.$input.prop('id') + '"]' )
+ if ( $label.length ) {
+ $label.prop( 'for', this.$element.prop('id') )
+ }
+
+ // Move original element outside of tokenfield wrapper
+ this.$element.insertBefore( this.$wrapper );
+
+ // Remove tokenfield-related data
+ this.$element.removeData('original-styles')
+ .removeData('original-tabindex')
+ .removeData('bs.tokenfield');
+
+ // Remove tokenfield from DOM
+ this.$wrapper.remove();
+ this.$mirror.remove();
+
+ var $_element = this.$element;
+
+ return $_element;
+ }
+
+ }
+
+
+ /* TOKENFIELD PLUGIN DEFINITION
+ * ======================== */
+
+ var old = $.fn.tokenfield
+
+ $.fn.tokenfield = function (option, param) {
+ var value
+ , args = []
+
+ Array.prototype.push.apply( args, arguments );
+
+ var elements = this.each(function () {
+ var $this = $(this)
+ , data = $this.data('bs.tokenfield')
+ , options = typeof option == 'object' && option
+
+ if (typeof option === 'string' && data && data[option]) {
+ args.shift()
+ value = data[option].apply(data, args)
+ } else {
+ if (!data && typeof option !== 'string' && !param) {
+ $this.data('bs.tokenfield', (data = new Tokenfield(this, options)))
+ $this.trigger('tokenfield:initialize')
+ }
+ }
+ })
+
+ return typeof value !== 'undefined' ? value : elements;
+ }
+
+ $.fn.tokenfield.defaults = {
+ minWidth: 60,
+ minLength: 0,
+ html: true,
+ allowEditing: true,
+ allowPasting: true,
+ limit: 0,
+ autocomplete: {},
+ typeahead: {},
+ showAutocompleteOnFocus: false,
+ createTokensOnBlur: false,
+ delimiter: ',',
+ beautify: true,
+ inputType: 'text'
+ }
+
+ $.fn.tokenfield.Constructor = Tokenfield
+
+
+ /* TOKENFIELD NO CONFLICT
+ * ================== */
+
+ $.fn.tokenfield.noConflict = function () {
+ $.fn.tokenfield = old
+ return this
+ }
+
+ return Tokenfield;
+
+}));
diff --git a/static/js/handlebars/LICENSE b/static/js/handlebars/LICENSE
new file mode 100644
index 00000000..b802d14e
--- /dev/null
+++ b/static/js/handlebars/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2011-2017 by Yehuda Katz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/static/js/handlebars.js b/static/js/handlebars/handlebars.js
similarity index 100%
rename from static/js/handlebars.js
rename to static/js/handlebars/handlebars.js
diff --git a/static/js/konami/LICENSE.md b/static/js/konami/LICENSE.md
new file mode 100644
index 00000000..ee46fc71
--- /dev/null
+++ b/static/js/konami/LICENSE.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2017 Snaptortoise
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/static/js/konami/konami.js b/static/js/konami/konami.js
new file mode 100644
index 00000000..0fcaab2c
--- /dev/null
+++ b/static/js/konami/konami.js
@@ -0,0 +1,139 @@
+/*
+ * Konami-JS ~
+ * :: Now with support for touch events and multiple instances for
+ * :: those situations that call for multiple easter eggs!
+ * Code: https://github.com/snaptortoise/konami-js
+ * Examples: http://www.snaptortoise.com/konami-js
+ * Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
+ * Version: 1.5.1 (9/4/2017)
+ * Licensed under the MIT License (http://opensource.org/licenses/MIT)
+ * Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android
+ */
+
+var Konami = function (callback) {
+ var konami = {
+ addEvent: function (obj, type, fn, ref_obj) {
+ if (obj.addEventListener)
+ obj.addEventListener(type, fn, false);
+ else if (obj.attachEvent) {
+ // IE
+ obj["e" + type + fn] = fn;
+ obj[type + fn] = function () {
+ obj["e" + type + fn](window.event, ref_obj);
+ }
+ obj.attachEvent("on" + type, obj[type + fn]);
+ }
+ },
+ removeEvent: function (obj, eventName, eventCallback) {
+ if (obj.removeEventListener) {
+ obj.removeEventListener(eventName, eventCallback);
+ } else if (obj.attachEvent) {
+ obj.detachEvent(eventName);
+ }
+ },
+ input: "",
+ pattern: "38384040373937396665",
+ keydownHandler: function (e, ref_obj) {
+ if (ref_obj) {
+ konami = ref_obj;
+ } // IE
+ konami.input += e ? e.keyCode : event.keyCode;
+ if (konami.input.length > konami.pattern.length) {
+ konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
+ }
+ if (konami.input === konami.pattern) {
+ konami.code(this._currentlink);
+ konami.input = '';
+ e.preventDefault();
+ return false;
+ }
+ },
+ load: function (link) {
+ this.addEvent(document, "keydown", this.keydownHandler, this);
+ this.iphone.load(link);
+ },
+ unload: function () {
+ this.removeEvent(document, 'keydown', this.keydownHandler);
+ this.iphone.unload();
+ },
+ code: function (link) {
+ window.location = link
+ },
+ iphone: {
+ start_x: 0,
+ start_y: 0,
+ stop_x: 0,
+ stop_y: 0,
+ tap: false,
+ capture: false,
+ orig_keys: "",
+ keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP"],
+ input: [],
+ code: function (link) {
+ konami.code(link);
+ },
+ touchmoveHandler: function (e) {
+ if (e.touches.length === 1 && konami.iphone.capture === true) {
+ var touch = e.touches[0];
+ konami.iphone.stop_x = touch.pageX;
+ konami.iphone.stop_y = touch.pageY;
+ konami.iphone.tap = false;
+ konami.iphone.capture = false;
+ konami.iphone.check_direction();
+ }
+ },
+ toucheendHandler: function () {
+ if (konami.iphone.tap === true) {
+ konami.iphone.check_direction(this._currentLink);
+ }
+ },
+ touchstartHandler: function (e) {
+ konami.iphone.start_x = e.changedTouches[0].pageX;
+ konami.iphone.start_y = e.changedTouches[0].pageY;
+ konami.iphone.tap = true;
+ konami.iphone.capture = true;
+ },
+ load: function (link) {
+ this.orig_keys = this.keys;
+ konami.addEvent(document, "touchmove", this.touchmoveHandler);
+ konami.addEvent(document, "touchend", this.toucheendHandler, false);
+ konami.addEvent(document, "touchstart", this.touchstartHandler);
+ },
+ unload: function () {
+ konami.removeEvent(document, 'touchmove', this.touchmoveHandler);
+ konami.removeEvent(document, 'touchend', this.toucheendHandler);
+ konami.removeEvent(document, 'touchstart', this.touchstartHandler);
+ },
+ check_direction: function () {
+ x_magnitude = Math.abs(this.start_x - this.stop_x);
+ y_magnitude = Math.abs(this.start_y - this.stop_y);
+ x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
+ y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
+ result = (x_magnitude > y_magnitude) ? x : y;
+ result = (this.tap === true) ? "TAP" : result;
+ return result;
+ }
+ }
+ }
+
+ typeof callback === "string" && konami.load(callback);
+ if (typeof callback === "function") {
+ konami.code = callback;
+ konami.load();
+ }
+
+ return konami;
+};
+
+
+if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
+ module.exports = Konami;
+} else {
+ if (typeof define === 'function' && define.amd) {
+ define([], function() {
+ return Konami;
+ });
+ } else {
+ window.Konami = Konami;
+ }
+}
diff --git a/static/js/sapphire.js b/static/js/sapphire.js
new file mode 100644
index 00000000..db65fdd1
--- /dev/null
+++ b/static/js/sapphire.js
@@ -0,0 +1,316 @@
+// 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.
+
+// General options
+//=====================================
+// Times the canvas is refreshed a second
+var FPS = 30;
+// Determine the length of the trail (0=instant disappear, maximum=window.innerHeight=no disappear)
+var TRAIL_TIME = 5;
+// The color of the characters
+var RAIN_COLOR = "#00F";
+// The characters displayed
+var CHARACTERS = "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑".split("");
+// The font size used to display the characters
+var FONT_SIZE = 10;
+// The maximum number of characters displayed by column
+var MAX_CHAR = 7;
+
+var Sapphire = function () {
+ var sapphire = {
+ triggerHandle: undefined,
+ activated: false,
+ runOnce: false,
+
+ getClass: function(elt, main, name) { elt.obj = main.getElementsByClassName(name); },
+ getTag: function(elt, main, name) { elt.obj = main.getElementsByTagName(name); },
+
+ getProp: function(elt) {
+ for (var i=0 ; i sapphire.canvas.height && Math.random() > 0.975)
+ sapphire.drops[i][j] = 0;
+ sapphire.drops[i][j]++;
+ }
+ }
+ }
+
+ function drawEverything() {
+ attenuateBackground();
+ drawMatrixRainDrop();
+ }
+
+ sapphire.resize();
+ window.addEventListener('resize', sapphire.resize);
+ sapphire.triggerHandle = setInterval(drawEverything, 1000/FPS);
+ },
+
+ stop: function() {
+ window.removeEventListener('resize', sapphire.resize);
+ clearInterval(sapphire.triggerHandle);
+ sapphire.canvas.parentNode.removeChild(sapphire.canvas);
+ },
+
+ alterElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].alter(main); } },
+ revertElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].revert(main); } },
+
+ activate: function() {
+ if (!sapphire.runOnce) {
+ sapphire.runOnce = true;
+ sapphire.init();
+ }
+ if (!sapphire.activated) {
+ sapphire.activated = true;
+ sapphire.alterElts();
+ sapphire.run()
+ }
+ else {
+ sapphire.activated = false;
+ sapphire.stop();
+ sapphire.revertElts();
+ }
+ }
+ }
+
+ return sapphire;
+}
+
+var s = Sapphire();
+Konami(s.activate);
+
diff --git a/static/js/typeahead/LICENSE b/static/js/typeahead/LICENSE
new file mode 100644
index 00000000..83817bac
--- /dev/null
+++ b/static/js/typeahead/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2013-2014 Twitter, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/static/js/typeahead.js b/static/js/typeahead/typeahead.js
similarity index 100%
rename from static/js/typeahead.js
rename to static/js/typeahead/typeahead.js
diff --git a/static/logo/etherpad.png b/static/logo/etherpad.png
deleted file mode 100644
index 4dde5bf3..00000000
Binary files a/static/logo/etherpad.png and /dev/null differ
diff --git a/static/logo/federez.png b/static/logo/federez.png
deleted file mode 100644
index 439de178..00000000
Binary files a/static/logo/federez.png and /dev/null differ
diff --git a/static/logo/gitlab.png b/static/logo/gitlab.png
deleted file mode 100644
index b5040adc..00000000
Binary files a/static/logo/gitlab.png and /dev/null differ
diff --git a/static/logo/kanboard.png b/static/logo/kanboard.png
deleted file mode 100644
index 5c13c937..00000000
Binary files a/static/logo/kanboard.png and /dev/null differ
diff --git a/static/logo/wiki.png b/static/logo/wiki.png
deleted file mode 100644
index c4437bb0..00000000
Binary files a/static/logo/wiki.png and /dev/null differ
diff --git a/static/logo/zerobin.png b/static/logo/zerobin.png
deleted file mode 100644
index becbe150..00000000
Binary files a/static/logo/zerobin.png and /dev/null differ
diff --git a/static_files/.static b/static_files/.static
deleted file mode 100644
index e69de29b..00000000
diff --git a/templates/base.html b/templates/base.html
index bd1a4bb1..24d9c624 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -33,16 +33,23 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{# Load CSS and JavaScript #}
{% bootstrap_css %}
+
+ {% comment %} {% endcomment %}
{% bootstrap_javascript %}
-
-
+
+
+
+
+
+ {% comment %}{% endcomment %}
{{ site_name }} : {% block title %}Accueil{% endblock %}
+ {% include "cookie_banner.html" %}
@@ -66,10 +73,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %}