# 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.

        .. warning::

            This originally existed for more convenient integration with the admin
            site. Its use is no longer recommended and it is not guaranteed to
            interact well with more recent features (such as throttling). Tokens
            should always be verified 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