您现在的位置是:网站首页>文章详情文章详情

xadmin的插件开发思路及流程

inlike2019-06-26 原创文章 浏览(2164) 评论(0) 喜欢(40)

简介xadmin是对Django自带后台管理的扩展,使用xadmin可以实现丰富多彩的后台管理样式,但是要实现更大的个性化就需要对xadmin进行插件开发,插件是对xadmin的扩展,这也是xadmin强大的原因之一

xadmin是对Django自带后台管理的扩展,使用xadmin可以实现丰富多彩的后台管理样式,但是要实现更大的个性化就需要对xadmin进行插件开发,插件是对xadmin的扩展,这也是xadmin强大的原因之一。

在做本博客的时候,需要实现封面多图选择,并且还可以从本地上传和网络图库选择的功能,那么xadmin的功能就不在适用于这个需求,因此需要自己单独开发,在这个过程中查询了一些资料,但是都没讲清楚和看明白,自己就慢慢摸索和修改,最后实现了该需求,本篇文章就是记录插件开发的过程。

ce9467d04b4aa17380be0b4c2e66a23026.png

在开发中主要接触了两类插件,一类是实现了模板页面中的预定义的埋点,另一种是更改表单样式的插件;对于这两种目前还未深入,空了需要读读源码,这里先记录一下实现思路,有了新的认识会不断更新本文章。

要做xadmin插件开发,我们还是需要先看看源码,梳理一下脉络:

插件类继承 BaseAdminPlugin, BaseAdminPlugin源码如下:

class BaseAdminPlugin(BaseAdminObject):

    def __init__(self, admin_view):
        self.admin_view = admin_view
        self.admin_site = admin_view.admin_site

        if hasattr(admin_view, 'model'):
            self.model = admin_view.model
            self.opts = admin_view.model._meta

    def init_request(self, *args, **kwargs):
        pass

而BaseAdminPlugin继承于BaseAdminObject,其源码如下:


class BaseAdminObject(object):

    def get_view(self, view_class, option_class=None, *args, **kwargs): # 获取AdminView的实例
        opts = kwargs.pop('opts', {})
        return self.admin_site.get_view_class(view_class, option_class, **opts)(self.request, *args, **kwargs)

    def get_model_view(self, view_class, model, *args, **kwargs): # 获取ModelAdminViewClass的实例
        return self.get_view(view_class, self.admin_site._registry.get(model), *args, **kwargs)

    def get_admin_url(self, name, *args, **kwargs):
        return reverse('%s:%s' % (self.admin_site.app_name, name), args=args, kwargs=kwargs)

    def get_model_url(self, model, name, *args, **kwargs):
        return reverse(
            '%s:%s_%s_%s' % (self.admin_site.app_name, model._meta.app_label,
                             model._meta.model_name, name),
            args=args, kwargs=kwargs, current_app=self.admin_site.name)

    def get_model_perm(self, model, name):
        return '%s.%s_%s' % (model._meta.app_label, name, model._meta.model_name)

    def has_model_perm(self, model, name, user=None):
        user = user or self.user
        return user.has_perm(self.get_model_perm(model, name)) or (name == 'view' and self.has_model_perm(model, 'change', user))

    def get_query_string(self, new_params=None, remove=None):
        if new_params is None:
            new_params = {}
        if remove is None:
            remove = []
        p = dict(self.request.GET.items()).copy()
        arr_keys = list(p.keys())
        for r in remove:
            for k in arr_keys:
                if k.startswith(r):
                    del p[k]
        for k, v in new_params.items():
            if v is None:
                if k in p:
                    del p[k]
            else:
                p[k] = v
        return '?%s' % urlencode(p)

    def get_form_params(self, new_params=None, remove=None):
        if new_params is None:
            new_params = {}
        if remove is None:
            remove = []
        p = dict(self.request.GET.items()).copy()
        arr_keys = list(p.keys())
        for r in remove:
            for k in arr_keys:
                if k.startswith(r):
                    del p[k]
        for k, v in new_params.items():
            if v is None:
                if k in p:
                    del p[k]
            else:
                p[k] = v
        return mark_safe(''.join(
            '<input type="hidden" name="%s" value="%s"/>' % (k, v) for k, v in p.items() if v))

    def render_response(self, content, response_type='json'):
        if response_type == 'json':
            response = HttpResponse(content_type="application/json; charset=UTF-8")
            response.write(
                json.dumps(content, cls=JSONEncoder, ensure_ascii=False))
            return response
        return HttpResponse(content)

    def template_response(self, template, context):
        return TemplateResponse(self.request, template, context)

    def message_user(self, message, level='info'):
        """
        Send a message to the user. The default implementation
        posts a message using the django.contrib.messages backend.
        "
""
        if hasattr(messages, level) and callable(getattr(messages, level)):
            getattr(messages, level)(self.request, message)

    def static(self, path):
        return static(path)

    def vendor(self, *tags):
        return vendor(*tags)

    def log(self, flag, message, obj=None):
        log = Log(
            user=self.user,
            ip_addr=self.request.META['REMOTE_ADDR'],
            action_flag=flag,
            message=message
        )
        if obj:
            log.content_type = get_content_type_for_model(obj)
            log.object_id = obj.pk
            log.object_repr = force_text(obj)
        log.save()

上面的方法主要重点关注:init_request方法,该方法是用来控制插件是否开启的方法,返回布尔值。

class HelloWorldPlugin(BaseAdminPlugin):
    say_hello = False
    # 初始化方法根据 ``say_hello`` 属性值返回
    def init_request(self, *args, **kwargs):
        return bool(self.say_hello)

如果我们在视图类中增加一个属性say_hello = True 那么该HelloWorldPlugin插件将启用。除了init_request方法之外,其余方法是用来获取模型或者字段相关属性和参数的方法,在插件开发中看实际情况使用。

接下是插件插件发挥核心功能的地方,是对AdminView类实现的拦截和修改,AdminView类是负责admin后台的渲染,属于AdminView对象的类有很多,如:

BaseAdminView:所有AdminView类的基类

CommAdminView:扩展BaseAdminView的通用AdminView类,Xadmin网站的全局设置可以通过这个类

ModelAdminView:基于模型的AdminView类,AdminSite实例将自动为每个注册的Model类扩展ModelAdminView的类创建一个url,通过调用AdminSite实例的register_modelview方法完成注册

ListAdminView:这是数据列表AdminView类,它实现了数据排序和分页功能

ModelFormAdminView:此AdminView类是一个基类,用于根据模型表单创建或更新数据,它提供了通过表单显示和更新数据等功能,类'CreateAdminView'和'UpdateAdminView'继承自此类

CreateAdminView:此模型AdminView用于创建继承自ModelFormAdminView类的模型对象

UpdateAdminView:此Model AdminView类用于更新模型对象,该对象继承自ModelFormAdminView类

DeleteAdminView:Model AdminView用于删除模型对象

DetailAdminView:模式AdminView类,显示模型的详细信息,视图页面仅适用于视图数据,视图布局与类相同:`xadmin.views.edit.ModelFormAdminView

不同的AdminView对象实现了admin后台的渲染,这些对象实现了一系列的方法,其中被@filter_hook装饰的方法就是我们可以拦截的方法。

98ad76107fbcd02533431530d041d69612.png

举一个列子:要实现封面图的选择,那么我就应该修改表单视图类,这个类是ModelFormAdminView负责创建表单样式,该类内部有一个方法:

@filter_hook
get_field_style(* args,** kwargs )

根据“字段样式”返回“表单字段”属性。此方法可以通过插件过滤掉并提供不同的样式。

参数:    
db_field - 模型的DB字段
style - 配置的Field Style,该值来自于属性style_fields

其中get_field_style就是我们需要复写的方法,复写如下:

class Test(BaseAdminPlugin):
    def init_request(self, *args, **kwargs):
        return True

    def get_field_style(self, attrs, db_field, style, **kwargs):
        if style == 'editcrover':
            attrs = dict()
            attrs['widget'] = TelInput()
        return attrs

当xadminview中指定了字段对应的样式是editcrover时,该插件就发生作用attrs['widget'] = TelInput(),用自定义的widget样式替换原来的widget,因此在编辑页面可以呈现下面的效果(多图选择):

image.png

TelInput类是Widget类的实现,Widget类内部又是一些列方法,其中关键的是:属性字段template_name、类方法render;前者用于指定需要渲染的模板名,后者用来实现渲染的逻辑。

class TelInput(Input):
    input_type = 'text'
    template_name = "choicecrover.html"

    def render(self, name, value, attrs=None, renderer=None):
        context = self.get_context(name, value, attrs)
        template = loader.get_template(self.template_name).render(context)
        return mark_safe(template)

self.get_context()返回该控件的属性,如控件值、控件名、空间类型等字典属性,然后将该字典用于渲染模板中的HTML源码,最后就得到了控件,HTML中模板源码如下:

<div class="article-cover-images" data-toggle="modal" data-target="#myModal"
     onclick="choosecover()">
    {% if widget.value == None %}
        <div class="article-cover-add"><i type="add" class="iconfont icon-add ">&#10010</i>
        </div>
        <div class="article-cover-add"><i type="add" class="iconfont icon-add ">&#10010</i>
        </div>
        <div class="article-cover-add">
        <i type="add" class="iconfont icon-add ">&#10010</i>
    {% else %}
        {% for url in widget.value|strip:';' %}
            <div class="article-cover-img-wrap">
                <img alt="cover" src="{{ url }}">
                <div data-uri="" data-key="1"
                     class="article-cover-img-modify"></div>
            </div>
        {% endfor %}
    {% endif %}

    </div>
</div>

<input type="hidden" name="{{ widget.name }}"
        {% if widget.value != None %}
       value="{{ widget.value|stringformat:'s' }}">
        {% endif %}
<br>
<span id="code-msg" style="margin-left:10px;"></span>

通过隐藏真实的控件,增加一个含有显示的控件,并通过该控件选择图片,然后把值通过js给input,最后提交的时候就实现了保存多张封面的效果。

说明:封面的模型字段是长文本型,多张封面通过字符分号来连接。

上面是实现表单类样式的控件,处理思路是:实现插件基类——查找页面生成的对象类——复写原AdminView中的被@filter_hook装饰的类方法——实现插件功能。

然后说第二种插件:模板插件

该插件是通过预先在xadmin的HTML模板中预先定义块,然后我们在插件中通过:block_ + 插入点名称(预定义块名)的方式定义方法,该方法的返回结果将被指定填充到快中,从而实现插件功能。

xadmin模板中的change_list.html模板:

{% load xadmin %}
...
<form id="changelist-form" action="" method="post"{% view_block 'result_list_form' %}>{% csrf_token %}
  {% view_block 'results_top' %}
  <div class="results">
    {% if results %}
    ...

实现其中的view_block填充:

class HelloWorldPlugin(BaseAdminPlugin):

    # context 即为 TemplateContext, nodes 参数包含了其他插件的返回内容。
    # 您可以直接返回 HTML 片段,或是将内容加入到 nodes 参数中
    def block_results_top(self, context, nodes):
        return s"<div class='info'>Hello %s</div>" % context['hello_target']

至此,xadmin的插件开发的思路和流程就说完了,其中还有很多细节未讲解,需要多看官方文档;实际开发的时候可以参考xadmin的内部插件images,这个插件实现的是详情页内显示图片字段的缩略图。

更多内容欢迎关注微信公众号:Python之战

image.png


很赞哦! ( 40)
    《Python实战进阶》
    None
    None
    夏至已深

站点信息

  • 建站时间:2019-5-24
  • 网站程序:like in love
  • 主题模板《今夕何夕》
  • 文章统计:104条
  • 文章评论:***条
  • 微信公众号:扫描二维码,关注我们
  • 个人微信公众号