8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-12-24 16:03:47 +00:00
re2o/preferences/models.py

690 lines
22 KiB
Python
Raw Normal View History

# -*- mode: python; coding: utf-8 -*-
# Re2o 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.
"""
Reglages généraux, machines, utilisateurs, mail, general pour l'application.
"""
from __future__ import unicode_literals
from django.utils.functional import cached_property
from django.utils import timezone
from django.db import models
2018-04-14 18:55:24 +00:00
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
from django.forms import ValidationError
from django.utils.translation import ugettext_lazy as _
2018-07-30 15:00:41 +00:00
2018-04-14 18:55:24 +00:00
import machines.models
2018-03-28 15:39:23 +00:00
from re2o.mixins import AclMixin
from re2o.aes_field import AESEncryptedField
from datetime import timedelta
2018-04-13 23:42:22 +00:00
2018-01-31 19:43:36 +00:00
class PreferencesModel(models.Model):
2018-04-14 18:55:24 +00:00
""" Base object for the Preferences objects
Defines methods to handle the cache of the settings (they should
not change a lot) """
2018-01-31 19:43:36 +00:00
@classmethod
def set_in_cache(cls):
2018-04-14 18:55:24 +00:00
""" Save the preferences in a server-side cache """
2018-01-31 19:43:36 +00:00
instance, _created = cls.objects.get_or_create()
cache.set(cls().__class__.__name__.lower(), instance, None)
return instance
@classmethod
def get_cached_value(cls, key):
2018-04-14 18:55:24 +00:00
""" Get the preferences from the server-side cache """
2018-01-31 19:43:36 +00:00
instance = cache.get(cls().__class__.__name__.lower())
2018-04-13 23:42:22 +00:00
if instance is None:
instance = cls.set_in_cache()
2018-01-31 19:43:36 +00:00
return getattr(instance, key)
class Meta:
abstract = True
2018-03-28 15:39:23 +00:00
class OptionalUser(AclMixin, PreferencesModel):
"""Options pour l'user : obligation ou nom du telephone,
activation ou non du solde, autorisation du negatif, fingerprint etc"""
is_tel_mandatory = models.BooleanField(default=True)
gpg_fingerprint = models.BooleanField(default=True)
all_can_create_club = models.BooleanField(
2017-11-21 04:47:05 +00:00
default=False,
2018-08-05 16:48:35 +00:00
help_text=_("Users can create a club")
)
all_can_create_adherent = models.BooleanField(
default=False,
2018-08-05 16:48:35 +00:00
help_text=_("Users can create a member"),
2017-11-21 04:47:05 +00:00
)
2018-03-24 20:19:52 +00:00
shell_default = models.OneToOneField(
'users.ListShell',
on_delete=models.PROTECT,
blank=True,
null=True
)
2018-08-13 17:36:57 +00:00
self_change_shell = models.BooleanField(
default=False,
2018-08-05 16:48:35 +00:00
help_text=_("Users can edit their shell")
2018-08-13 17:36:57 +00:00
)
self_change_room = models.BooleanField(
default=False,
help_text=_("Users can edit their room")
)
2018-07-30 15:00:41 +00:00
local_email_accounts_enabled = models.BooleanField(
2018-06-29 16:20:25 +00:00
default=False,
2018-08-05 16:48:35 +00:00
help_text=_("Enable local email accounts for users")
2018-06-29 16:20:25 +00:00
)
2018-07-30 15:00:41 +00:00
local_email_domain = models.CharField(
2018-08-05 16:48:35 +00:00
max_length=32,
default="@example.org",
help_text=_("Domain to use for local email accounts")
)
2018-08-01 11:06:25 +00:00
max_email_address = models.IntegerField(
2018-08-05 16:48:35 +00:00
default=15,
help_text=_("Maximum number of local email addresses for a standard"
" user")
2018-06-30 12:46:35 +00:00
)
delete_notyetactive = models.IntegerField(
default=15,
help_text=_("Inactive users will be deleted after this number of days")
)
self_adhesion = models.BooleanField(
default=False,
help_text=_("A new user can create their account on Re2o")
)
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_optionaluser", _("Can view the user options")),
)
2018-08-05 16:48:35 +00:00
verbose_name = _("user options")
def clean(self):
2018-07-29 17:13:09 +00:00
"""Clean model:
Check the mail_extension
"""
2018-07-30 15:00:41 +00:00
if self.local_email_domain[0] != "@":
2018-08-05 16:48:35 +00:00
raise ValidationError(_("Email domain must begin with @"))
2018-07-30 15:00:41 +00:00
@receiver(post_save, sender=OptionalUser)
2018-04-15 01:00:05 +00:00
def optionaluser_post_save(**kwargs):
"""Ecriture dans le cache"""
user_pref = kwargs['instance']
user_pref.set_in_cache()
2018-03-28 15:39:23 +00:00
class OptionalMachine(AclMixin, PreferencesModel):
"""Options pour les machines : maximum de machines ou d'alias par user
sans droit, activation de l'ipv6"""
SLAAC = 'SLAAC'
DHCPV6 = 'DHCPV6'
DISABLED = 'DISABLED'
CHOICE_IPV6 = (
2018-08-05 16:48:35 +00:00
(SLAAC, _("Autoconfiguration by RA")),
(DHCPV6, _("IP addresses assigning by DHCPv6")),
(DISABLED, _("Disabled")),
)
password_machine = models.BooleanField(default=False)
max_lambdauser_interfaces = models.IntegerField(default=10)
max_lambdauser_aliases = models.IntegerField(default=10)
ipv6_mode = models.CharField(
max_length=32,
choices=CHOICE_IPV6,
default='DISABLED'
)
create_machine = models.BooleanField(
2018-08-05 16:48:35 +00:00
default=True
)
@cached_property
def ipv6(self):
2018-04-14 18:55:24 +00:00
""" Check if the IPv6 option is activated """
2018-04-13 23:42:22 +00:00
return not self.get_cached_value('ipv6_mode') == 'DISABLED'
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_optionalmachine", _("Can view the machine options")),
)
2018-08-05 16:48:35 +00:00
verbose_name = _("machine options")
@receiver(post_save, sender=OptionalMachine)
2018-04-15 01:00:05 +00:00
def optionalmachine_post_save(**kwargs):
"""Synchronisation ipv6 et ecriture dans le cache"""
machine_pref = kwargs['instance']
machine_pref.set_in_cache()
if machine_pref.ipv6_mode != "DISABLED":
for interface in machines.models.Interface.objects.all():
interface.sync_ipv6()
2018-03-28 15:39:23 +00:00
class OptionalTopologie(AclMixin, PreferencesModel):
"""Reglages pour la topologie : mode d'accès radius, vlan où placer
les machines en accept ou reject"""
MACHINE = 'MACHINE'
DEFINED = 'DEFINED'
CHOICE_RADIUS = (
2018-08-05 16:48:35 +00:00
(MACHINE, _("On the IP range's VLAN of the machine")),
(DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")),
)
2018-07-12 15:33:26 +00:00
CHOICE_PROVISION = (
('sftp', 'sftp'),
('tftp', 'tftp'),
)
2018-07-10 22:16:35 +00:00
switchs_web_management = models.BooleanField(
default=False,
help_text="Web management, activé si provision automatique"
)
switchs_web_management_ssl = models.BooleanField(
default=False,
help_text="Web management ssl. Assurez-vous que un certif est installé sur le switch !"
)
2018-07-10 22:16:35 +00:00
switchs_rest_management = models.BooleanField(
default=False,
help_text="Rest management, activé si provision auto"
)
switchs_ip_type = models.OneToOneField(
'machines.IpType',
on_delete=models.PROTECT,
blank=True,
null=True,
help_text="Plage d'ip de management des switchs"
)
2018-07-12 15:33:26 +00:00
switchs_provision = models.CharField(
max_length=32,
choices=CHOICE_PROVISION,
default='tftp',
help_text="Mode de récupération des confs par les switchs"
)
sftp_login = models.CharField(
max_length=32,
null=True,
blank=True,
help_text="Login sftp des switchs"
)
sftp_pass = AESEncryptedField(
max_length=63,
null=True,
blank=True,
help_text="Mot de passe sftp"
)
2018-07-10 22:16:35 +00:00
@cached_property
def provisioned_switchs(self):
"""Liste des switches provisionnés"""
2018-07-10 22:16:35 +00:00
from topologie.models import Switch
2018-10-27 21:31:12 +00:00
return Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name')
2017-09-10 16:16:59 +00:00
@cached_property
def switchs_management_interface(self):
"""Return the ip of the interface that the switch have to contact to get it's config"""
if self.switchs_ip_type:
from machines.models import Role, Interface
return Interface.objects.filter(machine__interface__in=Role.interface_for_roletype("switch-conf-server")).filter(type__ip_type=self.switchs_ip_type).first()
else:
return None
@cached_property
def switchs_management_interface_ip(self):
"""Same, but return the ipv4"""
if not self.switchs_management_interface:
return None
return self.switchs_management_interface.ipv4
2018-07-12 15:33:26 +00:00
@cached_property
def switchs_management_sftp_creds(self):
"""Credentials des switchs pour provion sftp"""
if self.sftp_login and self.sftp_pass:
return {'login' : self.sftp_login, 'pass' : self.sftp_pass}
else:
return None
@cached_property
def switchs_management_utils(self):
"""Used for switch_conf, return a list of ip on vlans"""
from machines.models import Role, Ipv6List, Interface
def return_ips_dict(interfaces):
return {'ipv4' : [str(interface.ipv4) for interface in interfaces], 'ipv6' : Ipv6List.objects.filter(interface__in=interfaces).values_list('ipv6', flat=True)}
ntp_servers = Role.all_interfaces_for_roletype("ntp-server").filter(type__ip_type=self.switchs_ip_type)
log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type)
radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type)
dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server")
subnet = None
subnet6 = None
if self.switchs_ip_type:
subnet = self.switchs_ip_type.ip_set_full_info
subnet6 = self.switchs_ip_type.ip6_set_full_info
return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'subnet': subnet, 'subnet6': subnet6}
@cached_property
def provision_switchs_enabled(self):
"""Return true if all settings are ok : switchs on automatic provision,
ip_type"""
2018-07-12 15:33:26 +00:00
return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds))
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_optionaltopologie", _("Can view the topology options")),
)
2018-08-05 16:48:35 +00:00
verbose_name = _("topology options")
@receiver(post_save, sender=OptionalTopologie)
2018-04-15 01:00:05 +00:00
def optionaltopologie_post_save(**kwargs):
"""Ecriture dans le cache"""
topologie_pref = kwargs['instance']
topologie_pref.set_in_cache()
class RadiusKey(AclMixin, models.Model):
"""Class of a radius key"""
radius_key = AESEncryptedField(
max_length=255,
help_text="Clef radius"
)
comment = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Commentaire de cette clef"
)
default_switch = models.BooleanField(
default=True,
unique=True,
help_text= "Clef par défaut des switchs"
)
class Meta:
permissions = (
("view_radiuskey", "Peut voir un objet radiuskey"),
)
2018-07-10 23:40:06 +00:00
def __str__(self):
return "Clef radius " + str(self.id) + " " + str(self.comment)
class SwitchManagementCred(AclMixin, models.Model):
"""Class of a management creds of a switch, for rest management"""
management_id = models.CharField(
max_length=63,
help_text="Login du switch"
)
management_pass = AESEncryptedField(
max_length=63,
help_text="Mot de passe"
)
default_switch = models.BooleanField(
default=True,
unique=True,
help_text= "Creds par défaut des switchs"
)
class Meta:
permissions = (
("view_switchmanagementcred", "Peut voir un objet switchmanagementcred"),
)
def __str__(self):
return "Identifiant " + str(self.management_id)
class Reminder(AclMixin, models.Model):
"""Options pour les mails de notification de fin d'adhésion.
Days: liste des nombres de jours pour lesquells un mail est envoyé
optionalMessage: message additionel pour le mail
"""
PRETTY_NAME="Options pour le mail de fin d'adhésion"
days = models.IntegerField(
default=7,
unique=True,
help_text="Délais entre le mail et la fin d'adhésion"
)
message = models.CharField(
max_length=255,
default="",
null=True,
blank=True,
help_text="Message affiché spécifiquement pour ce rappel"
)
class Meta:
permissions = (
("view_reminder", "Peut voir un objet reminder"),
)
def users_to_remind(self):
from re2o.utils import all_has_access
date = timezone.now().replace(minute=0,hour=0)
futur_date = date + timedelta(days=self.days)
users = all_has_access(futur_date).exclude(pk__in = all_has_access(futur_date + timedelta(days=1)))
return users
2018-03-28 15:39:23 +00:00
class GeneralOption(AclMixin, PreferencesModel):
"""Options générales : nombre de resultats par page, nom du site,
temps les liens sont valides"""
general_message_fr = models.TextField(
default="",
blank=True,
2018-08-05 16:48:35 +00:00
help_text=_("General message displayed on the French version of the"
" website (e.g. in case of maintenance)")
)
general_message_en = models.TextField(
default="",
blank=True,
2018-08-05 16:48:35 +00:00
help_text=_("General message displayed on the English version of the"
" website (e.g. in case of maintenance)")
)
search_display_page = models.IntegerField(default=15)
pagination_number = models.IntegerField(default=25)
pagination_large_number = models.IntegerField(default=8)
req_expire_hrs = models.IntegerField(default=48)
site_name = models.CharField(max_length=32, default="Re2o")
email_from = models.EmailField(default="www-data@example.com")
2018-11-15 13:35:26 +00:00
main_site_url = models.URLField(max_length=255, default="http://re2o.example.org")
2018-01-14 22:47:44 +00:00
GTU_sum_up = models.TextField(
default="",
blank=True,
)
GTU = models.FileField(
2018-04-13 23:42:22 +00:00
upload_to='',
2018-01-14 22:47:44 +00:00
default="",
null=True,
blank=True,
)
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_generaloption", _("Can view the general options")),
)
2018-08-05 16:48:35 +00:00
verbose_name = _("general options")
@receiver(post_save, sender=GeneralOption)
2018-04-15 01:00:05 +00:00
def generaloption_post_save(**kwargs):
"""Ecriture dans le cache"""
general_pref = kwargs['instance']
general_pref.set_in_cache()
2018-03-28 15:39:23 +00:00
class Service(AclMixin, 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)
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_service", _("Can view the service options")),
)
2018-08-05 16:48:35 +00:00
verbose_name = _("service")
verbose_name_plural =_("services")
def __str__(self):
return str(self.name)
class MailContact(AclMixin, models.Model):
"""Contact email adress with a commentary."""
address = models.EmailField(
default = "contact@example.org",
2018-08-05 16:48:35 +00:00
help_text = _("Contact email address")
)
commentary = models.CharField(
blank = True,
null = True,
help_text = _(
2018-08-05 16:48:35 +00:00
"Description of the associated email address."),
max_length = 256
)
2018-07-01 08:49:47 +00:00
@cached_property
def get_name(self):
return self.address.split("@")[0]
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_mailcontact", _("Can view a contact email address object")),
)
2018-08-05 16:48:35 +00:00
verbose_name = _("contact email address")
verbose_name_plural = _("contact email addresses")
def __str__(self):
return(self.address)
2018-03-28 15:39:23 +00:00
class AssoOption(AclMixin, PreferencesModel):
"""Options générales de l'asso : siret, addresse, nom, etc"""
name = models.CharField(
2018-08-05 16:48:35 +00:00
default=_("Networking organisation school Something"),
max_length=256
)
siret = models.CharField(default="00000000000000", max_length=32)
2018-08-05 16:48:35 +00:00
adresse1 = models.CharField(default=_("Threadneedle Street"), max_length=128)
adresse2 = models.CharField(default=_("London EC2R 8AH"), max_length=128)
contact = models.EmailField(default="contact@example.org")
telephone = models.CharField(max_length=15, default="0000000000")
2018-08-05 16:48:35 +00:00
pseudo = models.CharField(default=_("Organisation"), max_length=32)
utilisateur_asso = models.OneToOneField(
'users.User',
on_delete=models.PROTECT,
blank=True,
null=True
)
2018-03-18 01:14:45 +00:00
description = models.TextField(
null=True,
blank=True,
)
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_assooption", _("Can view the organisation options")),
)
2018-08-05 16:48:35 +00:00
verbose_name = _("organisation options")
@receiver(post_save, sender=AssoOption)
2018-04-15 01:00:05 +00:00
def assooption_post_save(**kwargs):
"""Ecriture dans le cache"""
asso_pref = kwargs['instance']
asso_pref.set_in_cache()
2018-04-16 16:22:51 +00:00
class HomeOption(AclMixin, PreferencesModel):
"""Settings of the home page (facebook/twitter etc)"""
2018-04-16 03:28:27 +00:00
facebook_url = models.URLField(
null=True,
2018-08-05 16:48:35 +00:00
blank=True
2018-04-16 03:28:27 +00:00
)
twitter_url = models.URLField(
null=True,
2018-08-05 16:48:35 +00:00
blank=True
2018-04-16 03:28:27 +00:00
)
twitter_account_name = models.CharField(
max_length=32,
null=True,
2018-08-05 16:48:35 +00:00
blank=True
2018-04-16 03:28:27 +00:00
)
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_homeoption", _("Can view the homepage options")),
2018-04-16 03:28:27 +00:00
)
2018-08-05 16:48:35 +00:00
verbose_name = _("homepage options")
2018-04-16 03:28:27 +00:00
2018-04-16 16:22:51 +00:00
@receiver(post_save, sender=HomeOption)
def homeoption_post_save(**kwargs):
2018-04-16 03:28:27 +00:00
"""Ecriture dans le cache"""
2018-04-16 16:22:51 +00:00
home_pref = kwargs['instance']
home_pref.set_in_cache()
2018-04-16 03:28:27 +00:00
2018-03-28 15:39:23 +00:00
class MailMessageOption(AclMixin, models.Model):
"""Reglages, mail de bienvenue et autre"""
2018-07-10 22:16:35 +00:00
welcome_mail_fr = models.TextField(default="", help_text="Mail de bienvenue en français")
welcome_mail_en = models.TextField(default="", help_text="Mail de bienvenue en anglais")
class Meta:
permissions = (
2018-08-05 16:48:35 +00:00
("view_mailmessageoption", _("Can view the email message"
" options")),
)
2018-08-05 16:48:35 +00:00
verbose_name = _("email message options")
2018-09-02 14:53:21 +00:00
class RadiusOption(AclMixin, PreferencesModel):
2018-09-02 14:53:21 +00:00
class Meta:
verbose_name = _("radius policies")
MACHINE = 'MACHINE'
DEFINED = 'DEFINED'
CHOICE_RADIUS = (
(MACHINE, _("On the IP range's VLAN of the machine")),
(DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")),
)
2018-12-02 16:03:27 +00:00
REJECT = 'REJECT'
SET_VLAN = 'SET_VLAN'
CHOICE_POLICY = (
(REJECT, _('Reject the machine')),
(SET_VLAN, _('Place the machine on the VLAN'))
)
2018-09-02 14:53:21 +00:00
radius_general_policy = models.CharField(
max_length=32,
choices=CHOICE_RADIUS,
default='DEFINED'
)
2018-12-02 16:03:27 +00:00
unknown_machine = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
2018-09-02 14:53:21 +00:00
verbose_name=_("Policy for unknown machines"),
)
2018-12-02 16:03:27 +00:00
unknown_machine_vlan = models.ForeignKey(
'machines.Vlan',
2018-09-02 14:53:21 +00:00
on_delete=models.PROTECT,
2018-12-02 16:03:27 +00:00
related_name='unknown_machine_vlan',
blank=True,
null=True,
verbose_name=_('Unknown machine Vlan'),
help_text=_(
'Vlan for unknown machines if not rejected.'
)
)
unknown_port = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
verbose_name=_("Policy for unknown port"),
2018-09-02 14:53:21 +00:00
)
2018-12-02 16:03:27 +00:00
unknown_port_vlan = models.ForeignKey(
'machines.Vlan',
2018-09-02 14:53:21 +00:00
on_delete=models.PROTECT,
2018-12-02 16:03:27 +00:00
related_name='unknown_port_vlan',
blank=True,
null=True,
verbose_name=_('Unknown port Vlan'),
help_text=_(
'Vlan for unknown ports if not rejected.'
)
)
unknown_room = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
2018-09-02 14:53:21 +00:00
verbose_name=_(
"Policy for machine connecting from "
"unregistered room (relevant on ports with STRICT "
"radius mode)"
),
)
2018-12-02 16:03:27 +00:00
unknown_room_vlan = models.ForeignKey(
'machines.Vlan',
related_name='unknown_room_vlan',
2018-09-02 14:53:21 +00:00
on_delete=models.PROTECT,
2018-12-02 16:03:27 +00:00
blank=True,
null=True,
verbose_name=_('Unknown room Vlan'),
help_text=_(
'Vlan for unknown room if not rejected.'
)
)
non_member = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
2018-09-02 14:53:21 +00:00
verbose_name=_("Policy non member users."),
)
2018-12-02 16:03:27 +00:00
non_member_vlan = models.ForeignKey(
'machines.Vlan',
related_name='non_member_vlan',
2018-09-02 14:53:21 +00:00
on_delete=models.PROTECT,
2018-12-02 16:03:27 +00:00
blank=True,
null=True,
verbose_name=_('Non member Vlan'),
help_text=_(
'Vlan for non members if not rejected.'
)
)
banned = models.CharField(
max_length=32,
choices=CHOICE_POLICY,
default=REJECT,
2018-09-02 14:53:21 +00:00
verbose_name=_("Policy for banned users."),
2018-12-02 16:03:27 +00:00
)
banned_vlan = models.ForeignKey(
'machines.Vlan',
related_name='banned_vlan',
on_delete=models.PROTECT,
blank=True,
null=True,
verbose_name=_('Banned Vlan'),
help_text=_(
'Vlan for banned if not rejected.'
)
2018-09-02 14:53:21 +00:00
)
vlan_decision_ok = models.OneToOneField(
'machines.Vlan',
on_delete=models.PROTECT,
related_name='vlan_ok_option',
blank=True,
null=True
)