# -*- mode: python; coding: utf-8 -*-
# Re2o un logiciel d'administration développé initiallement au Rézo Metz. Il
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017  Gabriel Détraz
# Copyright © 2017  Lara 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.
"""
Models defining the preferences for users, machines, emails, general settings
etc.
"""
from __future__ import unicode_literals
import os

from django.utils.functional import cached_property
from django.utils import timezone
from django.db import models
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 _

import machines.models

from re2o.mixins import AclMixin, RevMixin
from re2o.aes_field import AESEncryptedField

from datetime import timedelta


class PreferencesModel(models.Model):
    """Base object for the Preferences objects.

    Defines methods to handle the cache of the settings (they should not change
    a lot).
    """

    @classmethod
    def set_in_cache(cls):
        """Save the preferences in a server-side cache."""
        instance = cls.objects.first()
        if not instance:
            instance, _created = cls.objects.get_or_create()
        cache.set(cls().__class__.__name__.lower(), instance, None)
        return instance

    @classmethod
    def get_cached_value(cls, key):
        """Get the preferences from the server-side cache."""
        instance = cache.get(cls().__class__.__name__.lower())
        if instance is None:
            instance = cls.set_in_cache()
        return getattr(instance, key)

    class Meta:
        abstract = True


class OptionalUser(AclMixin, PreferencesModel):
    """User preferences: telephone number requirement, user balance activation,
    creation of users by everyone etc.

    Attributes:
        is_tel_mandatory: whether indicating a telephone number is mandatory.
        gpg_fingerprint: whether GPG fingerprints are enabled.
        all_can_create_club: whether all users can create a club.
        all_can_create_adherent: whether all users can create a member.
        shell_default: the default shell for users connecting to machines
            managed by the organisation.
        self_change_shell: whether users can edit their shell.
        self_change_pseudo: whether users can edit their pseudo (username).
        self_room_policy: whether users can edit the policy of their room.
        local_email_accounts_enabled: whether local email accounts are enabled.
        local_email_domain: the domain used for local email accounts.
        max_email_address: the maximum number of local email addresses allowed
            for a standard user.
        delete_notyetactive: the number of days before deleting not yet active
            users.
        disable_emailnotyetconfirmed: the number of days before disabling users
            with not yet verified email address.
        self_adhesion: whether users can create their account themselves.
        all_users_active: whether newly created users are active.
        allow_set_password_during_user_creation: whether users can set their
            password directly when creating their account.
        allow_archived_connexion: whether archived users can connect on the web
            interface.
    """

    DISABLED = "DISABLED"
    ONLY_INACTIVE = "ONLY_INACTIVE"
    ALL_ROOM = "ALL_ROOM"
    ROOM_POLICY = (
        (DISABLED, _("Users can't select their room")),
        (
            ONLY_INACTIVE,
            _(
                "Users can only select a room occupied by a user with a disabled connection."
            ),
        ),
        (ALL_ROOM, _("Users can select all rooms")),
    )

    is_tel_mandatory = models.BooleanField(default=True)
    gpg_fingerprint = models.BooleanField(default=True)
    all_can_create_club = models.BooleanField(
        default=False, help_text=_("Users can create a club.")
    )
    all_can_create_adherent = models.BooleanField(
        default=False, help_text=_("Users can create a member.")
    )
    shell_default = models.OneToOneField(
        "users.ListShell", on_delete=models.PROTECT, blank=True, null=True
    )
    self_change_shell = models.BooleanField(
        default=False, help_text=_("Users can edit their shell.")
    )
    self_change_pseudo = models.BooleanField(
        default=True, help_text=_("Users can edit their pseudo.")
    )
    self_room_policy = models.CharField(
        max_length=32,
        choices=ROOM_POLICY,
        default="DISABLED",
        help_text=_("Policy on self users room edition"),
    )
    local_email_accounts_enabled = models.BooleanField(
        default=False, help_text=_("Enable local email accounts for users.")
    )
    local_email_domain = models.CharField(
        max_length=32,
        default="@example.org",
        help_text=_("Domain to use for local email accounts."),
    )
    max_email_address = models.IntegerField(
        default=15,
        help_text=_("Maximum number of local email addresses for a standard user."),
    )
    delete_notyetactive = models.IntegerField(
        default=15,
        help_text=_("Not yet active users will be deleted after this number of days."),
    )
    disable_emailnotyetconfirmed = models.IntegerField(
        default=2,
        help_text=_(
            "Users with an email address not yet confirmed will be disabled after this number of days."
        ),
    )
    self_adhesion = models.BooleanField(
        default=False, help_text=_("A new user can create their account on Re2o.")
    )
    all_users_active = models.BooleanField(
        default=False,
        help_text=_(
            "If True, all new created and connected users are active."
            " If False, only when a valid registration has been paid."
        ),
    )
    allow_set_password_during_user_creation = models.BooleanField(
        default=False,
        help_text=_(
            "If True, users have the choice to receive an email containing"
            " a link to reset their password during creation, or to directly"
            " set their password in the page."
            " If False, an email is always sent."
        ),
    )
    allow_archived_connexion = models.BooleanField(
        default=False, help_text=_("If True, archived users are allowed to connect.")
    )

    class Meta:
        permissions = (("view_optionaluser", _("Can view the user preferences")),)
        verbose_name = _("user preferences")

    def clean(self):
        """Check the email extension."""
        if self.local_email_domain[0] != "@":
            raise ValidationError(_("Email domain must begin with @."))


@receiver(post_save, sender=OptionalUser)
def optionaluser_post_save(**kwargs):
    """Write in the cache."""
    user_pref = kwargs["instance"]
    user_pref.set_in_cache()


class OptionalMachine(AclMixin, PreferencesModel):
    """Machines preferences: maximum number of machines per user, IPv6
    activation etc.

    Attributes:
        password_machine: whether password per machine is enabled.
        max_lambdauser_interfaces: the maximum number of interfaces allowed for
            a standard user.
        max_lambdauser_aliases: the maximum number of aliases allowed for a
            standard user.
        ipv6_mode: whether IPv6 mode is enabled.
        create_machine: whether creation of machine is enabled.
        default_dns_ttl: the default TTL for CNAME, A and AAAA records.
    """

    SLAAC = "SLAAC"
    DHCPV6 = "DHCPV6"
    DISABLED = "DISABLED"
    CHOICE_IPV6 = (
        (SLAAC, _("Automatic configuration by RA")),
        (DHCPV6, _("IP addresses assignment 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(default=True)
    default_dns_ttl = models.PositiveIntegerField(
        verbose_name=_("default Time To Live (TTL) for CNAME, A and AAAA records"),
        default=172800,  # 2 days
    )

    @cached_property
    def ipv6(self):
        """Check if the IPv6 mode is enabled."""
        return not self.get_cached_value("ipv6_mode") == "DISABLED"

    class Meta:
        permissions = (("view_optionalmachine", _("Can view the machine preferences")),)
        verbose_name = _("machine preferences")


@receiver(post_save, sender=OptionalMachine)
def optionalmachine_post_save(**kwargs):
    """Synchronise IPv6 mode and write in the 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()


class OptionalTopologie(AclMixin, PreferencesModel):
    """Configuration of switches: automatic provision, RADIUS mode, default
    VLANs etc.

    Attributes:
        switchs_web_management: whether web management for automatic provision
            is enabled.
        switchs_web_management_ssl: whether SSL web management is required.
        switchs_rest_management: whether REST management for automatic
            provision is enabled.
        switchs_ip_type: the IP range for the management of switches.
        switchs_provision: the provision mode for switches to get their
            configuration.
        sftp_login: the SFTP login for switches.
        sftp_pass: the SFTP password for switches.
    """

    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"')),
    )
    CHOICE_PROVISION = (("sftp", "SFTP"), ("tftp", "TFTP"))

    switchs_web_management = models.BooleanField(
        default=False,
        help_text=_("Web management, activated in case of automatic provision."),
    )
    switchs_web_management_ssl = models.BooleanField(
        default=False,
        help_text=_(
            "SSL web management, make sure that a certificate is"
            " installed on the switch."
        ),
    )
    switchs_rest_management = models.BooleanField(
        default=False,
        help_text=_("REST management, activated in case of automatic provision."),
    )
    switchs_ip_type = models.OneToOneField(
        "machines.IpType",
        on_delete=models.PROTECT,
        blank=True,
        null=True,
        help_text=_("IP range for the management of switches."),
    )
    switchs_provision = models.CharField(
        max_length=32,
        choices=CHOICE_PROVISION,
        default="tftp",
        help_text=_("Provision of configuration mode for switches."),
    )
    sftp_login = models.CharField(
        max_length=32, null=True, blank=True, help_text=_("SFTP login for switches.")
    )
    sftp_pass = AESEncryptedField(
        max_length=63, null=True, blank=True, help_text=_("SFTP password.")
    )

    @cached_property
    def provisioned_switchs(self):
        """Get the list of provisioned switches."""
        from topologie.models import Switch

        return Switch.objects.filter(automatic_provision=True).order_by(
            "interface__domain__name"
        )

    @cached_property
    def switchs_management_interface(self):
        """Get the interface that the switch has to contact to get its
        configuration.
        """
        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(machine_type__ip_type=self.switchs_ip_type)
                .first()
            )
        else:
            return None

    @cached_property
    def switchs_management_interface_ip(self):
        """Get the IPv4 address of the interface that the switch has to contact
        to get its configuration.
        """
        if not self.switchs_management_interface:
            return None
        return self.switchs_management_interface.ipv4

    @cached_property
    def switchs_management_sftp_creds(self):
        """Get the switch credentials for SFTP provisioning."""
        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):
        """Get the dictionary of IP addresses for the configuration of
        switches.
        """
        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).filter(active=True).values_list(
                    "ipv6", flat=True
                ),
            }

        ntp_servers = Role.all_interfaces_for_roletype("ntp-server").filter(
            machine_type__ip_type=self.switchs_ip_type
        )
        log_servers = Role.all_interfaces_for_roletype("log-server").filter(
            machine_type__ip_type=self.switchs_ip_type
        )
        radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(
            machine_type__ip_type=self.switchs_ip_type
        )
        dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server")
        dns_recursive_servers = Role.all_interfaces_for_roletype(
            "dns-recursive-server"
        ).filter(machine_type__ip_type=self.switchs_ip_type)
        subnet = None
        subnet6 = None
        if self.switchs_ip_type:
            subnet = (
                self.switchs_ip_type.ip_net_full_info
                or self.switchs_ip_type.ip_set_full_info[0]
            )
            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),
            "dns_recursive_servers": return_ips_dict(dns_recursive_servers),
            "subnet": subnet,
            "subnet6": subnet6,
        }

    @cached_property
    def provision_switchs_enabled(self):
        """Check if all automatic provisioning settings are OK."""
        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 = (
            ("view_optionaltopologie", _("Can view the topology preferences")),
        )
        verbose_name = _("topology preferences")


@receiver(post_save, sender=OptionalTopologie)
def optionaltopologie_post_save(**kwargs):
    """Write in the cache."""
    topologie_pref = kwargs["instance"]
    topologie_pref.set_in_cache()


class RadiusKey(AclMixin, models.Model):
    """Class of a RADIUS key.

    Attributes:
        radius_key: the encrypted RADIUS key.
        comment: a comment related to the key.
        default_switch: bool, True if the key is to be used by default on
            switches and False otherwise.
    """

    radius_key = AESEncryptedField(max_length=255, help_text=_("RADIUS key."))
    comment = models.CharField(
        max_length=255, null=True, blank=True, help_text=_("Comment for this key.")
    )
    default_switch = models.BooleanField(
        default=False, help_text=_("Default key for switches.")
    )

    class Meta:
        permissions = (("view_radiuskey", _("Can view a RADIUS key object")),)
        verbose_name = _("RADIUS key")
        verbose_name_plural = _("RADIUS keys")

    def clean(self):
        """Check if there is a unique default RADIUS key."""
        if RadiusKey.objects.filter(default_switch=True).count() > 1:
            raise ValidationError(_("Default RADIUS key for switches already exists."))

    def __str__(self):
        return _("RADIUS key ") + str(self.id) + " " + str(self.comment)


class SwitchManagementCred(AclMixin, models.Model):
    """Class of a switch management credentials, for rest management.

    Attributes:
        management_id: the login used to connect to switches.
        management_pass: the encrypted password used to connect to switches.
        default_switch: bool, True if the credentials are to be used by default
            on switches and False otherwise.
    """

    management_id = models.CharField(max_length=63, help_text=_("Switch login."))
    management_pass = AESEncryptedField(max_length=63, help_text=_("Password."))
    default_switch = models.BooleanField(
        default=True, unique=True, help_text=_("Default credentials for switches.")
    )

    class Meta:
        permissions = (
            (
                "view_switchmanagementcred",
                _("Can view a switch management credentials object"),
            ),
        )
        verbose_name = _("switch management credentials")

    def __str__(self):
        return _("Switch login ") + str(self.management_id)


class Reminder(AclMixin, models.Model):
    """Reminder of membership's end preferences: email messages, number of days
    before sending emails.

    Attributes:
        days: the number of days before the membership's end to send the
            reminder.
        message: the content of the reminder.
    """

    days = models.IntegerField(
        default=7,
        unique=True,
        help_text=_("Delay between the email and the membership's end."),
    )
    message = models.TextField(
        default="",
        null=True,
        blank=True,
        help_text=_("Message displayed specifically for this reminder."),
    )

    class Meta:
        permissions = (("view_reminder", _("Can view a reminder object")),)
        verbose_name = _("reminder")
        verbose_name_plural = _("reminders")

    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


class GeneralOption(AclMixin, PreferencesModel):
    """General preferences: number of search results per page, website name
    etc.

    Attributes:
        general_message_fr: general message displayed on the French version of
            the website (e.g. in case of maintenance).
        general_message_en: general message displayed on the English version of
            the website (e.g. in case of maintenance).
        search_display_page: number of results displayed (in each category)
            when searching.
        pagination_number: number of items per page (standard size).
        pagination_large_number: number of items per page (large size).
        req_expire_hrs: number of hours before expiration of the reset password
            link.
        site_name: website name.
        email_from: email address for automatic emailing.
        main_site_url: main site URL.
        GTU_sum_up: summary of the General Terms of Use.
        GTU: file, General Terms of Use.
    """

    general_message_fr = models.TextField(
        default="",
        blank=True,
        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,
        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")
    main_site_url = models.URLField(max_length=255, default="http://re2o.example.org")
    GTU_sum_up = models.TextField(default="", blank=True)
    GTU = models.FileField(upload_to="", default="", null=True, blank=True)

    class Meta:
        permissions = (("view_generaloption", _("Can view the general preferences")),)
        verbose_name = _("general preferences")


@receiver(post_save, sender=GeneralOption)
def generaloption_post_save(**kwargs):
    """Write in the cache."""
    general_pref = kwargs["instance"]
    general_pref.set_in_cache()


class Service(AclMixin, models.Model):
    """Service displayed on the home page.

    Attributes:
        name: the name of the service.
        url: the URL of the service.
        description: the description of the service.
        image: an image to illustrate the service (e.g. logo).
    """

    name = models.CharField(max_length=32)
    url = models.URLField()
    description = models.TextField()
    image = models.ImageField(upload_to="logo", blank=True)

    class Meta:
        permissions = (("view_service", _("Can view the service preferences")),)
        verbose_name = _("service")
        verbose_name_plural = _("services")

    def __str__(self):
        return str(self.name)


class MailContact(AclMixin, models.Model):
    """Contact email address with a comment.

    Attributes:
        address: the contact email address.
        commentary: a comment used to describe the contact email address.
    """

    address = models.EmailField(
        default="contact@example.org", help_text=_("Contact email address.")
    )

    commentary = models.CharField(
        blank=True,
        null=True,
        help_text=_("Description of the associated email address."),
        max_length=256,
    )

    @cached_property
    def get_name(self):
        return self.address.split("@")[0]

    class Meta:
        permissions = (
            ("view_mailcontact", _("Can view a contact email address object")),
        )
        verbose_name = _("contact email address")
        verbose_name_plural = _("contact email addresses")

    def __str__(self):
        return self.address


class Mandate(RevMixin, AclMixin, models.Model):
    """Mandate, documenting who was the president of the organisation at a
    given time.

    Attributes:
        president: User, the president during the mandate.
        start_date: datetime, the date when the mandate started.
        end_date: datetime, the date when the mandate ended.
    """

    class Meta:
        verbose_name = _("mandate")
        verbose_name_plural = _("mandates")
        permissions = (("view_mandate", _("Can view a mandate object")),)

    president = models.ForeignKey(
        "users.User",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        verbose_name=_("president of the association"),
        help_text=_("Displayed on subscription vouchers."),
    )
    start_date = models.DateTimeField(verbose_name=_("start date"))
    end_date = models.DateTimeField(verbose_name=_("end date"), blank=True, null=True)

    @classmethod
    def get_mandate(cls, date=timezone.now):
        """"Get the mandate taking place at the given date.

        Args:
            date: the date used to find the mandate (default: timezone.now).

        Returns:
            The mandate related to the given date.
        """
        if callable(date):
            date = date()
        mandate = (
            cls.objects.exclude(end_date__lte=date).order_by("start_date").first()
            or cls.objects.order_by("start_date").last()
        )
        if not mandate:
            raise cls.DoesNotExist(
                _(
                    "No mandates have been created. Please go to the preferences page to create one."
                )
            )
        return mandate

    def is_over(self):
        return self.end_date is None

    def __str__(self):
        return str(self.president) + " " + str(self.start_date.year)


class AssoOption(AclMixin, PreferencesModel):
    """Information about the organisation: name, address, SIRET number etc.

    Attributes:
        name: the name of the organisation.
        siret: the SIRET number of the organisation.
        adresse1: the first line of the organisation's address, e.g. street and
            number.
        adresse2: the second line of the organisation's address, e.g. city and
            postal code.
        contact: contact email address.
        telephone: contact telephone number.
        pseudo: short name of the organisation.
        utilisateur_asso: the user used to manage the organisation.
        description: the description of the organisation.
    """

    name = models.CharField(
        default=_("Networking organisation school Something"), max_length=256
    )
    siret = models.CharField(default="00000000000000", max_length=32)
    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")
    pseudo = models.CharField(default=_("Organisation"), max_length=32)
    utilisateur_asso = models.OneToOneField(
        "users.User", on_delete=models.PROTECT, blank=True, null=True
    )
    description = models.TextField(null=True, blank=True)

    class Meta:
        permissions = (("view_assooption", _("Can view the organisation preferences")),)
        verbose_name = _("organisation preferences")


@receiver(post_save, sender=AssoOption)
def assooption_post_save(**kwargs):
    """Write in the cache."""
    asso_pref = kwargs["instance"]
    asso_pref.set_in_cache()


class HomeOption(AclMixin, PreferencesModel):
    """Social networks displayed on the home page (supports only Facebook and
    Twitter).

    Attributes:
        facebook_url: URL of the Facebook account.
        twitter_url: URL of the Twitter account.
        twitter_account_name: name of the Twitter account.
    """

    facebook_url = models.URLField(null=True, blank=True)
    twitter_url = models.URLField(null=True, blank=True)
    twitter_account_name = models.CharField(max_length=32, null=True, blank=True)

    class Meta:
        permissions = (("view_homeoption", _("Can view the homepage preferences")),)
        verbose_name = _("homepage preferences")


@receiver(post_save, sender=HomeOption)
def homeoption_post_save(**kwargs):
    """Write in the cache."""
    home_pref = kwargs["instance"]
    home_pref.set_in_cache()


class MailMessageOption(AclMixin, models.Model):
    """Welcome email messages preferences.

    Attributes:
        welcome_mail_fr: the text of the welcome email in French.
        welcome_mail_en: the text of the welcome email in English.
    """

    welcome_mail_fr = models.TextField(
        default="", blank=True, help_text=_("Welcome email in French.")
    )
    welcome_mail_en = models.TextField(
        default="", blank=True, help_text=_("Welcome email in English.")
    )

    class Meta:
        permissions = (
            ("view_mailmessageoption", _("Can view the email message preferences")),
        )
        verbose_name = _("email message preferences")


class RadiusAttribute(RevMixin, AclMixin, models.Model):
    """RADIUS attributes preferences.

    Attributes:
        attribute: the name of the RADIUS attribute.
        value: the value of the RADIUS attribute.
        comment: the comment to document the attribute.
    """

    class Meta:
        verbose_name = _("RADIUS attribute")
        verbose_name_plural = _("RADIUS attributes")

    attribute = models.CharField(
        max_length=255,
        verbose_name=_("attribute"),
        help_text=_("See https://freeradius.org/rfc/attributes.html."),
    )
    value = models.CharField(max_length=255, verbose_name=_("value"))
    comment = models.TextField(
        verbose_name=_("comment"),
        help_text=_("Use this field to document this attribute."),
        blank=True,
        default="",
    )

    def __str__(self):
        return " ".join([self.attribute, "=", self.value])


class RadiusOption(AclMixin, PreferencesModel):
    """RADIUS preferences.

    Attributes:
        radius_general_policy: the general RADIUS policy (MACHINE or DEFINED).
        unknown_machine: the RADIUS policy for unknown machines.
        unknown_machine_vlan: the VLAN for unknown machines if not rejected.
        unknown_machine_attributes: the answer attributes for unknown machines.
        unknown_port: the RADIUS policy for unknown ports.
        unknown_port_vlan: the VLAN for unknown ports if not rejected;
        unknown_port_attributes: the answer attributes for unknown ports.
        unknown_room: the RADIUS policy for machines connecting from
            unregistered rooms (relevant for ports with STRICT RADIUS mode).
        unknown_room_vlan: the VLAN for unknown rooms if not rejected.
        unknown_room_attributes: the answer attributes for unknown rooms.
        non_member: the RADIUS policy for non members.
        non_member_vlan: the VLAN for non members if not rejected.
        non_member_attributes: the answer attributes for non members.
        banned: the RADIUS policy for banned users.
        banned_vlan: the VLAN for banned users if not rejected.
        banned_attributes: the answer attributes for banned users.
        vlan_decision_ok: the VLAN for accepted machines.
        ok_attributes: the answer attributes for accepted machines.
    """

    class Meta:
        verbose_name = _("RADIUS policy")
        verbose_name_plural = _("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"')),
    )
    REJECT = "REJECT"
    SET_VLAN = "SET_VLAN"
    CHOICE_POLICY = (
        (REJECT, _("Reject the machine")),
        (SET_VLAN, _("Place the machine on the VLAN")),
    )
    radius_general_policy = models.CharField(
        max_length=32, choices=CHOICE_RADIUS, default="DEFINED"
    )
    unknown_machine = models.CharField(
        max_length=32,
        choices=CHOICE_POLICY,
        default=REJECT,
        verbose_name=_("policy for unknown machines"),
    )
    unknown_machine_vlan = models.ForeignKey(
        "machines.Vlan",
        on_delete=models.PROTECT,
        related_name="unknown_machine_vlan",
        blank=True,
        null=True,
        verbose_name=_("unknown machines VLAN"),
        help_text=_("VLAN for unknown machines if not rejected."),
    )
    unknown_machine_attributes = models.ManyToManyField(
        RadiusAttribute,
        related_name="unknown_machine_attribute",
        blank=True,
        verbose_name=_("unknown machines attributes"),
        help_text=_("Answer attributes for unknown machines."),
    )
    unknown_port = models.CharField(
        max_length=32,
        choices=CHOICE_POLICY,
        default=REJECT,
        verbose_name=_("policy for unknown ports"),
    )
    unknown_port_vlan = models.ForeignKey(
        "machines.Vlan",
        on_delete=models.PROTECT,
        related_name="unknown_port_vlan",
        blank=True,
        null=True,
        verbose_name=_("unknown ports VLAN"),
        help_text=_("VLAN for unknown ports if not rejected."),
    )
    unknown_port_attributes = models.ManyToManyField(
        RadiusAttribute,
        related_name="unknown_port_attribute",
        blank=True,
        verbose_name=_("unknown ports attributes"),
        help_text=_("Answer attributes for unknown ports."),
    )
    unknown_room = models.CharField(
        max_length=32,
        choices=CHOICE_POLICY,
        default=REJECT,
        verbose_name=_(
            "Policy for machines connecting from unregistered rooms"
            " (relevant on ports with STRICT RADIUS mode)"
        ),
    )
    unknown_room_vlan = models.ForeignKey(
        "machines.Vlan",
        related_name="unknown_room_vlan",
        on_delete=models.PROTECT,
        blank=True,
        null=True,
        verbose_name=_("unknown rooms VLAN"),
        help_text=_("VLAN for unknown rooms if not rejected."),
    )
    unknown_room_attributes = models.ManyToManyField(
        RadiusAttribute,
        related_name="unknown_room_attribute",
        blank=True,
        verbose_name=_("unknown rooms attributes"),
        help_text=_("Answer attributes for unknown rooms."),
    )
    non_member = models.CharField(
        max_length=32,
        choices=CHOICE_POLICY,
        default=REJECT,
        verbose_name=_("policy for non members"),
    )
    non_member_vlan = models.ForeignKey(
        "machines.Vlan",
        related_name="non_member_vlan",
        on_delete=models.PROTECT,
        blank=True,
        null=True,
        verbose_name=_("non members VLAN"),
        help_text=_("VLAN for non members if not rejected."),
    )
    non_member_attributes = models.ManyToManyField(
        RadiusAttribute,
        related_name="non_member_attribute",
        blank=True,
        verbose_name=_("non members attributes"),
        help_text=_("Answer attributes for non members."),
    )
    banned = models.CharField(
        max_length=32,
        choices=CHOICE_POLICY,
        default=REJECT,
        verbose_name=_("policy for banned users"),
    )
    banned_vlan = models.ForeignKey(
        "machines.Vlan",
        related_name="banned_vlan",
        on_delete=models.PROTECT,
        blank=True,
        null=True,
        verbose_name=_("banned users VLAN"),
        help_text=_("VLAN for banned users if not rejected."),
    )
    banned_attributes = models.ManyToManyField(
        RadiusAttribute,
        related_name="banned_attribute",
        blank=True,
        verbose_name=_("banned users attributes"),
        help_text=_("Answer attributes for banned users."),
    )
    vlan_decision_ok = models.OneToOneField(
        "machines.Vlan",
        on_delete=models.PROTECT,
        related_name="vlan_ok_option",
        blank=True,
        null=True,
    )
    ok_attributes = models.ManyToManyField(
        RadiusAttribute,
        related_name="ok_attribute",
        blank=True,
        verbose_name=_("accepted users attributes"),
        help_text=_("Answer attributes for accepted users."),
    )

    @classmethod
    def get_attributes(cls, name, attribute_kwargs={}):
        return (
            (str(attribute.attribute), str(attribute.value % attribute_kwargs))
            for attribute in cls.get_cached_value(name).all()
        )


def default_invoice():
    tpl, _ = DocumentTemplate.objects.get_or_create(
        name="Re2o default invoice", template="templates/default_invoice.tex"
    )
    return tpl.id


def default_voucher():
    tpl, _ = DocumentTemplate.objects.get_or_create(
        name="Re2o default voucher", template="templates/default_voucher.tex"
    )
    return tpl.id


class CotisationsOption(AclMixin, PreferencesModel):
    """Subscription preferences.

    Attributes:
        invoice_template: the template for invoices.
        voucher_template: the template for vouchers.
        send_voucher_mail: whether the voucher is sent by email when the
            invoice is controlled.
    """

    class Meta:
        verbose_name = _("subscription preferences")

    invoice_template = models.OneToOneField(
        "preferences.DocumentTemplate",
        verbose_name=_("template for invoices"),
        related_name="invoice_template",
        on_delete=models.PROTECT,
        default=default_invoice,
    )
    voucher_template = models.OneToOneField(
        "preferences.DocumentTemplate",
        verbose_name=_("template for subscription vouchers"),
        related_name="voucher_template",
        on_delete=models.PROTECT,
        default=default_voucher,
    )
    send_voucher_mail = models.BooleanField(
        verbose_name=_("send voucher by email when the invoice is controlled"),
        help_text=_(
            "Be careful, if no mandate is defined on the preferences page,"
            " errors will be triggered when generating vouchers."
        ),
        default=False,
    )


class DocumentTemplate(RevMixin, AclMixin, models.Model):
    """Represent a template in order to create documents such as invoice or
    subscription voucher.

    Attributes:
        template: file, the template used to create documents.
        name: the name of the template.
    """

    template = models.FileField(upload_to="templates/", verbose_name=_("template"))
    name = models.CharField(max_length=125, verbose_name=_("name"), unique=True)

    class Meta:
        verbose_name = _("document template")
        verbose_name_plural = _("document templates")

    def __str__(self):
        return str(self.name)


@receiver(models.signals.post_delete, sender=DocumentTemplate)
def auto_delete_file_on_delete(sender, instance, **kwargs):
    """Delete the tempalte file from filesystem when the related
    DocumentTemplate object is deleted.
    """
    if instance.template:
        if os.path.isfile(instance.template.path):
            os.remove(instance.template.path)


@receiver(models.signals.pre_save, sender=DocumentTemplate)
def auto_delete_file_on_change(sender, instance, **kwargs):
    """Delete the previous file from filesystem when the related
    DocumentTemplate object is updated with new file.
    """
    if not instance.pk:
        return False

    try:
        old_file = DocumentTemplate.objects.get(pk=instance.pk).template
    except DocumentTemplate.DoesNotExist:
        return False

    new_file = instance.template
    if not old_file == new_file:
        if os.path.isfile(old_file.path):
            os.remove(old_file.path)