一 、 前言 javascript
Django 提供了admin 组件 为项目提供基本的管理后台功能(对数据表的增删改查)。html
本篇文章经过 admin源码 简单分析admin 内部原理 ,扩展使用方式,为之后进行定制和本身开发组件作铺垫。java
2、 简单使用python
1.在app 目录下的admin.py 中经过注册表django
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/
3、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 #历史记录
4、 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,修改密码,登陆登出,主页。
里面包含了许多编程思想和方法,值得继续深刻研究。
5、总结
在这篇文章中,经过基本使用,分析了admin组件第一步相关的sites源码,理解了site 这个对象的构造方式和包含方法。