"""This module holds the models of the ``accounts`` app."""

import waffle

from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin
from django.contrib.auth.signals import user_logged_in
from django.core import validators
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.db import models
from django.dispatch import receiver
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _

from extended_choices import OrderedChoices

from django_hashable_file_fields.hashers import with_default
from django_hashable_file_fields.models import HashableFileFieldsBaseModel

from mtp.apps.assessments.models import CompetencyTree
from mtp.apps.broker_models_hooks.models import BrokerHookM2MRelatedMixin
from mtp.apps.common.limpyd_base.django_models import RedisBackedDjangoModel
from mtp.apps.common.utils import upload_to_generator
from mtp.apps.eventlog.helpers import event_log
from mtp.apps.eventlog.models import ACTION_CHOICES as EVENT_TYPES
from mtp.apps.likes.constants import LIKE_DEFAULT_WEIGHT
from mtp.apps.likes.utils import check_likeability
from mtp.apps.sync.models import Syncable
from mtp.apps.translation import model_fields as translation_fields

from .constants import (
    GROUPTYPE_JOB_PROFILE,
    INTERNAL_GROUP_TRAINEE,
    INTERNAL_GROUP_TRAINER,
    POTENTIAL_DUPLICATE_REASONS,
)
from .managers import (
    GroupManager,
    GroupRoleManager,
    GroupTypeManager,
    GroupTypeQuerySet,
    GroupQuerySet,
    MTPUserManager
)


class Avatar(HashableFileFieldsBaseModel, Syncable):
    """Store an avatar for a user"""

    img = models.ImageField(upload_to=upload_to_generator, verbose_name=_("Photo"))
    hash = models.CharField(max_length=36, blank=True)
    date = models.DateTimeField(auto_now_add=True)

    hashable_fields = [('img', 'hash', with_default(settings.EMPTY_FILE_HASH))]

    sync_files_to_master = True

    class Meta:
        ordering = ('date', )


class GroupSet(Syncable):
    name = models.CharField(max_length=200)
    mgroups = models.ManyToManyField('Group')

    def __str__(self):
        return self.name


class GroupType(Syncable):
    """This class allows to group `accounts.Group` objects.

    It will especially be used to create categorization axises for trainer
    groups. For example, a trainer may belong to a `CFR`-typed group, and to
    a `population`-typed group, who have similar functions but different
    semantics. Hence this model.

    """
    name = models.CharField(_('name'), max_length=255, unique=True)
    description = models.TextField(blank=True)
    is_stats_filter = models.BooleanField(default=False, help_text=_(
        "Should the groups of this type used as a stats filter?"))
    is_training_filter = models.BooleanField(default=False, help_text=_(
        "Should the groups of this type used as a training filter?"))

    objects = GroupTypeManager.from_queryset(GroupTypeQuerySet)()

    def __str__(self):
        return self.name


class Group(Syncable):
    name = models.CharField(max_length=100, verbose_name=_("name"))
    type = models.ForeignKey(
        'GroupType',
        null=True,
        blank=True,
        verbose_name=_("type"),
        on_delete=models.CASCADE
    )
    parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)
    color = models.CharField(max_length=30, blank=True)

    objects = GroupManager.from_queryset(GroupQuerySet)()

    class Meta:
        unique_together = ('name', 'type')

    def __str__(self):
        return "%s - %s" % (self.type, self.name)

    def save(self, *args, **kwargs):

        super().save(*args, **kwargs)

        # If the assessments system is active, we must create
        # a competency tree for each job profile.

        if (self.type and self.type.name == GROUPTYPE_JOB_PROFILE and
                waffle.switch_is_active('enable_assessments')):

            CompetencyTree.objects.get_or_create(
                job_profile=self,
                defaults={
                    'name': self.name
                },
            )

    @property
    def is_job_profile(self):
        return self.type.name == GROUPTYPE_JOB_PROFILE

    @property
    def manager_member(self):
        """Try to get the user manager of the current group.

        Returns
        -------
        ``MTPUser`` instance, or None

        Raises
        ------
        ``django.core.exceptions.MultipleObjectsReturned``
            If muliple ``MTPUser`` instances were returned.

        """
        try:
            return self.member_roles.get(role=GroupRole.ROLES.MANAGER).user
        except GroupRole.DoesNotExist:
            return None

    def set_manager(self, user):
        """Set the provided user as manager of this group.

        Parameters
        ----------
        user: MTPUser
            The user to define as manager of this group.

        """

        role, created = GroupRole.objects.get_or_create(
            group=self,
            role=GroupRole.ROLES.MANAGER,
            defaults={
                'user': user,
            }
        )

        # We delete the old role before creating the new one
        # to pass the validation in ``save`` method.
        if not created and role.user != user:
            role.delete()
            GroupRole.objects.create(group=self, role=GroupRole.ROLES.MANAGER, user=user)

    def natural_key(self):
        return (self.name,)

    def get_absolute_url(self):
        """ Get the absolute url of the group.

        It actually returns the edit url as there is no "read" view for the user

        Returns
        -------
        string
            The wanted url.

        """

        return self.get_edit_url()

    def get_edit_url(self):
        """ Get the url to edit the group

        Returns
        -------
        string
            The wanted url.

        """

        return reverse('accounts:group-update', kwargs={'group_uuid': self.pk})

    def get_delete_url(self):
        """ Get the url to soft-delete the group

        Returns
        -------
        string
            The wanted url.

        """

        return reverse('accounts:group-delete', kwargs={'group_uuid': self.pk})

    @classmethod
    def get_create_url(cls):
        """ Get the url create a new group

        As it is not tied to an instance, this method is a ``classmethod``.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('accounts:group-create')

    @classmethod
    def get_list_url(cls):
        """ Get the url to list the groups

        As it is not tied to an instance, this method is a ``classmethod``.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('accounts:group-list')


class GroupRole(Syncable):
    """Django model representing a role for a given ``MTPUser`` inside a given ``Group``.

    Attributes
    ----------
    ROLES : ``Choices`` instance
        It contains all available types of ``role``:
        - ``MEMBER`` : The user is a basic member of the group.
        - ``MANAGER`` : The user is a manager of the group.
    id : django field, instance of ``django_extensions.db.fields.UUIDField``
    user : foreign key to ``mtp.apps.accounts.models.MTPUser``
    group : foreign key to ``mtp.apps.accounts.models.Group``
    role : django ``CharField`` field
        The role of the user inside the group.

    Notes
    -----
    This model is ``Syncable``.

    """

    ROLES = OrderedChoices(
        ('MEMBER', 'me', _("Member")),
        ('MANAGER', 'ma', _("Manager")),
    )

    user = models.ForeignKey('MTPUser', related_name='group_roles', on_delete=models.CASCADE)
    group = models.ForeignKey('Group', related_name='member_roles', on_delete=models.CASCADE)
    role = models.CharField(
        _('role'),
        choices=ROLES,
        max_length=2,
        default=ROLES.MEMBER
    )

    objects = GroupRoleManager()

    class Meta:
        unique_together = ('user', 'group')

    def save(self, *args, **kwargs):
        """Enforce the unicity of given group's manager.

        We are doing so by checking if the linked ``Group`` instance
        already has a member with the ``MANAGER`` role.

        For the parameters, see ``django.db.models.Model

        """

        if self.role == self.ROLES.MANAGER and self.group.manager_member and (
                not self.group.manager_member == self.user):
            # A manager already exist in this group
            raise ValidationError('Only one manager is allowed by group.')

        super().save(*args, **kwargs)


class DuplicatedMTPUser(Syncable):
    """Django model representing a "duplicate" action for two given ``MTPUser``.

    Attributes
    ----------
    original_user : foreign key to ``mtp.apps.accounts.models.MTPUser``
        The original user (ie: imported on the CMS of the master instance)
    duplicate_user : foreign key to ``mtp.apps.accounts.models.MTPUser``
        The user flagged as a duplicate (ie: created on-the-fly on a slave instance)
    origin_user : foreign key to ``mtp.apps.accounts.models.MTPUser``
        The user who asked the creation of this duplicate
    date : django ``DateTime`` Field
        The creation date of this duplicate

    """

    original_user = models.ForeignKey(
        'MTPUser',
        related_name='duplicate_user_originals',
        on_delete=models.CASCADE
    )
    duplicate_user = models.ForeignKey(
        'MTPUser',
        related_name='duplicate_user_duplicates',
        on_delete=models.CASCADE
    )
    origin_user = models.ForeignKey(
        'MTPUser',
        related_name='duplicate_user_origins',
        on_delete=models.CASCADE
    )
    date = models.DateTimeField(default=timezone.now)

    def save(self, *args, **kwargs):
        """Ensure that creating the duplicate is permitted"""

        # The duplicate shouldn't be the same as the original
        if self.original_user == self.duplicate_user:
            raise ValidationError(
                "The original user and the duplicated one should be different users"
            )

        # The original shouldn't be already a duplicate
        originals_qs = DuplicatedMTPUser.objects.filter(
            duplicate_user=self.original_user
        )

        if self.pk:
            originals_qs = originals_qs.exclude(pk=self.pk)

        if originals_qs.exists():
            raise ValidationError(
                "The original user is already flagged as a duplicate"
            )

        # One shouldn't associate a duplicate with multiple originals
        duplicates_qs = DuplicatedMTPUser.objects.filter(
            duplicate_user=self.duplicate_user
        )

        if self.pk:
            duplicates_qs = duplicates_qs.exclude(pk=self.pk)

        if duplicates_qs.exists():
            raise ValidationError(
                "The duplicate user is already flagged as a duplicate for another user"
            )

        super().save(*args, **kwargs)


class MTPUser(BrokerHookM2MRelatedMixin,
              AbstractBaseUser,
              PermissionsMixin,
              RedisBackedDjangoModel,
              Syncable):
    """Django model representing a User"""

    USERNAME_LENGTH = 50

    ORIGINS = OrderedChoices(
        ('UNKNOWN', 'un', _('Unknown')),
        ('CMS_IMPORT', 'ci', _('Import on the CMS')),
        ('CMS_FORM', 'cf', _('Form on the CMS')),
        ('SLAVE_REGISTRATION', 'sr', _('Box registration')),
        ('MASTER_REGISTRATION', 'mr', _('Online registration')),
    )

    username = models.CharField(
        _('username'),
        max_length=USERNAME_LENGTH,
        unique=True,
        help_text=_('Required. 50 characters or fewer. Letters, digits and '
                    '@/./+/-/_ only.'),
        validators=[
            validators.RegexValidator(
                r'^[\w.@+-]+$',
                _('Enter a valid username. This value may contain only letters, numbers '
                  'and @/./+/-/_ characters.'), 'invalid'),
        ],
        error_messages={
            'unique': _("A user with that username already exists."),
        }
    )
    first_name = models.CharField(_('first name'), max_length=50, blank=True)
    last_name = models.CharField(_('last name'), max_length=50, blank=True)
    email = models.EmailField(_('email address'), blank=True)
    is_staff = models.BooleanField(_('staff status'), default=False, help_text=_(
        'Designates whether the user can log into this admin ' 'site.'))
    is_active = models.BooleanField(
        _('active'), default=True, help_text=_(
            'Designates whether this user should be treated as '
            'active. Unselect this instead of deleting accounts.'))
    date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
    created_from = models.CharField(
        verbose_name=_('Created from'),
        choices=ORIGINS,
        default=ORIGINS.UNKNOWN,
        max_length=2,
    )

    mgroups = models.ManyToManyField(
        'Group',
        related_name='members',
        verbose_name=_("MTP Groups"),
        through='GroupRole'
    )

    photo = models.ForeignKey(
        'Avatar',
        related_name='profile_by_photo',
        null=True,
        blank=True,
        on_delete=models.CASCADE
    )
    photos = models.ManyToManyField('Avatar', related_name='profile_by_photos', blank=True)

    natural_language = translation_fields.LanguageField(
        verbose_name=_('natural language'),
        default=settings.DEFAULT_NATURAL_LANGUAGE,
        help_text=_('Your natural language.'))
    business = models.CharField(max_length=255, blank=True)
    # french for "direction regionale".... # LEGACY ALERT
    dr = models.CharField(max_length=255, blank=True)  # pylint: disable=invalid-name

    last_changed_password = models.DateTimeField(default=timezone.now)

    external_id = models.CharField(
        max_length=255,
        blank=True,
        verbose_name=_('External id')
    )

    old_username = models.CharField(max_length=USERNAME_LENGTH, blank=True)

    duplicates = models.ManyToManyField(
        'MTPUser',
        symmetrical=False,
        through=DuplicatedMTPUser,
        through_fields=('original_user', 'duplicate_user', 'origin_user',),
        related_name='original_users'
    )
    has_potential_duplicates = models.BooleanField(
        default=False,
        verbose_name=_('Do this user have some potential duplicates ?')
    )

    objects = MTPUserManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        """
        Returns the first_name plus the last_name, with a space in between.
        """
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        "Returns the short name for the user."
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        """
        Sends an email to this User.
        """
        send_mail(subject, message, from_email, [self.email], **kwargs)

    @cached_property
    def redis_model(self):
        """Returns the ``DjangoBackedRedisModel`` linked to the current one."""

        from .redis_models import MTPUser as RedisMTPUser

        return RedisMTPUser

    @cached_property
    def managed_groups(self):
        """Get the groups managed by this user

        Returns
        -------
        ``mtp.apps.accounts.managers.GroupQuerySet`` instance
            A django ``QuerySet`` with the groups managed by this user

        """

        return self.mgroups.managed_by(user=self)

    @cached_property
    def original_user(self):
        """Get the original of this user

        Returns
        -------
        MTPUser or None
            MTPUser if this user a duplicate,
            None otherwise

        Raises
        ------
        MultipleObjectsReturned
            Something went wrong, the user was associated with multiple originals.

        """

        try:
            return self.duplicate_user_duplicates.first().original_user
        except AttributeError:
            return None

    @cached_property
    def duplicate_users(self):
        """Get the duplicates from this user

        Returns
        -------
        list of MTPUser
            A list containing the duplicates of this user

        """

        return self.duplicates.all()

    @cached_property
    def original_or_self_external_id(self):
        """Get the external_id if this user's original if any or this user's external_id

        Notes
        -----
        The priority goes to the original_user

        Returns
        -------
        string
            The external_id, can be an empty string

        """

        return self.original_user.external_id if self.original_user else self.external_id

    @cached_property
    def potential_duplicates(self):
        """Get the "potential duplicates" for this user

        The potential duplicate ids are computed and stored in the tied redis model.

        Returns
        -------
        dict
            A dict with the users as the keys and the reasons of why this user has been flagged
            as a duplicate as the value.

        """

        return {
            MTPUser.objects.get(id=user_id): POTENTIAL_DUPLICATE_REASONS.for_value(reason)
            for (user_id, reason) in self.redis_instance.potential_duplicates_ids.hgetall().items()
        }

    def flag_user_as_potential_duplicate(self, user, reason):
        """Flag the given user as a potential duplicate for the current user

        Parameters
        ----------
        user: MTPUser
            The user we want to flag as a potential duplicate
        reason: One of the constants in ``mtp.apps.accounts.constants.PotentialDuplicateReasons``
            The reason we want to flag the user as a potential duplicate

        """

        # If the user is not already in the potential duplicate list OR in the duplicates list
        if not self.redis_instance.potential_duplicates_ids.hexists(user.id)\
                and user not in self.duplicate_users:
            self.redis_instance.flag_user_as_potential_duplicate(
                user,
                reason
            )

        # Finally, flag the current user for having a potential duplicate
        if not self.has_potential_duplicates:
            self.has_potential_duplicates = True
            self.save()

    def unflag_user_as_potential_duplicate(self, user):
        """Unflag the given user as a potential duplicate for the current user

        Parameters
        ----------
        user: MTPUser
            The user we want to remove as the potential duplicate of the current user

        """

        if self.redis_instance.potential_duplicates_ids.hexists(user.id):
            self.redis_instance.remove_user_as_potential_duplicate(user)

        # If the user have no more potential / confirmed duplicates, unflag him
        if not self.potential_duplicates and not self.duplicate_users:
            self.has_potential_duplicates = False
            self.save()

    def _get_job_profile(self):
        """Get the job profile of this ``MTPUser``.

        Returns
        -------
        ``Group`` instance or ``None``
            The user's job profile.

        Notes
        -----
        This method is better used indirectly by calling the ``job_profile``
        property below.

        """
        try:
            return self.mgroups.get(type__name=GROUPTYPE_JOB_PROFILE)
        except (Group.DoesNotExist, Group.MultipleObjectsReturned):
            return None

    def _set_job_profile(self, group):
        """Assign a job profile to this ``MTPUser``.

        Parameters
        ----------
        group : instance of ``mtp.apps.accounts.models.Group``
            The job profile to assign to the user.

        Raises
        ------
        ValueError
            If the group that was passed is not actually a job profile.

        Notes
        -----
        This method enforces the unicity of the user's job profile.
        It is better used indirectly by calling the ``job_profile``
        property below.

        """
        if not group.is_job_profile:
            raise ValueError("The specified Group is not a Job Profile.")
        current_job_profiles = self.mgroups.filter(type__name=GROUPTYPE_JOB_PROFILE)
        try:
            GroupRole.objects.get(group__in=current_job_profiles, user=self).delete()
        except (GroupRole.DoesNotExist, GroupRole.MultipleObjectsReturned):
            # The user don't have a job_profile yet
            pass

        GroupRole.objects.create(group=group, user=self)

    def flag_user_as_duplicate(self, duplicate_user, origin_user):
        """Flag a given user as a "duplicate" of the current user

        Parameters
        ----------
        duplicate_user: MTPUser
            The user we want to flag as a "duplicate" of the current user
        origin_user: MTPUser
            The user who asked the duplicate

        """

        # Add the user to duplicate list
        DuplicatedMTPUser.objects.create(
            original_user=self,
            duplicate_user=duplicate_user,
            origin_user=origin_user
        )

        # Remove him from the potential duplicates list
        if self.redis_instance.potential_duplicates_ids.hexists(duplicate_user.id):
            self.redis_instance.remove_user_as_potential_duplicate(duplicate_user)

    def get_serialized_representation(self):
        """Get the serialized representation of this instance

        Returns
        -------
        dict

        """

        # Avoid circular imports
        from .apiv2.serializers import MTPUserSerializer

        return MTPUserSerializer(self).data

    def get_trainingsession_broker_topic(self, training_session):
        """Get the broker topic to publish when this user is added/removed into a training session

        Parameters
        ----------
        training_session: TrainingSession
            The training session the user was added/removed to

        Returns
        -------
        str

        """

        return '%s/users' % training_session.pk

    def get_absolute_url(self):
        """ Get the absolute url of the user.

        It actually returns the edit url as there is no "ready" view for the user

        Returns
        -------
        string
            The wanted url.

        """

        return self.get_edit_url()

    def get_edit_url(self):
        """ Get the url to edit the user

        Returns
        -------
        string
            The wanted url.

        """

        return reverse('accounts:user-update', kwargs={'user_uuid': self.pk})

    def get_delete_url(self):
        """ Get the url to soft-delete the user

        Returns
        -------
        string
            The wanted url.

        """

        return reverse('accounts:user-delete', kwargs={'user_uuid': self.pk})

    def get_undelete_url(self):
        """ Get the url to un-soft-delete the user

        Returns
        -------
        string
            The wanted url.

        """

        return reverse('accounts:user-undelete', kwargs={'user_uuid': self.pk})

    def get_welcome_url(self):
        """ Get the url to send the welcome mail to the user

        Returns
        -------
        string
            The wanted url.

        """

        return reverse('accounts:user-welcome', kwargs={'user_uuid': self.pk})

    @classmethod
    def get_list_url(cls):
        """ Get the url to list the user

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('accounts:user-list')

    @classmethod
    def get_create_url(cls, trainer=False):
        """ Get the url to create a new user

        Returns
        -------
        string
            The wanted url.

        """
        if trainer:
            return "%s?trainer=true" % reverse('accounts:user-create')
        else:
            return reverse('accounts:user-create')

    # This property lets us get and set this user's job profile as if it was
    # an actual field.
    job_profile = property(_get_job_profile, _set_job_profile)

    def save(self, *args, **kwargs):

        is_new = False if self.pk else True

        super(MTPUser, self).save(*args, **kwargs)

        # If the favorite per default Waffle flag is active, we add the
        # selected trainings to the user's favorites
        if waffle.switch_is_active('enable_favorites_trainings_per_default'):
            if is_new:

                # lazy import
                from mtp.apps.trainings.models import Training

                trainings = Training.objects.non_deleted(
                ).filter(
                    models.Q(trainer_groups=None) | models.Q(trainer_groups__in=self.mgroups.all())
                )

                self.favorite_trainings.add(*trainings)

    def get_session_notes(self, session):
        return self.note_set.filter(training_session=session)

    def check_password_expiry(self):
        """
        Check if the user's password has expired, return ``True`` if it has,
        ``False`` otherwise.
        """
        if not settings.PASSWORD_EXTRA_SECURITY:
            return False
        now = timezone.now()
        expiry_delay = settings.PASSWORD_TTL
        expiry_date = self.last_changed_password + timezone.timedelta(days=expiry_delay)
        return now > expiry_date

    def get_pools_url(self):
        """ Get the url to list the pools of the current user.

        Returns
        -------
        string
            The wanted url.

        """
        return reverse('surveys:pools-list')

    @cached_property
    def is_trainer(self):
        """Tell if the current user is a trainer.

        Returns
        -------
        boolean
            ``True`` if the user is a trainer, or ``False``.

        Notes
        -----
        This is done by checking if the user is in the ``trainer`` group.

        """

        return (
            not self.is_deleted and
            self.is_active and
            self.is_authenticated() and
            self.mgroups.filter(name=INTERNAL_GROUP_TRAINER).exists()
        )

    @cached_property
    def is_trainee(self):
        """Tell if the current user is a trainee.

        Returns
        -------
        boolean
            ``True`` if the user is a trainee, or ``False``.

        Notes
        -----
        This is done by checking if the user is in the ``trainee`` group.

        """

        return (
            not self.is_deleted and
            self.is_active and
            self.is_authenticated() and
            self.mgroups.filter(name=INTERNAL_GROUP_TRAINEE).exists()
        )

    def like(self, obj, weight=LIKE_DEFAULT_WEIGHT):
        """Allow a user to like or dislike an object according the weight.

        Parameters
        ----------
        obj : object
            The object which will be liked by this user.
        weight : Optional[int]
            The weight of this like.

        Raises
        ------
        ValueError via `check_likeability`
            If the provided object isn't a likeable object.

        Notes
        -----
        We can't have more than one ``Like`` per author/object couple.

        Returns
        -------
        Like
            The created/updated like.

        """

        check_likeability(obj)

        try:
            like = obj.likes.for_author(self).get()
        except ObjectDoesNotExist:
            return obj.likes.create(author=self, weight=weight)
        else:
            like.weight = weight
            # The like's author could be deleted if he unliked it before.
            # So we reactive it.
            like.unremove(commit=False)
            like.save()
            return like

    def unlike(self, obj):
        """Allow a user to unlike an object.

        Parameters
        ----------
        obj : object
            The object which will be unliked by this user.

        Raises
        ------
        ValueError via `check_likeability`
            If the provided object is not a likeable object.

        Notes
        -----
        We can't have more than one Like per author/object couple.

        """

        check_likeability(obj)

        try:
            obj.likes.for_author(self).get().remove()
        except ObjectDoesNotExist:
            pass

    @classmethod
    def test_if_superuser(cls, user):
        """Class method to test if a user a an authenticated and active superuser.

        This is intended to be used with the ``user_passes_test`` from django.

        Example
        -------
        >>> from django.contrib.auth.decorators import user_passes_test
        >>> from mtp.apps.accounts.models import MTPUser
        >>>
        >>> @user_passes_test(MTPUser.test_is_superuser)
        >>> def function_based_view(request):
        >>>     pass  # do something
        ...
        >>> from django.utils.decorators import method_decorator
        >>> from django.views import generic
        >>>
        >>> class ClassBasedView(generic.View):
        >>>
        >>>     @method_decorator(user_passes_test(MTPUser.test_is_superuser))
        >>>     def dispatch(self, request, *args, **kwargs):
        >>>         super().dispatch(request, *args, **kwargs)
        ...

        """

        # check
        return (
            user.is_superuser and
            user.is_active and
            user.is_authenticated() and
            (not isinstance(user, cls) or not user.is_deleted)
        )

    @classmethod
    def test_if_superuser_or_trainer(cls, user):
        """Class method to test if a user a an authenticated and active superuser or trainer.

        This is intended to be used with the ``user_passes_test`` from django.

        Example
        -------
        >>> from django.contrib.auth.decorators import user_passes_test
        >>> from mtp.apps.accounts.models import MTPUser
        >>>
        >>> @user_passes_test(MTPUser.test_is_superuser_or_trainer)
        >>> def function_based_view(request):
        >>>     pass  # do something
        ...
        >>> from django.utils.decorators import method_decorator
        >>> from django.views import generic
        >>>
        >>> class ClassBasedView(generic.View):
        >>>
        >>>     @method_decorator(user_passes_test(MTPUser.test_is_superuser_or_trainer))
        >>>     def dispatch(self, request, *args, **kwargs):
        >>>         super().dispatch(request, *args, **kwargs)
        ...

        """

        # check
        return cls.test_if_trainer(user) or (
            user.is_superuser and
            user.is_active and
            user.is_authenticated() and
            (not isinstance(user, cls) or not user.is_deleted)
        )

    @classmethod
    def test_if_trainer(cls, user):
        """Class method to test if a user is an authenticated and active trainer.

        This is intended to be used with the ``user_passes_test`` from django.

        Example
        -------
        >>> from django.contrib.auth.decorators import user_passes_test
        >>> from mtp.apps.accounts.models import MTPUser
        >>>
        >>> @user_passes_test(MTPUser.test_is_trainer)
        >>> def function_based_view(request):
        >>>     pass  # do something
        ...
        >>> from django.utils.decorators import method_decorator
        >>> from django.views import generic
        >>>
        >>> class ClassBasedView(generic.View):
        >>>
        >>>     @method_decorator(user_passes_test(MTPUser.test_is_trainer))
        >>>     def dispatch(self, request, *args, **kwargs):
        >>>         super().dispatch(request, *args, **kwargs)
        ...

        """

        return isinstance(user, cls) and user.is_trainer

    @classmethod
    def test_if_trainee(cls, user):
        """Class method to test if a user is an authenticated and active trainee.

        This is intended to be used with the ``user_passes_test`` from django.

        Example
        -------
        >>> from django.contrib.auth.decorators import user_passes_test
        >>> from mtp.apps.accounts.models import MTPUser
        >>>
        >>> @user_passes_test(MTPUser.test_is_trainee)
        >>> def function_based_view(request):
        >>>     pass  # do something
        ...
        >>> from django.utils.decorators import method_decorator
        >>> from django.views import generic
        >>>
        >>> class ClassBasedView(generic.View):
        >>>
        >>>     @method_decorator(user_passes_test(MTPUser.test_is_trainee))
        >>>     def dispatch(self, request, *args, **kwargs):
        >>>         super().dispatch(request, *args, **kwargs)
        ...

        """

        return isinstance(user, cls) and user.is_trainee


@receiver(user_logged_in)
def user_logged_in_handler(user, **kwargs):
    """This handler `redispatch` signals coming from
    the django auth app to the handler in ``mtp.apps.eventlog`` app.
    """

    if 'request' in kwargs:
        # If we got a request object coming along with the event
        if 'cms' in kwargs['request'].GET.get('next', ''):
            # We got the keyword 'cms' in the query, the event come from the cms
            event_log(sender=user, subject=user, action=EVENT_TYPES.CMS_LOGIN)
        elif '/api/' in kwargs['request'].META.get('PATH_INFO', ''):
            # We got the 'api' keyword in the query, the event come from the api
            event_log(sender=user, subject=user, action=EVENT_TYPES.APP_LOGIN)
