一 、 前言 Django 提供了admin 組件 為項目提供基本的管理後臺功能(對數據表的增刪改查)。 本篇文章通過 admin源碼 簡單分析admin 內部原理 ,擴展使用方式,為以後進行定製和自己開發組件做鋪墊。 二、 簡單使用 1.在app 目錄下的admin.py 中通過註冊表 2. 創建 ...
一 、 前言
Django 提供了admin 組件 為項目提供基本的管理後臺功能(對數據表的增刪改查)。
本篇文章通過 admin源碼 簡單分析admin 內部原理 ,擴展使用方式,為以後進行定製和自己開發組件做鋪墊。
二、 簡單使用
1.在app 目錄下的admin.py 中通過註冊表
from django.contrib import admin from blog01.models import * admin.site.register([UserInfo,User,Blog]) # 或者通過 @admin.register 裝飾器實現
2. 創建root用戶
python manage.py createsuperuser #輸入用戶名 #輸入密碼 #再次輸入密碼
3. 登錄admin後臺進行管理
瀏覽器訪問 http://127.0.0.1/admin/
三、admin簡單分析
1. admin 是一個Django 提供的後臺管理app,功能也比較強大,在敏捷開發的過程中可以考慮直接使用。
但是面對複雜的業務情況,要實現更高的定製,必然要求我們實現自己的admin組件,這樣面對各種情況我們才能游刃有餘。
2. admin 是通過”註冊“類自動生成url,執行對應的視圖函數,提供友好可視化界面,實現增刪改查功能。
3. admin 內部 url 列表
url(r'^$', wrap(self.index), name='index'),
url(r'^login/$', self.login, name='login'),
url(r'^logout/$', wrap(self.logout), name='logout'),
url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'),
url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True),name='password_change_done'),
url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', wrap(contenttype_views.shortcut),name='view_on_site'),#將我們表格生成url
4. 註冊類生成的url
127.0.0.1/admin/appname/classname/ #查看數據
127.0.0.1/admin/appname/classname/add #增加數據
127.0.0.1/admin/appname/classname/id/delete #刪除數據
127.0.0.1/admin/appname/classname/id/change #更新數據
127.0.0.1/admin/appname/classname/id/history #歷史記錄
四、 admin 流程分析之sites.py 分析
1.從目錄開始
下圖是django.contrib.admin 目錄。可以看見熟悉的static,templates,views,migrations目錄,說明admin 是一個app。
2. 從 admin.site.register( model_or_iterable, admin_class=None,) 分析
admin 是什麼?
是一個後臺管理app
site 是什麼?
點開發現是來自sites.py 中的一個實例,代表當前admin站點,也就是通過模塊導入方式實現的單例模式。下麵為site.py 中源碼,後續如不說明,均為admin源碼材料。
# This global object represents the default admin site, for the common case. # You can instantiate AdminSite in your own code to create a custom admin site. # 這個全局對象代表了在一般情況下的預設admin 站點 # 你可以在你自己的代碼中實例化AdminSite來創造一個自定義的admin 站點 site = AdminSite()
register 是什麼?
是site的一個方法,也就是site的類AdminSite的一個方法,
def register(self, model_or_iterable, admin_class=None, **options): '''Registers the given model(s) with the given admin class. The model(s) should be Model classes, not instances. If an admin class isn't given, it will use ModelAdmin (the default admin options). If keyword arguments are given -- e.g., list_display -- they'll be applied as options to the admin class. If a model is already registered, this will raise AlreadyRegistered. If a model is abstract, this will raise ImproperlyConfigured.''' '''用提供的admin 類給model(我們的表格)註冊,必須給Model類,而不是實例 如果沒有指定admin類,會用預設的ModelAdmin,如果給了關鍵詞參數,如list_display,他們會被作為選項應用在admin類中 如果一個model 已經被註冊了,會報AlreadyRegistered異常 如果一個model是抽象的,這會引起ImproperlyConfigured異常。'''
小結:
所以我們做的事是將 代表我們表格的類 傳給 site.py中 AdminSite類 實例化的site對象 的register 方法 進行註冊,預設是 用 ModelAdmin 管理 。
site 對象就是生成的admin 站點。
3. 進入 sites.py
顧名思義是生成站點的文件,一共兩個對象,三個類
第一個對象是”弱集合“,
第二個對象是我們需要的站點
第一個類是已經註冊的異常,繼承了Exception,第二個類是沒有註冊的異常,同樣繼承了Exception,無內容,兩個用來拋異常的類。
第三個是重點關註,生成站點的類AdminSite。
class AdminSite(object): """ An AdminSite object encapsulates an instance of the Django admin application, ready to be hooked in to your URLconf. Models are registered with the AdminSite using the register() method, and the get_urls() method can then be used to access Django view functions that present a full admin interface for the collection of registered models.
一個AdminSite對象封裝了Django管理應用程式的一個實例,準備被掛鉤到你的URLconf。 使用register()方法向AdminSite註冊模型, 然後可以使用get_urls()方法訪問為註冊模型集合提供完整管理界面的Django視圖函數。 """ # Text to put at the end of each page's <title>. # 放在每頁<title>的文本 site_title = ugettext_lazy('Django site admin') # Text to put in each page's <h1>. # 放在每頁<h1>的文本 site_header = ugettext_lazy('Django administration') # Text to put at the top of the admin index page. # 放在admin 主頁頂部的文本 index_title = ugettext_lazy('Site administration') # URL for the "View site" link at the top of each admin page. # 根url site_url = '/' _empty_value_display = '-' login_form = None index_template = None app_index_template = None login_template = None logout_template = None password_change_template = None password_change_done_template = None
下麵來看 AdminSite 的 25 個方法和相關內容
def __init__(self, name='admin'): self._registry = {} # model_class class -> admin_class instance 將model_class類轉為admin_class實例,也就是我們的表放的地方 self.name = name # 站點名 self._actions = {'delete_selected': actions.delete_selected} # 預設行為,刪除選中,在actions.py 中只有這一個方法 self._global_actions = self._actions.copy() # 全局行為,複製預設行為 all_sites.add(self) # 將實例加入all_sites 這個’弱集合’
解釋: 初始化一些變數,一些方法如 delete_selected,暫時不討論內部如何實現。
def check(self, app_configs): """ Run the system checks on all ModelAdmins, except if they aren't customized at all.
如果沒有自定義,就對所有ModelAdmins進行系統檢查 """ if app_configs is None: app_configs = apps.get_app_configs() # 沒有傳配置,就去apps對象中拿配置信息 app_configs = set(app_configs) # Speed up lookups below 加速下麵查找(去重) errors = [] modeladmins = (o for o in self._registry.values() if o.__class__ is not ModelAdmin)#生成器加遞歸檢查,將不是ModelAdmin的對象放入erros列表 for modeladmin in modeladmins: if modeladmin.model._meta.app_config in app_configs: errors.extend(modeladmin.check()) return errors
解釋:apps 是django.apps.register.py 中 Apps 類實例的一個對象,存儲已安裝應用程式配置的註冊表。它也跟蹤模型,例如。 提供反向關係。後續有時間研究。
這個方法主要拿到配置信息和錯誤對象。
def register(self, model_or_iterable, admin_class=None, **options): """ Registers the given model(s) with the given admin class. 用提供的admin 類 註冊給的表 model The model(s) should be Model classes, not instances. 必須給Model類,而不是實例 If an admin class isn't given, it will use ModelAdmin (the default admin options). If keyword arguments are given -- e.g., list_display -- they'll be applied as options to the admin class. 如果沒有指定admin類,會用預設的ModelAdmin,如果給了關鍵詞參數,如list_display, 他們會被作為選項應用在admin類中 If a model is already registered, this will raise AlreadyRegistered. 如果一個model 已經被註冊了,會報AlreadyRegistered異常 If a model is abstract, this will raise ImproperlyConfigured. 如果一個model是抽象的,這會引起ImproperlyConfigured異常。 """ if not admin_class: admin_class = ModelAdmin # 如果沒指定,就用ModelAdmin if isinstance(model_or_iterable, ModelBase): # 如果輸入的是一個代表表格的類,就把它變成列表,所以能傳類或者列表,ModelBase是Model的元類 model_or_iterable = [model_or_iterable] for model in model_or_iterable: # 判斷列表中每個類是不是抽象類,如果是,拋出異常,背後比較複雜,在ModelBase中實現,有空研究 if model._meta.abstract: raise ImproperlyConfigured( 'The model %s is abstract, so it cannot be registered with admin.' % model.__name__ ) if model in self._registry: raise AlreadyRegistered('The model %s is already registered' % model.__name__) #如果已經註冊,拋出異常 # Ignore the registration if the model has been # swapped out. if not model._meta.swapped: #如果沒有被 swapped,繼續,同樣在ModelBase 中屬性,不太明白 # If we got **options then dynamically construct a subclass of #生成自定義配置 # admin_class with those **options. if options: # For reasons I don't quite understand, without a __module__ # 作者也不知道為什麼,就是要加__model__屬性 # the created class appears to "live" in the wrong place, # which causes issues later on. options['__module__'] = __name__ admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) # 用type函數將自定義屬性添加到預設的ModelAdmin 中,生成新的類 # Instantiate the admin class to save in the registry # 將表格的類作為鍵,將ModelAdmin或自定義後的ModelAdmin 用 該類和site實例 生成的 self._registry[model] = admin_class(model, self) # 作為鍵值
解釋:1. 該函數目的是將我們的表格和管理的類結合一一對應下來,
2. ype函數有兩種用法:
type(object) -> the object's type
type(name, bases, dict) -> a new type
3. **options 是可擴展的功能,在admin 的options.py 中有詳細列出,之後在高級定製中討論。
def unregister(self, model_or_iterable): """ Unregisters the given model(s). If a model isn't already registered, this will raise NotRegistered. """ if isinstance(model_or_iterable, ModelBase): model_or_iterable = [model_or_iterable] for model in model_or_iterable: if model not in self._registry: raise NotRegistered('The model %s is not registered' % model.__name__) del self._registry[model]
def is_registered(self, model): """ Check if a model class is registered with this `AdminSite`. """ return model in self._registry
解釋: 取消註冊和判斷是否註冊,本質就是判斷對象是否在我們生成的字典中
def add_action(self, action, name=None): """ Register an action to be available globally.
註冊新的操作 """ name = name or action.__name__ self._actions[name] = action self._global_actions[name] = action def disable_action(self, name): """ Disable a globally-registered action. Raises KeyError for invalid names.
刪除已有操作 """ del self._actions[name] def get_action(self, name): """ Explicitly get a registered global action whether it's enabled or not. Raises KeyError for invalid names.
返回全局操作,無論是否運行 , """ return self._global_actions[name] @property def actions(self): """ Get all the enabled actions as an iterable of (name, func).
獲得所有運行的操作組成的可迭代的元組,如(name,func),property裝飾器將方法變為屬性調用 """ return six.iteritems(self._actions)
解釋:1. 操作增刪改查的行為,預設是刪除選中這一種,
2. six.iteritems 目的, 相容py2實現 將目標字典轉為 迭代器
@property def empty_value_display(self): return self._empty_value_display @empty_value_display.setter def empty_value_display(self, empty_value_display): self._empty_value_display = empty_value_display
解釋:預設空值顯示 ’-‘, 可以自定義空值符號,調用property的setter方法實現
def has_permission(self, request): """ Returns True if the given HttpRequest has permission to view #檢查登錄許可權 *at least one* page in the admin site. """ return request.user.is_active and request.user.is_staff def admin_view(self, view, cacheable=False): """ Decorator to create an admin view attached to this ``AdminSite``. This wraps the view and provides permission checking by calling ``self.has_permission``. You'll want to use this from within ``AdminSite.get_urls()``: class MyAdminSite(AdminSite): def get_urls(self): from django.conf.urls import url urls = super(MyAdminSite, self).get_urls() urls += [ url(r'^my_view/$', self.admin_view(some_view)) ] return urls By default, admin_views are marked non-cacheable using the ``never_cache`` decorator. If the view can be safely cached, set cacheable=True.
用來創造添在這個"AdminSite"的視圖函數的裝飾器,其中調用 self.has_permission 檢查許可權,
我們也可以用此函數來自定義我們需要在admin後臺出現的視圖
預設是不緩存,如果確認是安全緩存的,就設置 cacheable = False """ def inner(request, *args, **kwargs): if not self.has_permission(request): #如果沒有許可權, if request.path == reverse('admin:logout', current_app=self.name): #如果為登出,就轉到首頁 index_path = reverse('admin:index', current_app=self.name) return HttpResponseRedirect(index_path) # Inner import to prevent django.contrib.admin (app) from # 在此處導入而不是開頭是因為要防止從無關的用戶認證組件導入 # importing django.contrib.auth.models.User (unrelated model). from django.contrib.auth.views import redirect_to_login return redirect_to_login( request.get_full_path(), reverse('admin:login', current_app=self.name) #記錄想去的頁面之後,跳轉登錄頁面,登錄成功進入想去頁面 ) return view(request, *args, **kwargs) if not cacheable: inner = never_cache(inner) # 通過 never_cache 閉包函數在request上加header 設置不緩存 # We add csrf_protect here so this function can be used as a utility # function for any view, without having to repeat 'csrf_protect'. if not getattr(view, 'csrf_exempt', False): # 如果沒有明確說 取消"csrf"機制,那就通過 csrf_poctect 閉包添加 inner = csrf_protect(inner) return update_wrapper(inner, view)
解釋:用來創立admin自己的視圖函數。
def get_urls(self): from django.conf.urls import url, include # Since this module gets imported in the application's root package, # it cannot import models from other applications at the module level, # and django.contrib.contenttypes.views imports ContentType.
"""
這個模塊在app 根包里導入了,它無法在其他app 里從模塊水平導入,
"""
from django.contrib.contenttypes import views as contenttype_views def wrap(view, cacheable=False): def wrapper(*args, **kwargs): return self.admin_view(view, cacheable)(*args, **kwargs) wrapper.admin_site = self return update_wrapper(wrapper, view) # Admin-site-wide views. urlpatterns = [ url(r'^$', wrap(self.index), name='index'), url(r'^login/$', self.login, name='login'), url(r'^logout/$', wrap(self.logout), name='logout'), url(r'^password_change/$', wrap(self.password_change, cacheable=True), name='password_change'), url(r'^password_change/done/$', wrap(self.password_change_done, cacheable=True), name='password_change_done'), url(r'^jsi18n/$', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'), url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', wrap(contenttype_views.shortcut), name='view_on_site'), ] # Add in each model's views, and create a list of valid URLS for the app_index
# 生成每一個表的視圖函數和url列表,appname/modelname/ 開頭,
valid_app_labels = [] for model, model_admin in self._registry.items(): urlpatterns += [ url(r'^%s/%s/' % (model._meta.app_label, model._meta.model_name), include(model_admin.urls)), ] if model._meta.app_label not in valid_app_labels: valid_app_labels.append(model._meta.app_label) # If there were ModelAdmins registered, we should have a list of app # labels for which we need to allow access to the app_index view,
# 如果有註冊的表,生成到顯示某個app內所有表格信息的頁面。
if valid_app_labels: regex = r'^(?P<app_label>' + '|'.join(valid_app_labels) + ')/$' urlpatterns += [ url(regex, wrap(self.app_index), name='app_list'), ] return urlpatterns
@property
def urls(self):
return self.get_urls(), 'admin', self.name
解釋:很明顯,這個函數是生成url的核心函數,url列表包括:固定的(login logout 等),根據註冊表拼接的(app名/表名/),還有某一app(app名/)
def each_context(self, request): """ Returns a dictionary of variables to put in the template context for *every* page in the admin site. For sites running on a subpath, use the SCRIPT_NAME value if site_url hasn't been customized.
返回一個每頁都有的變數組成的字典,在子路徑的頁面,如果沒有定製,就用SCRIPT_NAME 的值
""" script_name = request.META['SCRIPT_NAME'] site_url = script_name if self.site_url == '/' and script_name else self.site_url return { 'site_title': self.site_title, 'site_header': self.site_header, 'site_url': site_url, 'has_permission': self.has_permission(request), 'available_apps': self.get_app_list(request), }
解釋: 用來傳遞通用變數
def password_change(self, request, extra_context=None): """ Handles the "change password" task -- both form display and validation.
解決改密碼任務, 表單展示和驗證 """ from django.contrib.admin.forms import AdminPasswordChangeForm from django.contrib.auth.views import PasswordChangeView url = reverse('admin:password_change_done', current_app=self.name) defaults = { 'form_class': AdminPasswordChangeForm, 'success_url': url, 'extra_context': dict(self.each_context(request), **(extra_context or {})), } if self.password_change_template is not None: defaults['template_name'] = self.password_change_template request.current_app = self.name return PasswordChangeView.as_view(**defaults)(request) #as_view 完整性檢查 def password_change_done(self, request, extra_context=None): """ Displays the "success" page after a password change.
展示修改密碼成功界面 """ from django.contrib.auth.views import PasswordChangeDoneView defaults = { 'extra_context': dict(self.each_context(request), **(extra_context or {})), } if self.password_change_done_template is not None: defaults['template_name'] = self.password_change_done_template request.current_app = self.name return PasswordChangeDoneView.as_view(**defaults)(request) def i18n_javascript(self, request, extra_context=None): """ Displays the i18n JavaScript that the Django admin requires. `extra_context` is unused but present for consistency with the other admin views.
展示 Django admin 需要的多語言js
""" return JavaScriptCatalog.as_view(packages=['django.contrib.admin'])(request)
解釋: 邏輯一樣,先設置預設字典,有成功後url,當前表單,額外上下文變數(在預設中添加),模板名(預設或自定義),
傳入cbv的PasswordChangeView,實現修改密碼,等視圖函數
@never_cache def logout(self, request, extra_context=None): """ Logs out the user for the given HttpRequest. This should *not* assume the user is already logged in. """ from django.contrib.auth.views import LogoutView defaults = { 'extra_context': dict( self.each_context(request), # Since the user isn't logged out at this point, the value of # has_permission must be overridden. has_permission=False, **(extra_context or {}) ), } if self.logout_template is not None: defaults['template_name'] = self.logout_template request.current_app = self.name return LogoutView.as_view(**defaults)(request) @never_cache def login(self, request, extra_context=None): """ Displays the login form for the given HttpRequest. """ if request.method == 'GET' and self.has_permission(request): # Already logged-in, redirect to admin index index_path = reverse('admin:index', current_app=self.name) return HttpResponseRedirect(index_path) from django.contrib.auth.views import LoginView # Since this module gets imported in the application's root package, # it cannot import models from other applications at the module level, # and django.contrib.admin.forms eventually imports User. from django.contrib.admin.forms import AdminAuthenticationForm context = dict( self.each_context(request), title=_('Log in'), app_path=request.get_full_path(), username=request.user.get_username(), ) if (REDIRECT_FIELD_NAME not in request.GET and REDIRECT_FIELD_NAME not in request.POST): context[REDIRECT_FIELD_NAME] = reverse('admin:index', current_app=self.name) context.update(extra_context or {}) defaults = { 'extra_context': context, 'authentication_form': self.login_form or AdminAuthenticationForm, 'template_name': self.login_template or 'admin/login.html', } request.current_app = self.name return LoginView.as_view(**defaults)(request)
解釋:login logout 同上
def _build_app_dict(self, request, label=None): """ Builds the app dictionary. Takes an optional label parameters to filter models of a specific app. """ app_dict = {} if label: models = { m: m_a for m, m_a in self._registry.items() if m._meta.app_label == label } else: models = self._registry for model, model_admin in models.items(): app_label = model._meta.app_label has_module_perms = model_admin.has_module_permission(request) if not has_module_perms: continue perms = model_admin.get_model_perms(request) # Check whether user has any perm for this module. # If so, add the module to the model_list. if True not in perms.values(): continue info = (app_label, model._meta.model_name) model_dict = { 'name': capfirst(model._meta.verbose_name_plural), 'object_name': model._meta.object_name, 'perms': perms, } if perms.get('change'): try: model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name) except NoReverseMatch: pass if perms.get('add'): try: model_dict['add_url'] = reverse('admin:%s_%s_add' % info, current_app=self.name) except NoReverseMatch: pass if app_label in app_dict: app_dict[app_label]['models'].append(model_dict) else: app_dict[app_label] = { 'name': apps.get_app_config(app_label).verbose_name, 'app_label': app_label, 'app_url': reverse( 'admin:app_list', kwargs={'app_label': app_label}, current_app=self.name, ), 'has_module_perms': has_module_perms, 'models': [model_dict], } if label: return app_dict.get(label) return app_dict def get_app_list(self, request): """ Returns a sorted list of all the installed apps that have been registered in this site. """ app_dict = self._build_app_dict(request) # Sort the apps alphabetically. app_list = sorted(app_dict.values(), key=lambda x: x['name'].lower()) # Sort the models alphabetically within each app. for app in app_list: app['models'].sort(key=lambda x: x['name']) return app_list
解釋: 建立app的字典 在排序
@never_cache def index(self, request, extra_context=None): """ Displays the main admin index page, which lists all of the installed apps that have been registered in this site. """ app_list = self.get_app_list(request) context = dict( self.each_context(request), title=self.index_title, app_list=app_list, ) context.update(extra_context or {}) request.current_app = self.name return TemplateResponse(request, self.index_template or 'admin/index.html', context) def app_index(self, request, app_label, extra_context=None): app_dict = self._build_app_dict(request, app_label) if not app_dict: raise Http404('The requested admin page does not exist.') # Sort the models alphabetically within each app. app_dict['models'].sort(key=lambda x: x['name']) app_name = apps.get_app_config(app_label).verbose_name context = dict( self.each_context(request), title=_('%(app)s administration') % {'app': app_name}, app_list=[app_dict], app_label=app_label, ) context.update(extra_context or {}) request.current_app = self.name return TemplateResponse(request, self.app_index_template or [ 'admin/%s/app_index.html' % app_label, 'admin/app_index.html' ], context)
解釋: index 好理解,就是將之前的處理數據渲染主頁模板,app_index 就是 顯示所有app 的頁面
小結:25 種方法 實現了admin站點的基本功能和介面,有註冊方面,操作方面,預設空值符,生成url,修改密碼,登錄登出,主頁。
裡面包含了許多編程思想和方法,值得繼續深入研究。
五、總結
在這篇文章中,通過基本使用,分析了admin組件第一步相關的sites源碼,理解了site 這個對象的構造方式和包含方法。