Skip to content

Commit

Permalink
#898 Added the ability to customize classes for django admin (#904)
Browse files Browse the repository at this point in the history
  • Loading branch information
folt committed Dec 16, 2020
1 parent 5cb5398 commit 86e78b9
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 31 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Contributors

Abhishek Patel
Alessandro De Angelis
Aleksander Vaskevich
Alan Crosswell
Anvesh Agarwal
Asif Saif Uddin
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!--
<!--
## [unreleased]
### Added
### Changed
Expand All @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [unreleased]

* #898 Added the ability to customize classes for django admin

### Added
* #884 Added support for Python 3.9

Expand Down
24 changes: 24 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,30 @@ The import string of the class (model) representing your grants. Overwrite
this value if you wrote your own implementation (subclass of
``oauth2_provider.models.Grant``).

APPLICATION_ADMIN_CLASS
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your application admin class.
Overwrite this value if you wrote your own implementation (subclass of
``oauth2_provider.admin.ApplicationAdmin``).

ACCESS_TOKEN_ADMIN_CLASS
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your access token admin class.
Overwrite this value if you wrote your own implementation (subclass of
``oauth2_provider.admin.AccessTokenAdmin``).

GRANT_ADMIN_CLASS
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your grant admin class.
Overwrite this value if you wrote your own implementation (subclass of
``oauth2_provider.admin.GrantAdmin``).

REFRESH_TOKEN_ADMIN_CLASS
~~~~~~~~~~~~~~~~~
The import string of the class (model) representing your refresh token admin class.
Overwrite this value if you wrote your own implementation (subclass of
``oauth2_provider.admin.RefreshTokenAdmin``).

OAUTH2_SERVER_CLASS
~~~~~~~~~~~~~~~~~~~
The import string for the ``server_class`` (or ``oauthlib.oauth2.Server`` subclass)
Expand Down
42 changes: 28 additions & 14 deletions oauth2_provider/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from django.contrib import admin

from .models import get_access_token_model, get_application_model, get_grant_model, get_refresh_token_model
from oauth2_provider.models import (
get_access_token_admin_class,
get_access_token_model,
get_application_admin_class,
get_application_model,
get_grant_admin_class,
get_grant_model,
get_refresh_token_admin_class,
get_refresh_token_model,
)


class ApplicationAdmin(admin.ModelAdmin):
Expand All @@ -13,27 +22,32 @@ class ApplicationAdmin(admin.ModelAdmin):
raw_id_fields = ("user",)


class GrantAdmin(admin.ModelAdmin):
list_display = ("code", "application", "user", "expires")
raw_id_fields = ("user",)


class AccessTokenAdmin(admin.ModelAdmin):
list_display = ("token", "user", "application", "expires")
raw_id_fields = ("user", "source_refresh_token")


class GrantAdmin(admin.ModelAdmin):
list_display = ("code", "application", "user", "expires")
raw_id_fields = ("user",)


class RefreshTokenAdmin(admin.ModelAdmin):
list_display = ("token", "user", "application")
raw_id_fields = ("user", "access_token")


Application = get_application_model()
Grant = get_grant_model()
AccessToken = get_access_token_model()
RefreshToken = get_refresh_token_model()
application_model = get_application_model()
access_token_model = get_access_token_model()
grant_model = get_grant_model()
refresh_token_model = get_refresh_token_model()

application_admin_class = get_application_admin_class()
access_token_admin_class = get_access_token_admin_class()
grant_admin_class = get_grant_admin_class()
refresh_token_admin_class = get_refresh_token_admin_class()

admin.site.register(Application, ApplicationAdmin)
admin.site.register(Grant, GrantAdmin)
admin.site.register(AccessToken, AccessTokenAdmin)
admin.site.register(RefreshToken, RefreshTokenAdmin)
admin.site.register(application_model, application_admin_class)
admin.site.register(access_token_model, access_token_admin_class)
admin.site.register(grant_model, grant_admin_class)
admin.site.register(refresh_token_model, refresh_token_admin_class)
24 changes: 24 additions & 0 deletions oauth2_provider/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,30 @@ def get_refresh_token_model():
return apps.get_model(oauth2_settings.REFRESH_TOKEN_MODEL)


def get_application_admin_class():
""" Return the Application admin class that is active in this project. """
application_admin_class = oauth2_settings.APPLICATION_ADMIN_CLASS
return application_admin_class


def get_access_token_admin_class():
""" Return the AccessToken admin class that is active in this project. """
access_token_admin_class = oauth2_settings.ACCESS_TOKEN_ADMIN_CLASS
return access_token_admin_class


def get_grant_admin_class():
""" Return the Grant admin class that is active in this project. """
grant_admin_class = oauth2_settings.GRANT_ADMIN_CLASS
return grant_admin_class


def get_refresh_token_admin_class():
""" Return the RefreshToken admin class that is active in this project. """
refresh_token_admin_class = oauth2_settings.REFRESH_TOKEN_ADMIN_CLASS
return refresh_token_admin_class


def clear_expired():
now = timezone.now()
refresh_expire_at = None
Expand Down
63 changes: 47 additions & 16 deletions oauth2_provider/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
OAuth2 Provider settings, checking for user settings first, then falling
back to the defaults.
"""
import importlib

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.test.signals import setting_changed
from django.utils.module_loading import import_string


USER_SETTINGS = getattr(settings, "OAUTH2_PROVIDER", None)
Expand Down Expand Up @@ -53,6 +54,10 @@
"ACCESS_TOKEN_MODEL": ACCESS_TOKEN_MODEL,
"GRANT_MODEL": GRANT_MODEL,
"REFRESH_TOKEN_MODEL": REFRESH_TOKEN_MODEL,
"APPLICATION_ADMIN_CLASS": "oauth2_provider.admin.ApplicationAdmin",
"ACCESS_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.AccessTokenAdmin",
"GRANT_ADMIN_CLASS": "oauth2_provider.admin.GrantAdmin",
"REFRESH_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.RefreshTokenAdmin",
"REQUEST_APPROVAL_PROMPT": "force",
"ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"],
# Special settings that will be evaluated at runtime
Expand Down Expand Up @@ -88,6 +93,10 @@
"OAUTH2_VALIDATOR_CLASS",
"OAUTH2_BACKEND_CLASS",
"SCOPES_BACKEND_CLASS",
"APPLICATION_ADMIN_CLASS",
"ACCESS_TOKEN_ADMIN_CLASS",
"GRANT_ADMIN_CLASS",
"REFRESH_TOKEN_ADMIN_CLASS",
)


Expand All @@ -96,23 +105,21 @@ def perform_import(val, setting_name):
If the given setting is a string import notation,
then perform the necessary import or imports.
"""
if isinstance(val, (list, tuple)):
return [import_from_string(item, setting_name) for item in val]
elif "." in val:
if val is None:
return None
elif isinstance(val, str):
return import_from_string(val, setting_name)
else:
raise ImproperlyConfigured("Bad value for %r: %r" % (setting_name, val))
elif isinstance(val, (list, tuple)):
return [import_from_string(item, setting_name) for item in val]
return val


def import_from_string(val, setting_name):
"""
Attempt to import a class from a string representation.
"""
try:
parts = val.split(".")
module_path, class_name = ".".join(parts[:-1]), parts[-1]
module = importlib.import_module(module_path)
return getattr(module, class_name)
return import_string(val)
except ImportError as e:
msg = "Could not import %r for setting %r. %s: %s." % (val, setting_name, e.__class__.__name__, e)
raise ImportError(msg)
Expand All @@ -127,14 +134,21 @@ class OAuth2ProviderSettings:
"""

def __init__(self, user_settings=None, defaults=None, import_strings=None, mandatory=None):
self.user_settings = user_settings or {}
self.defaults = defaults or {}
self.import_strings = import_strings or ()
self._user_settings = user_settings or {}
self.defaults = defaults or DEFAULTS
self.import_strings = import_strings or IMPORT_STRINGS
self.mandatory = mandatory or ()
self._cached_attrs = set()

@property
def user_settings(self):
if not hasattr(self, "_user_settings"):
self._user_settings = getattr(settings, "OAUTH2_PROVIDER", {})
return self._user_settings

def __getattr__(self, attr):
if attr not in self.defaults.keys():
raise AttributeError("Invalid OAuth2Provider setting: %r" % (attr))
if attr not in self.defaults:
raise AttributeError("Invalid OAuth2Provider setting: %s" % attr)

try:
# Check if present in user settings
Expand Down Expand Up @@ -166,12 +180,13 @@ def __getattr__(self, attr):
self.validate_setting(attr, val)

# Cache the result
self._cached_attrs.add(attr)
setattr(self, attr, val)
return val

def validate_setting(self, attr, val):
if not val and attr in self.mandatory:
raise AttributeError("OAuth2Provider setting: %r is mandatory" % (attr))
raise AttributeError("OAuth2Provider setting: %s is mandatory" % attr)

@property
def server_kwargs(self):
Expand Down Expand Up @@ -199,5 +214,21 @@ def server_kwargs(self):
kwargs.update(self.EXTRA_SERVER_KWARGS)
return kwargs

def reload(self):
for attr in self._cached_attrs:
delattr(self, attr)
self._cached_attrs.clear()
if hasattr(self, "_user_settings"):
delattr(self, "_user_settings")


oauth2_settings = OAuth2ProviderSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY)


def reload_oauth2_settings(*args, **kwargs):
setting = kwargs["setting"]
if setting == "OAUTH2_PROVIDER":
oauth2_settings.reload()


setting_changed.connect(reload_oauth2_settings)
17 changes: 17 additions & 0 deletions tests/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.contrib import admin


class CustomApplicationAdmin(admin.ModelAdmin):
list_display = ("id",)


class CustomAccessTokenAdmin(admin.ModelAdmin):
list_display = ("id",)


class CustomGrantAdmin(admin.ModelAdmin):
list_display = ("id",)


class CustomRefreshTokenAdmin(admin.ModelAdmin):
list_display = ("id",)
90 changes: 90 additions & 0 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from django.test import TestCase
from django.test.utils import override_settings

from oauth2_provider.admin import (
get_access_token_admin_class,
get_application_admin_class,
get_grant_admin_class,
get_refresh_token_admin_class,
)
from oauth2_provider.settings import OAuth2ProviderSettings, oauth2_settings
from tests.admin import (
CustomAccessTokenAdmin,
CustomApplicationAdmin,
CustomGrantAdmin,
CustomRefreshTokenAdmin,
)


class TestAdminClass(TestCase):
def test_import_error_message_maintained(self):
"""
Make sure import errors are captured and raised sensibly.
"""
settings = OAuth2ProviderSettings({"CLIENT_ID_GENERATOR_CLASS": "invalid_module.InvalidClassName"})
with self.assertRaises(ImportError):
settings.CLIENT_ID_GENERATOR_CLASS

def test_get_application_admin_class(self):
"""
Test for getting class for application admin.
"""
application_admin_class = get_application_admin_class()
default_application_admin_class = oauth2_settings.APPLICATION_ADMIN_CLASS
assert application_admin_class == default_application_admin_class

def test_get_access_token_admin_class(self):
"""
Test for getting class for access token admin.
"""
access_token_admin_class = get_access_token_admin_class()
default_access_token_admin_class = oauth2_settings.ACCESS_TOKEN_ADMIN_CLASS
assert access_token_admin_class == default_access_token_admin_class

def test_get_grant_admin_class(self):
"""
Test for getting class for grant admin.
"""
grant_admin_class = get_grant_admin_class()
default_grant_admin_class = oauth2_settings.GRANT_ADMIN_CLASS
assert grant_admin_class, default_grant_admin_class

def test_get_refresh_token_admin_class(self):
"""
Test for getting class for refresh token admin.
"""
refresh_token_admin_class = get_refresh_token_admin_class()
default_refresh_token_admin_class = oauth2_settings.REFRESH_TOKEN_ADMIN_CLASS
assert refresh_token_admin_class == default_refresh_token_admin_class

@override_settings(OAUTH2_PROVIDER={"APPLICATION_ADMIN_CLASS": "tests.admin.CustomApplicationAdmin"})
def test_get_custom_application_admin_class(self):
"""
Test for getting custom class for application admin.
"""
application_admin_class = get_application_admin_class()
assert application_admin_class == CustomApplicationAdmin

@override_settings(OAUTH2_PROVIDER={"ACCESS_TOKEN_ADMIN_CLASS": "tests.admin.CustomAccessTokenAdmin"})
def test_get_custom_access_token_admin_class(self):
"""
Test for getting custom class for access token admin.
"""
access_token_admin_class = get_access_token_admin_class()
assert access_token_admin_class == CustomAccessTokenAdmin

@override_settings(OAUTH2_PROVIDER={"GRANT_ADMIN_CLASS": "tests.admin.CustomGrantAdmin"})
def test_get_custom_grant_admin_class(self):
"""
Test for getting custom class for grant admin.
"""
grant_admin_class = get_grant_admin_class()
assert grant_admin_class == CustomGrantAdmin

@override_settings(OAUTH2_PROVIDER={"REFRESH_TOKEN_ADMIN_CLASS": "tests.admin.CustomRefreshTokenAdmin"})
def test_get_custom_refresh_token_admin_class(self):
"""
Test for getting custom class for refresh token admin.
"""
refresh_token_admin_class = get_refresh_token_admin_class()
assert refresh_token_admin_class == CustomRefreshTokenAdmin

0 comments on commit 86e78b9

Please sign in to comment.