# Source code for django_otp

    from django.contrib.auth.signals import user_logged_in
    from django.db import transaction

    DEVICE_ID_SESSION_KEY = 'otp_device_id'

    def login(request, device):
        """
        Persist the given OTP device in the current session. The device will be
        rejected if it does not belong to ``request.user``.

        This is called automatically any time :func:`django.contrib.auth.login` is
        called with a user having an ``otp_device`` atribute. If you use Django's
        :class:`~django.contrib.auth.views.LoginView` view with the django-otp
        authentication forms, then you won't need to call this.

        :param request: The HTTP request
        :type request: :class:`~django.http.HttpRequest`

        :param device: The OTP device used to verify the user.
        :type device: :class:`~django_otp.models.Device`
        """
        user = getattr(request, 'user', None)

        if (user is not None) and (device is not None) and (device.user_id == user.pk):
            request.session[DEVICE_ID_SESSION_KEY] = device.persistent_id
            request.user.otp_device = device

    def _handle_auth_login(sender, request, user, **kwargs):
        """
        Automatically persists an OTP device that was set by an OTP-aware
        AuthenticationForm.
        """
        if hasattr(user, 'otp_device'):
            login(request, user.otp_device)

    user_logged_in.connect(_handle_auth_login)

    def verify_token(user, device_id, token):
        """
        Attempts to verify a :term:`token` against a specific device, identified by
        :attr:`~django_otp.models.Device.persistent_id`.

        This wraps the verification process in a transaction to ensure that things
        like throttling polices are properly enforced.

        :param user: The user supplying the token.
        :type user: :class:`~django.contrib.auth.models.User`

        :param str device_id: A device's persistent_id value.

        :param str token: An OTP token to verify.

        :returns: The device that accepted ``token``, if any.
        :rtype: :class:`~django_otp.models.Device` or ``None``

        """
        from django_otp.models import Device

        verified = None
        with transaction.atomic():
            device = Device.from_persistent_id(device_id, for_verify=True)
            if (device is not None) and (device.user_id == user.pk) and device.verify_token(token):
                verified = device

        return verified

    def match_token(user, token):
        """
        Attempts to verify a :term:`token` on every device attached to the given
        user until one of them succeeds. When possible, you should prefer to verify
        tokens against specific devices.

        :param user: The user supplying the token.
        :type user: :class:`~django.contrib.auth.models.User`

        :param str token: An OTP token to verify.

        :returns: The device that accepted ``token``, if any.
        :rtype: :class:`~django_otp.models.Device` or ``None``
        """
        with transaction.atomic():
            for device in devices_for_user(user, for_verify=True):
                if device.verify_token(token):
                    break
            else:
                device = None

        return device

    def devices_for_user(user, confirmed=True, for_verify=False):
        """
        Return an iterable of all devices registered to the given user.

        Returns an empty iterable for anonymous users.

        :param user: standard or custom user object.
        :type user: :class:`~django.contrib.auth.models.User`

        :param bool confirmed: If ``None``, all matching devices are returned.
            Otherwise, this can be any true or false value to limit the query
            to confirmed or unconfirmed devices, respectively.

        :param bool for_verify: If ``True``, we'll load the devices with
            :meth:`~django.db.models.query.QuerySet.select_for_update` to prevent
            concurrent verifications from succeeding. In which case, this must be
            called inside a transaction.

        :rtype: iterable
        """
        if user.is_anonymous:
            return

        for model in device_classes():
            device_set = model.objects.devices_for_user(user, confirmed=confirmed)
            if for_verify:
                device_set = device_set.select_for_update()

            yield from device_set

    def user_has_device(user, confirmed=True):
        """
        Return ``True`` if the user has at least one device.

        Returns ``False`` for anonymous users.

        :param user: standard or custom user object.
        :type user: :class:`~django.contrib.auth.models.User`

        :param confirmed: If ``None``, all matching devices are considered.
            Otherwise, this can be any true or false value to limit the query
            to confirmed or unconfirmed devices, respectively.
        """
        try:
            next(devices_for_user(user, confirmed=confirmed))
        except StopIteration:
            has_device = False
        else:
            has_device = True

        return has_device

    def device_classes():
        """
        Returns an iterable of all loaded device models.
        """
        from django.apps import apps           # isort: skip
        from django_otp.models import Device

        for config in apps.get_app_configs():
            for model in config.get_models():
                if issubclass(model, Device):
                    yield model