feat: dev mode admin + refactor api init (#7628)
* feat: style admin site in dev mode * refactor: eliminate base_site.html * fix: remove debug * fix: commit missing __init__.py * refactor: make method static; fix tests * refactor: move api init to AppConfig.ready() Avoids interacting with the app registry before it's ready.
This commit is contained in:
parent
b90820eb7a
commit
95a7e14ada
0
ietf/admin/__init__.py
Normal file
0
ietf/admin/__init__.py
Normal file
6
ietf/admin/apps.py
Normal file
6
ietf/admin/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||||
|
from django.contrib.admin import apps as admin_apps
|
||||||
|
|
||||||
|
|
||||||
|
class AdminConfig(admin_apps.AdminConfig):
|
||||||
|
default_site = "ietf.admin.sites.AdminSite"
|
15
ietf/admin/sites.py
Normal file
15
ietf/admin/sites.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Copyright The IETF Trust 2024, All Rights Reserved
|
||||||
|
from django.contrib.admin import AdminSite as _AdminSite
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
|
class AdminSite(_AdminSite):
|
||||||
|
site_title = "Datatracker admin"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def site_header():
|
||||||
|
if settings.SERVER_MODE == "production":
|
||||||
|
return "Datatracker administration"
|
||||||
|
else:
|
||||||
|
return mark_safe('Datatracker administration <span class="text-danger">δ</span>')
|
|
@ -7,8 +7,10 @@ import re
|
||||||
|
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from django.conf import settings
|
from django.apps import apps as django_apps
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.utils.module_loading import autodiscover_modules
|
||||||
|
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
|
@ -19,40 +21,29 @@ from tastypie.bundle import Bundle
|
||||||
from tastypie.exceptions import ApiFieldError
|
from tastypie.exceptions import ApiFieldError
|
||||||
from tastypie.fields import ApiField
|
from tastypie.fields import ApiField
|
||||||
|
|
||||||
|
|
||||||
_api_list = []
|
_api_list = []
|
||||||
|
|
||||||
for _app in settings.INSTALLED_APPS:
|
|
||||||
_module_dict = globals()
|
def populate_api_list():
|
||||||
if '.' in _app:
|
for app_config in django_apps.get_app_configs():
|
||||||
_root, _name = _app.split('.', 1)
|
_module_dict = globals()
|
||||||
if _root == 'ietf':
|
if '.' in app_config.name:
|
||||||
if not '.' in _name:
|
_root, _name = app_config.name.split('.', 1)
|
||||||
_api = Api(api_name=_name)
|
if _root == 'ietf':
|
||||||
_module_dict[_name] = _api
|
if not '.' in _name:
|
||||||
_api_list.append((_name, _api))
|
_api = Api(api_name=_name)
|
||||||
|
_module_dict[_name] = _api
|
||||||
|
_api_list.append((_name, _api))
|
||||||
|
|
||||||
def autodiscover():
|
def autodiscover():
|
||||||
"""
|
"""
|
||||||
Auto-discover INSTALLED_APPS resources.py modules and fail silently when
|
Auto-discover INSTALLED_APPS resources.py modules and fail silently when
|
||||||
not present. This forces an import on them to register any admin bits they
|
not present. This forces an import on them to register any resources they
|
||||||
may want.
|
may want.
|
||||||
"""
|
"""
|
||||||
|
autodiscover_modules("resources")
|
||||||
|
|
||||||
from importlib import import_module
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.module_loading import module_has_submodule
|
|
||||||
|
|
||||||
for app in settings.INSTALLED_APPS:
|
|
||||||
mod = import_module(app)
|
|
||||||
# Attempt to import the app's admin module.
|
|
||||||
try:
|
|
||||||
import_module('%s.resources' % (app, ))
|
|
||||||
except:
|
|
||||||
# Decide whether to bubble up this error. If the app just
|
|
||||||
# doesn't have an admin module, we can ignore the error
|
|
||||||
# attempting to import it, otherwise we want it to bubble up.
|
|
||||||
if module_has_submodule(mod, "resources"):
|
|
||||||
raise
|
|
||||||
|
|
||||||
class ModelResource(tastypie.resources.ModelResource):
|
class ModelResource(tastypie.resources.ModelResource):
|
||||||
def generate_cache_key(self, *args, **kwargs):
|
def generate_cache_key(self, *args, **kwargs):
|
||||||
|
|
|
@ -30,4 +30,5 @@ class Serializer(): ...
|
||||||
class ToOneField(tastypie.fields.ToOneField): ...
|
class ToOneField(tastypie.fields.ToOneField): ...
|
||||||
class TimedeltaField(tastypie.fields.ApiField): ...
|
class TimedeltaField(tastypie.fields.ApiField): ...
|
||||||
|
|
||||||
|
def populate_api_list() -> None: ...
|
||||||
def autodiscover() -> None: ...
|
def autodiscover() -> None: ...
|
||||||
|
|
15
ietf/api/apps.py
Normal file
15
ietf/api/apps.py
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
from . import populate_api_list
|
||||||
|
|
||||||
|
|
||||||
|
class ApiConfig(AppConfig):
|
||||||
|
name = "ietf.api"
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
"""Hook to do init after the app registry is fully populated
|
||||||
|
|
||||||
|
Importing models or accessing the app registry is ok here, but do not
|
||||||
|
interact with the database. See
|
||||||
|
https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready
|
||||||
|
"""
|
||||||
|
populate_api_list()
|
|
@ -11,6 +11,7 @@ from ietf.meeting import views as meeting_views
|
||||||
from ietf.submit import views as submit_views
|
from ietf.submit import views as submit_views
|
||||||
from ietf.utils.urls import url
|
from ietf.utils.urls import url
|
||||||
|
|
||||||
|
|
||||||
api.autodiscover()
|
api.autodiscover()
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -436,7 +436,7 @@ STATICFILES_DIRS = (
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
# Django apps
|
# Django apps
|
||||||
'django.contrib.admin',
|
'ietf.admin', # replaces django.contrib.admin
|
||||||
'django.contrib.admindocs',
|
'django.contrib.admindocs',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
'django.contrib.contenttypes',
|
'django.contrib.contenttypes',
|
||||||
|
|
27
ietf/templates/admin/base.html
Normal file
27
ietf/templates/admin/base.html
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{% extends 'admin/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% block extrastyle %}{{ block.super }}
|
||||||
|
{% if server_mode and server_mode != "production" %}
|
||||||
|
<style>
|
||||||
|
{# grab colors that match bootstrap so we don't have to import the css #}
|
||||||
|
html, :root{
|
||||||
|
--bs-danger-bg-subtle: #F8D7DAFF;
|
||||||
|
--bs-danger-text-emphasis: #58151CFF;
|
||||||
|
--bs-danger: #DC3545FF;
|
||||||
|
--bs-secondary: #6C757DFF;
|
||||||
|
--bs-primary-text-emphasis: #052C65FF;
|
||||||
|
}
|
||||||
|
html[data-theme="light"], :root {
|
||||||
|
--primary: var(--bs-danger-bg-subtle);
|
||||||
|
--secondary: var(--bs-danger-bg-subtle);
|
||||||
|
--accent: var(--bs-danger-text-emphasis);
|
||||||
|
--primary-fg: var(--bs-primary-text-emphasis);
|
||||||
|
--link-fg: var(--bs-danger-text-emphasis);
|
||||||
|
--header-color: var(--bs-secondary);
|
||||||
|
--breadcrumbs-fg: var(--bs-secondary);
|
||||||
|
--breadcrumbs-link-fg: var(--link-fg);
|
||||||
|
}
|
||||||
|
span.text-danger { color: var(--bs-danger); }
|
||||||
|
</style>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -20,8 +20,6 @@ from ietf.liaisons.sitemaps import LiaisonMap
|
||||||
from ietf.utils.urls import url
|
from ietf.utils.urls import url
|
||||||
|
|
||||||
|
|
||||||
admin.autodiscover()
|
|
||||||
|
|
||||||
# sometimes, this code gets called more than once, which is an
|
# sometimes, this code gets called more than once, which is an
|
||||||
# that seems impossible to work around.
|
# that seems impossible to work around.
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -36,6 +36,7 @@ from django.urls import reverse as urlreverse
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
|
from ietf.admin.sites import AdminSite
|
||||||
from ietf.person.name import name_parts, unidecode_name
|
from ietf.person.name import name_parts, unidecode_name
|
||||||
from ietf.submit.tests import submission_file
|
from ietf.submit.tests import submission_file
|
||||||
from ietf.utils.draft import PlaintextDraft, getmeta
|
from ietf.utils.draft import PlaintextDraft, getmeta
|
||||||
|
@ -325,7 +326,7 @@ class AdminTestCase(TestCase):
|
||||||
User.objects.create_superuser('admin', 'admin@example.org', 'admin+password')
|
User.objects.create_superuser('admin', 'admin@example.org', 'admin+password')
|
||||||
self.client.login(username='admin', password='admin+password')
|
self.client.login(username='admin', password='admin+password')
|
||||||
rtop = self.client.get("/admin/")
|
rtop = self.client.get("/admin/")
|
||||||
self.assertContains(rtop, 'Django administration')
|
self.assertContains(rtop, AdminSite.site_header())
|
||||||
for name in self.apps:
|
for name in self.apps:
|
||||||
app_name = self.apps[name]
|
app_name = self.apps[name]
|
||||||
self.assertContains(rtop, name)
|
self.assertContains(rtop, name)
|
||||||
|
|
Loading…
Reference in a new issue