Repository: jazzband/django-axes

Issue:
"is_admin_site" has the opposite meaning
`AxesHandler.is_admin_site()` actually checks if a `request.path` is *NOT* located under the `reverse("admin:index")`.

https://github.com/jazzband/django-axes/blob/ea4d3c477dd1fda2045ed8c927b6d0bb87200d27/axes/handlers/base.py#L136-L147


Discussion:
Yeah, I agree the name is not good.
`is_admin_site` returns `True` if `AXES_ONLY_ADMIN_SITE` is enable and the request path located under admin site.
do you have any suggestions for the function name?
How about the following?

```python
def is_allowed(self, request, credentials: dict = None) -> bool:
    ...
    if settings.AXES_ONLY_ADMIN_SITE and self.is_admin_request(request):
        return True
    ...

def is_admin_request(self, request) -> bool:
    if hasattr(request, "path"):
        return not is_admin_site(request)
    else:
        return False

def is_admin_site(self, request) -> bool:
    """Deprecated. Use `is_admin_request` instead."""
    ...
```
@aleksihakli what do you think about the new naming?
The naming is misleading. If you think there would be a better naming convention or logic for the function, then I'd vouch for fixing both the semantics for naming and functionality, as that would make the code easier to read and maintain in the long run? If we just add a method and mark another one deprecated then that doesn't really solve the issue of the implementation not being clear as-is.
Agreed. I'm just worried that someone uses `is_admin_site` for their custom handler. I also think the logic depends on `settings` should be placed at `is_allowed` if you can fix all `is_*`s.

I think it's OK to just introduce a new method with a better name and deprecate the old method either with a DeprecationWarning or just remove it so that anyone utilizing it would get a warning or an error from using it?
@okapies we're baking a new version 6.x release, would you be interested in including this change in that release? Now would be a good time to make breaking improvements and changes to the code base if you have any improvements in mind :)

Resolving patch:
diff --git a/axes/handlers/base.py b/axes/handlers/base.py
--- a/axes/handlers/base.py
+++ b/axes/handlers/base.py
@@ -1,6 +1,7 @@
 import re
 from abc import ABC, abstractmethod
 from typing import Optional
+from warnings import warn
 
 from django.urls import reverse
 from django.urls.exceptions import NoReverseMatch
@@ -81,7 +82,7 @@ def is_allowed(self, request, credentials: Optional[dict] = None) -> bool:
         and inspiration on some common checks and access restrictions before writing your own implementation.
         """
 
-        if self.is_admin_site(request):
+        if settings.AXES_ONLY_ADMIN_SITE and not self.is_admin_request(request):
             return True
 
         if self.is_blacklisted(request, credentials):
@@ -134,10 +135,41 @@ def is_locked(self, request, credentials: Optional[dict] = None) -> bool:
 
         return False
 
-    def is_admin_site(self, request) -> bool:
+    def get_admin_url(self) -> Optional[str]:
+        """
+        Returns admin url if exists, otherwise returns None
+        """
+        try:
+            return reverse("admin:index")
+        except NoReverseMatch:
+            return None
+
+    def is_admin_request(self, request) -> bool:
+        """
+        Checks that request located under admin site
         """
-        Checks if the request is for admin site.
+        if hasattr(request, "path"):
+            admin_url = self.get_admin_url()
+            return (
+                admin_url is not None
+                and re.match(f"^{admin_url}", request.path) is not None
+            )
+
+        return False
+
+    def is_admin_site(self, request) -> bool:
         """
+        Checks if the request is NOT for admin site
+        if `settings.AXES_ONLY_ADMIN_SITE` is True.
+        """
+        warn(
+            (
+                "This method is deprecated and will be removed in future versions. "
+                "If you looking for method that checks if `request.path` located under "
+                "admin site, use `is_admin_request` instead."
+            ),
+            DeprecationWarning,
+        )
         if settings.AXES_ONLY_ADMIN_SITE and hasattr(request, "path"):
             try:
                 admin_url = reverse("admin:index")


Test patch:
diff --git a/tests/test_handlers.py b/tests/test_handlers.py
--- a/tests/test_handlers.py
+++ b/tests/test_handlers.py
@@ -55,14 +55,36 @@ def test_is_admin_site(self):
         for setting_value, url, expected in tests:
             with override_settings(AXES_ONLY_ADMIN_SITE=setting_value):
                 request.path = url
-                self.assertEqual(AxesProxyHandler().is_admin_site(request), expected)
+                with self.assertWarns(DeprecationWarning):
+                    self.assertEqual(AxesProxyHandler().is_admin_site(request), expected)
+
+    def test_is_admin_request(self):
+        request = MagicMock()
+        tests = (  # (URL, Expected)
+            ("/test/", False),
+            (reverse("admin:index"), True),
+        )
+
+        for url, expected in tests:
+            request.path = url
+            self.assertEqual(AxesProxyHandler().is_admin_request(request), expected)
 
     @override_settings(ROOT_URLCONF="tests.urls_empty")
     @override_settings(AXES_ONLY_ADMIN_SITE=True)
     def test_is_admin_site_no_admin_site(self):
         request = MagicMock()
         request.path = "/admin/"
-        self.assertTrue(AxesProxyHandler().is_admin_site(self.request))
+        with self.assertWarns(DeprecationWarning):
+            self.assertTrue(AxesProxyHandler().is_admin_site(self.request))
+
+    @override_settings(ROOT_URLCONF="tests.urls_empty")
+    def test_is_admin_request_no_admin_site(self):
+        request = MagicMock()
+        request.path = "/admin/"
+        self.assertFalse(AxesProxyHandler().is_admin_request(self.request))
+
+    def test_is_admin_request_no_path(self):
+        self.assertFalse(AxesProxyHandler().is_admin_request(self.request))
 
 
 class AxesProxyHandlerTestCase(AxesTestCase):