Skip to content

Commit 86e78b9

Browse files
authored
#898 Added the ability to customize classes for django admin (#904)
1 parent 5cb5398 commit 86e78b9

File tree

8 files changed

+234
-31
lines changed

8 files changed

+234
-31
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Contributors
99

1010
Abhishek Patel
1111
Alessandro De Angelis
12+
Aleksander Vaskevich
1213
Alan Crosswell
1314
Anvesh Agarwal
1415
Asif Saif Uddin

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

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

1717
## [unreleased]
1818

19+
* #898 Added the ability to customize classes for django admin
20+
1921
### Added
2022
* #884 Added support for Python 3.9
2123

docs/settings.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,30 @@ The import string of the class (model) representing your grants. Overwrite
9797
this value if you wrote your own implementation (subclass of
9898
``oauth2_provider.models.Grant``).
9999

100+
APPLICATION_ADMIN_CLASS
101+
~~~~~~~~~~~~~~~~~
102+
The import string of the class (model) representing your application admin class.
103+
Overwrite this value if you wrote your own implementation (subclass of
104+
``oauth2_provider.admin.ApplicationAdmin``).
105+
106+
ACCESS_TOKEN_ADMIN_CLASS
107+
~~~~~~~~~~~~~~~~~
108+
The import string of the class (model) representing your access token admin class.
109+
Overwrite this value if you wrote your own implementation (subclass of
110+
``oauth2_provider.admin.AccessTokenAdmin``).
111+
112+
GRANT_ADMIN_CLASS
113+
~~~~~~~~~~~~~~~~~
114+
The import string of the class (model) representing your grant admin class.
115+
Overwrite this value if you wrote your own implementation (subclass of
116+
``oauth2_provider.admin.GrantAdmin``).
117+
118+
REFRESH_TOKEN_ADMIN_CLASS
119+
~~~~~~~~~~~~~~~~~
120+
The import string of the class (model) representing your refresh token admin class.
121+
Overwrite this value if you wrote your own implementation (subclass of
122+
``oauth2_provider.admin.RefreshTokenAdmin``).
123+
100124
OAUTH2_SERVER_CLASS
101125
~~~~~~~~~~~~~~~~~~~
102126
The import string for the ``server_class`` (or ``oauthlib.oauth2.Server`` subclass)

oauth2_provider/admin.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
from django.contrib import admin
22

3-
from .models import get_access_token_model, get_application_model, get_grant_model, get_refresh_token_model
3+
from oauth2_provider.models import (
4+
get_access_token_admin_class,
5+
get_access_token_model,
6+
get_application_admin_class,
7+
get_application_model,
8+
get_grant_admin_class,
9+
get_grant_model,
10+
get_refresh_token_admin_class,
11+
get_refresh_token_model,
12+
)
413

514

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

1524

16-
class GrantAdmin(admin.ModelAdmin):
17-
list_display = ("code", "application", "user", "expires")
18-
raw_id_fields = ("user",)
19-
20-
2125
class AccessTokenAdmin(admin.ModelAdmin):
2226
list_display = ("token", "user", "application", "expires")
2327
raw_id_fields = ("user", "source_refresh_token")
2428

2529

30+
class GrantAdmin(admin.ModelAdmin):
31+
list_display = ("code", "application", "user", "expires")
32+
raw_id_fields = ("user",)
33+
34+
2635
class RefreshTokenAdmin(admin.ModelAdmin):
2736
list_display = ("token", "user", "application")
2837
raw_id_fields = ("user", "access_token")
2938

3039

31-
Application = get_application_model()
32-
Grant = get_grant_model()
33-
AccessToken = get_access_token_model()
34-
RefreshToken = get_refresh_token_model()
40+
application_model = get_application_model()
41+
access_token_model = get_access_token_model()
42+
grant_model = get_grant_model()
43+
refresh_token_model = get_refresh_token_model()
44+
45+
application_admin_class = get_application_admin_class()
46+
access_token_admin_class = get_access_token_admin_class()
47+
grant_admin_class = get_grant_admin_class()
48+
refresh_token_admin_class = get_refresh_token_admin_class()
3549

36-
admin.site.register(Application, ApplicationAdmin)
37-
admin.site.register(Grant, GrantAdmin)
38-
admin.site.register(AccessToken, AccessTokenAdmin)
39-
admin.site.register(RefreshToken, RefreshTokenAdmin)
50+
admin.site.register(application_model, application_admin_class)
51+
admin.site.register(access_token_model, access_token_admin_class)
52+
admin.site.register(grant_model, grant_admin_class)
53+
admin.site.register(refresh_token_model, refresh_token_admin_class)

oauth2_provider/models.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,30 @@ def get_refresh_token_model():
453453
return apps.get_model(oauth2_settings.REFRESH_TOKEN_MODEL)
454454

455455

456+
def get_application_admin_class():
457+
""" Return the Application admin class that is active in this project. """
458+
application_admin_class = oauth2_settings.APPLICATION_ADMIN_CLASS
459+
return application_admin_class
460+
461+
462+
def get_access_token_admin_class():
463+
""" Return the AccessToken admin class that is active in this project. """
464+
access_token_admin_class = oauth2_settings.ACCESS_TOKEN_ADMIN_CLASS
465+
return access_token_admin_class
466+
467+
468+
def get_grant_admin_class():
469+
""" Return the Grant admin class that is active in this project. """
470+
grant_admin_class = oauth2_settings.GRANT_ADMIN_CLASS
471+
return grant_admin_class
472+
473+
474+
def get_refresh_token_admin_class():
475+
""" Return the RefreshToken admin class that is active in this project. """
476+
refresh_token_admin_class = oauth2_settings.REFRESH_TOKEN_ADMIN_CLASS
477+
return refresh_token_admin_class
478+
479+
456480
def clear_expired():
457481
now = timezone.now()
458482
refresh_expire_at = None

oauth2_provider/settings.py

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@
1515
OAuth2 Provider settings, checking for user settings first, then falling
1616
back to the defaults.
1717
"""
18-
import importlib
1918

2019
from django.conf import settings
2120
from django.core.exceptions import ImproperlyConfigured
21+
from django.test.signals import setting_changed
22+
from django.utils.module_loading import import_string
2223

2324

2425
USER_SETTINGS = getattr(settings, "OAUTH2_PROVIDER", None)
@@ -53,6 +54,10 @@
5354
"ACCESS_TOKEN_MODEL": ACCESS_TOKEN_MODEL,
5455
"GRANT_MODEL": GRANT_MODEL,
5556
"REFRESH_TOKEN_MODEL": REFRESH_TOKEN_MODEL,
57+
"APPLICATION_ADMIN_CLASS": "oauth2_provider.admin.ApplicationAdmin",
58+
"ACCESS_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.AccessTokenAdmin",
59+
"GRANT_ADMIN_CLASS": "oauth2_provider.admin.GrantAdmin",
60+
"REFRESH_TOKEN_ADMIN_CLASS": "oauth2_provider.admin.RefreshTokenAdmin",
5661
"REQUEST_APPROVAL_PROMPT": "force",
5762
"ALLOWED_REDIRECT_URI_SCHEMES": ["http", "https"],
5863
# Special settings that will be evaluated at runtime
@@ -88,6 +93,10 @@
8893
"OAUTH2_VALIDATOR_CLASS",
8994
"OAUTH2_BACKEND_CLASS",
9095
"SCOPES_BACKEND_CLASS",
96+
"APPLICATION_ADMIN_CLASS",
97+
"ACCESS_TOKEN_ADMIN_CLASS",
98+
"GRANT_ADMIN_CLASS",
99+
"REFRESH_TOKEN_ADMIN_CLASS",
91100
)
92101

93102

@@ -96,23 +105,21 @@ def perform_import(val, setting_name):
96105
If the given setting is a string import notation,
97106
then perform the necessary import or imports.
98107
"""
99-
if isinstance(val, (list, tuple)):
100-
return [import_from_string(item, setting_name) for item in val]
101-
elif "." in val:
108+
if val is None:
109+
return None
110+
elif isinstance(val, str):
102111
return import_from_string(val, setting_name)
103-
else:
104-
raise ImproperlyConfigured("Bad value for %r: %r" % (setting_name, val))
112+
elif isinstance(val, (list, tuple)):
113+
return [import_from_string(item, setting_name) for item in val]
114+
return val
105115

106116

107117
def import_from_string(val, setting_name):
108118
"""
109119
Attempt to import a class from a string representation.
110120
"""
111121
try:
112-
parts = val.split(".")
113-
module_path, class_name = ".".join(parts[:-1]), parts[-1]
114-
module = importlib.import_module(module_path)
115-
return getattr(module, class_name)
122+
return import_string(val)
116123
except ImportError as e:
117124
msg = "Could not import %r for setting %r. %s: %s." % (val, setting_name, e.__class__.__name__, e)
118125
raise ImportError(msg)
@@ -127,14 +134,21 @@ class OAuth2ProviderSettings:
127134
"""
128135

129136
def __init__(self, user_settings=None, defaults=None, import_strings=None, mandatory=None):
130-
self.user_settings = user_settings or {}
131-
self.defaults = defaults or {}
132-
self.import_strings = import_strings or ()
137+
self._user_settings = user_settings or {}
138+
self.defaults = defaults or DEFAULTS
139+
self.import_strings = import_strings or IMPORT_STRINGS
133140
self.mandatory = mandatory or ()
141+
self._cached_attrs = set()
142+
143+
@property
144+
def user_settings(self):
145+
if not hasattr(self, "_user_settings"):
146+
self._user_settings = getattr(settings, "OAUTH2_PROVIDER", {})
147+
return self._user_settings
134148

135149
def __getattr__(self, attr):
136-
if attr not in self.defaults.keys():
137-
raise AttributeError("Invalid OAuth2Provider setting: %r" % (attr))
150+
if attr not in self.defaults:
151+
raise AttributeError("Invalid OAuth2Provider setting: %s" % attr)
138152

139153
try:
140154
# Check if present in user settings
@@ -166,12 +180,13 @@ def __getattr__(self, attr):
166180
self.validate_setting(attr, val)
167181

168182
# Cache the result
183+
self._cached_attrs.add(attr)
169184
setattr(self, attr, val)
170185
return val
171186

172187
def validate_setting(self, attr, val):
173188
if not val and attr in self.mandatory:
174-
raise AttributeError("OAuth2Provider setting: %r is mandatory" % (attr))
189+
raise AttributeError("OAuth2Provider setting: %s is mandatory" % attr)
175190

176191
@property
177192
def server_kwargs(self):
@@ -199,5 +214,21 @@ def server_kwargs(self):
199214
kwargs.update(self.EXTRA_SERVER_KWARGS)
200215
return kwargs
201216

217+
def reload(self):
218+
for attr in self._cached_attrs:
219+
delattr(self, attr)
220+
self._cached_attrs.clear()
221+
if hasattr(self, "_user_settings"):
222+
delattr(self, "_user_settings")
223+
202224

203225
oauth2_settings = OAuth2ProviderSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS, MANDATORY)
226+
227+
228+
def reload_oauth2_settings(*args, **kwargs):
229+
setting = kwargs["setting"]
230+
if setting == "OAUTH2_PROVIDER":
231+
oauth2_settings.reload()
232+
233+
234+
setting_changed.connect(reload_oauth2_settings)

tests/admin.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from django.contrib import admin
2+
3+
4+
class CustomApplicationAdmin(admin.ModelAdmin):
5+
list_display = ("id",)
6+
7+
8+
class CustomAccessTokenAdmin(admin.ModelAdmin):
9+
list_display = ("id",)
10+
11+
12+
class CustomGrantAdmin(admin.ModelAdmin):
13+
list_display = ("id",)
14+
15+
16+
class CustomRefreshTokenAdmin(admin.ModelAdmin):
17+
list_display = ("id",)

tests/test_settings.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from django.test import TestCase
2+
from django.test.utils import override_settings
3+
4+
from oauth2_provider.admin import (
5+
get_access_token_admin_class,
6+
get_application_admin_class,
7+
get_grant_admin_class,
8+
get_refresh_token_admin_class,
9+
)
10+
from oauth2_provider.settings import OAuth2ProviderSettings, oauth2_settings
11+
from tests.admin import (
12+
CustomAccessTokenAdmin,
13+
CustomApplicationAdmin,
14+
CustomGrantAdmin,
15+
CustomRefreshTokenAdmin,
16+
)
17+
18+
19+
class TestAdminClass(TestCase):
20+
def test_import_error_message_maintained(self):
21+
"""
22+
Make sure import errors are captured and raised sensibly.
23+
"""
24+
settings = OAuth2ProviderSettings({"CLIENT_ID_GENERATOR_CLASS": "invalid_module.InvalidClassName"})
25+
with self.assertRaises(ImportError):
26+
settings.CLIENT_ID_GENERATOR_CLASS
27+
28+
def test_get_application_admin_class(self):
29+
"""
30+
Test for getting class for application admin.
31+
"""
32+
application_admin_class = get_application_admin_class()
33+
default_application_admin_class = oauth2_settings.APPLICATION_ADMIN_CLASS
34+
assert application_admin_class == default_application_admin_class
35+
36+
def test_get_access_token_admin_class(self):
37+
"""
38+
Test for getting class for access token admin.
39+
"""
40+
access_token_admin_class = get_access_token_admin_class()
41+
default_access_token_admin_class = oauth2_settings.ACCESS_TOKEN_ADMIN_CLASS
42+
assert access_token_admin_class == default_access_token_admin_class
43+
44+
def test_get_grant_admin_class(self):
45+
"""
46+
Test for getting class for grant admin.
47+
"""
48+
grant_admin_class = get_grant_admin_class()
49+
default_grant_admin_class = oauth2_settings.GRANT_ADMIN_CLASS
50+
assert grant_admin_class, default_grant_admin_class
51+
52+
def test_get_refresh_token_admin_class(self):
53+
"""
54+
Test for getting class for refresh token admin.
55+
"""
56+
refresh_token_admin_class = get_refresh_token_admin_class()
57+
default_refresh_token_admin_class = oauth2_settings.REFRESH_TOKEN_ADMIN_CLASS
58+
assert refresh_token_admin_class == default_refresh_token_admin_class
59+
60+
@override_settings(OAUTH2_PROVIDER={"APPLICATION_ADMIN_CLASS": "tests.admin.CustomApplicationAdmin"})
61+
def test_get_custom_application_admin_class(self):
62+
"""
63+
Test for getting custom class for application admin.
64+
"""
65+
application_admin_class = get_application_admin_class()
66+
assert application_admin_class == CustomApplicationAdmin
67+
68+
@override_settings(OAUTH2_PROVIDER={"ACCESS_TOKEN_ADMIN_CLASS": "tests.admin.CustomAccessTokenAdmin"})
69+
def test_get_custom_access_token_admin_class(self):
70+
"""
71+
Test for getting custom class for access token admin.
72+
"""
73+
access_token_admin_class = get_access_token_admin_class()
74+
assert access_token_admin_class == CustomAccessTokenAdmin
75+
76+
@override_settings(OAUTH2_PROVIDER={"GRANT_ADMIN_CLASS": "tests.admin.CustomGrantAdmin"})
77+
def test_get_custom_grant_admin_class(self):
78+
"""
79+
Test for getting custom class for grant admin.
80+
"""
81+
grant_admin_class = get_grant_admin_class()
82+
assert grant_admin_class == CustomGrantAdmin
83+
84+
@override_settings(OAUTH2_PROVIDER={"REFRESH_TOKEN_ADMIN_CLASS": "tests.admin.CustomRefreshTokenAdmin"})
85+
def test_get_custom_refresh_token_admin_class(self):
86+
"""
87+
Test for getting custom class for refresh token admin.
88+
"""
89+
refresh_token_admin_class = get_refresh_token_admin_class()
90+
assert refresh_token_admin_class == CustomRefreshTokenAdmin

0 commit comments

Comments
 (0)