Django+SimpleUI快速开发指南


适合创业团队的后台工具快速开发指南

print(u“人生苦短,我用Python”)

一、环境准备

1、创建新的django项目

2、安装simpleui

<code># 指定清华源安装simpleuipip install django-simpleui -i https://pypi.tuna.tsinghua.edu.cn/simple/<code>

3、修改django后台模块默认的模板

<code># 修改project的setting文件,在INSTALLED_APPS 首行引入simple应用 INSTALLED_APPS = [ 'simpleui', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', ... ]/<code>

4、字符集及时区设置

<code># 修改project的setting文件LANGUAGE_CODE = 'zh-hans'TIME_ZONE = 'Asia/Shanghai'USE_TZ = False # 这里务必调整为False,否则时区设置无效/<code>

5、静态资源及debug状态设置

<code># django在开发环境提供bug调试功能,同时该设置影响到runserver运行时的静态资源引入,故生产环境关闭debug开关的时候,保障静态资源的正确引入需要显性定义静态资源DEBUG = True# 定义静态资源位置 STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static"), ]# or 通过克隆方式将静态资源克隆到项目的静态资源目录,交由ngnix调用python3 manage.py collectstatic# 如果克隆报错提示找不到静态目录,请先在settings.py指定静态目录STATIC_ROOT = os.path.join(BASE_DIR, "static")# 由于开发过程中simpleui有限通过cdn进行页面渲染,一般情况下我们需要将其调整成本地资源方式,往往生产环境是无法连接外网的SIMPLEUI_STATIC_OFFLINE = True # 离线模式/<code>

6、数据库连接配置

<code># 这里以postgresql数据库为例,通常建议使用mysql数据库,多数据库连接不在此处讨论# postgresql## 安装postgresql驱动pip install psycopg2-binary#pip install psycopg2## 这里需要设置postgresql的schema信息,通过options字典指定配置DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'NAME' : 'moon', 'USER': 'postgres', 'PASSWORD': 'postgres', 'HOST':'127.0.0.1', 'PORT': '5432', 'OPTIONS': { 'options': '-c search_path=myapp' }, }}# mysql## 安装mysql驱动pip install pymysql## init文件引入pymysql 用于代理django认可的mysql驱动,同时由于django的版本问题对pymysql的版本存在要求需要增加一个版本欺骗动作import pymysqlpymysql.version_info = (1, 3, 13, "final", 0)pymysql.install_as_MySQLdb()## setting增加mysql连接串配置信息DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME':'jay_django_test', 'HOST':'127.0.0.1', 'USER':'root', 'PASSWORD':'qweasd', 'PORT':'3306', }}/<code>

7、admin模块进行数据库初始化

<code># 执行数据库迁移python manage.py migrate/<code>

8、创建admin模块超级管理员角色

<code>python manage.py createsuperuser/<code>

9、检查simple是否正确引入

<code># 浏览器访问 127.0.0.1:8000/admin 使用admin账户登录确认是否生效/<code>

二、创建APP

1、创建app

<code>python manage.py startapp myapp# 将app注册到projectINSTALLED_APPS = [ 'simpleui', 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'myapp', # 添加到末尾]# 说明:这里注册app是整个app的起点,包括后续涉及到数据迁移文件的生成都依赖于注册列表/<code>

2、创建model

<code># 在app的model创建测试用例的modelimport django.utils.timezone as timezone# Create your models here.class job_detail(models.Model): ## 定义枚举值状态 JOB_TYPE_STATUS_GP = '01' JOB_TYPE_STATUS_DEFAULT = '02' JOB_TYPE_STATUS_CHOICES = ( (JOB_TYPE_STATUS_GP, 'Greenplum函数'), (JOB_TYPE_STATUS_DEFAULT, '其他'), ) CREATED_BY = models.CharField('创建人',max_length=32) # 创建人 CREATED_TIME = models.DateTimeField('创建时间',default=timezone.now) # 创建时间 UPDATED_BY = models.CharField('更新人',max_length=32) # 更新人 UPDATED_TIME = models.DateTimeField('更新时间',default=timezone.now) # 更新时间 JOB_TYPE = models.CharField('任务类型',max_length=32, choices=JOB_TYPE_STATUS_CHOICES, default=JOB_TYPE_STATUS_DEFAULT ) # 任务类型 01:gp任务;02:datax任务;03:kafka推送任务;04:http请求任务 JOB_NAME = models.CharField('任务名称',max_length=128) # 任务名称 JOB_COMMENT = models.CharField('任务描述',max_length=128) # 任务描述 IN_PARA = models.CharField('输入参数定义',max_length=32) # 输入参数定义 参数使用英文逗号分隔 IN_PARA_COMMENT = models.CharField('输入参数描述',max_length=1024) # 输入参数描述 OUT_PARA = models.CharField('输出参数定义',max_length=32) # 输出参数定义 参数使用英文逗号分隔 OUT_PARA_COMMENT = models.CharField('输出参数描述',max_length=1024) # 输出参数描述 VERSION = models.CharField('版本号',max_length=32) # 版本号 IS_DELETE = models.CharField('是否删除',max_length=1, default='N') # 逻辑删除 Y:删除;N:正常 PRO_STATUS = models.CharField('发布状态',max_length=1,default='N') # 发布状态 Y:发布;N:开发# 生成数据库迁移文件,生产环境建议指定app进行迁移文件生成,避免未完善功能对已有正常版本的影响python manage.py makemigrations# 执行数据库迁移python manage.py migrate/<code>

3、配置路由

<code># 常规的django的web开发一般需要在各个app中创建urls文件,并且将urls路由引入到project的urls中进行统一管理,这里我们通过admin模块的路由注册功能,注册到admin的路由中# 应用的admin.py中进行model对象注册from myapp.models import job_detailadmin.site.register(job_detail)/<code>

三、页面布局

1、增删改查model测试

<code># 此时登录到页面上可以发现,刚刚创建的数据表已经作为一个类目在页面上展示了,但同时会发现一个问题,详情页面里面显示的并不是字段信息,而是具体的表名称,此处与我们设想的内容并不匹配。通过这一步骤我们成功验证了app的注册方式。具体展示内容,下面的步骤会详细介绍/<code>

2、admin首页布局

<code># 如上图所示,首页基础展示项目由三个部分组成,往往我们需要对首页进行定制重写,这里我们先把这几个展示模块先关闭# project的setting文件最后追加如下设置,即可关闭以上几个模块以及信息采集模块SIMPLEUI_HOME_INFO = FalseSIMPLEUI_HOME_QUICK = FalseSIMPLEUI_HOME_ACTION = FalseSIMPLEUI_ANALYSIS = False# SIMPLEUI_HOME_TITLE = '百度一下你就知道' # 【首页】按钮文案配置SIMPLEUI_HOME_PAGE = 'https://www.baidu.com' # 可用于嵌入其他链接,这里可以直接方便的嵌入报表链接SIMPLEUI_HOME_ICON = 'el el-icon-platform-eleme'# ICON 支持element-ui和fontawesome eg:fa fa-user# 图标ICON 样式查看https://element.eleme.cn/#/zh-CN/component/iconhttp://www.fontawesome.com.cn/cheatsheet/# 经过实际测试,饿了么的图标稍微有点小,而且数量少/<code>

接下来我们一起看下侧边栏设置,simpleui的侧边栏支持菜单排序,动态菜单等功能

<code># project的setting文件中增加如下配置,icon信息可以参考http://www.fontawesome.com.cn/cheatsheet/SIMPLEUI_CONFIG = { 'system_keep': False, # 关闭系统菜单 'menu_display': ['任务管理', '测试', '权限认证'], 'dynamic': True, # 设置是否开启动态菜单, 默认为False. 如果开启, 则会在每次用户登陆时动态展示菜单内容 'menus': [{ 'app': 'myapp', 'name': '任务管理', 'icon': 'fas fa-user-shield', 'models': [{ 'name': '任务管理1', 'icon': 'fa fa-user', 'url': 'job_detail/' }] },{ 'app': 'auth', 'name': '权限认证', 'icon': 'fas fa-user-shield', 'models': [{ 'name': '用户', 'icon': 'fa fa-user', 'url': 'auth/user/' }] }, { 'name': '测试', 'icon': 'fa fa-file', 'models': [{ 'name': 'Baidu', 'url': 'http://baidu.com', 'icon': 'far fa-surprise' }, { 'name': '内网穿透', 'url': 'https://www.wezoz.com', 'icon': 'fab fa-github' }] }]}/<code>

如图所示,新配置的侧边栏已经生效,但点击我们自己创建的app的详情页面提示404错误,从异常信息得知,我们在路由配置上存在问题,系统未能找到我们的路由信息

<code># 问题配置'url': 'job_detail/'# 正确配置'url': 'myapp/job_detail/'# 这里大家可以思考下,url的配置与常规的urls.py文件的路由显性引入的异同点/<code>

3、页面标题及侧边栏标题定制

<code># 应用的admin.py文件中admin.site.site_header = '数据中心'admin.site.site_title = 'XXX数据中心'admin.site.index_title = u'XXXX数据中心'/<code>

目前看index_title在simpleui中无效,检查过simpleui的template文件确实没使用到这个参数

4、列表查询页面定制

<code># admin文件中定义展示页面,后续方便页面管理,可以独立文件,通过import方式引入进行注册from myapp.models import job_detail@admin.register(job_detail)class job_detail(admin.ModelAdmin): # 设置页面可以展示的字段 list_display = ('JOB_TYPE', 'JOB_NAME','JOB_COMMENT', 'IN_PARA','IN_PARA_COMMENT', 'OUT_PARA','OUT_PARA_COMMENT') # 默认不配置的话,第一个字段会存在链接到记录编辑页面 # list_display_links = None list_display_links = ('JOB_NAME',) # 设置过滤选项 list_filter = ('JOB_TYPE', 'CREATED_TIME',) # 每页显示条目数 缺省值100 list_per_page = 1 # show all页面上的model数目,缺省200 # list_max_show_all = 200 # 设置可编辑字段 如果设置了可以编辑字段,页面会自动增加保存按钮 list_editable = ('IN_PARA_COMMENT',) # 按日期月份筛选 该属性一般不用 # date_hierarchy = 'CREATED_TIME' # 按发布日期降序排序 ordering = ('-CREATED_TIME',) # 搜索条件设置 search_fields = ('JOB_NAME',) # 表头字段显示中文名称,这里需要修改models文件,在定义字段的时候增加别名 # eg1:JOB_NAME = models.CharField('任务名称',max_length=128) # eg2: name = models.CharField(max_length=30,verbose_name=u"姓名") # 字段关联展示 ## 场景1、关联其他表的数据展示,此处外键展示不做演示,生产环境尽量减少外键使用 ## 场景2、枚举信息转义展示 ### 此处需要在model定义页面通过枚举值转义配置对应展示中文信息,参考model模块代码设置 """ 这种禁用编辑链接的放法只是不让它在页面中显示,即把超链接去掉了, 但是还是可以通过手动输入url的方式来进入编辑页面。 不过可以配合设置fieldsets或者readonly_fieldss来达到目的 注意:这里建议删除按钮要禁用掉,否则只有拥有view权限的人员依然可以进行删除动作,或者需要进行人员角色判断 """ def has_add_permission(self, request): # 禁用添加按钮 return True def has_delete_permission(self, request, obj=None): # 禁用删除按钮 return False/<code>

<code>"""这一段主要用于屏蔽 【保存并继续编辑】以及【保存并新建下一个】的按钮,不符合用户习惯同时需要在 类的定义里面要重写"""from django.contrib.admin.templatetags.admin_modify import *from django.contrib.admin.templatetags.admin_modify import submit_row as original_submit_row@register.inclusion_tag('admin/submit_line.html', takes_context=True)def submit_row(context): ctx = original_submit_row(context) ctx.update({ 'show_save_and_add_another': context.get('show_save_and_add_another', ctx['show_save_and_add_another']), 'show_save_and_continue': context.get('show_save_and_continue', ctx['show_save_and_continue']) }) return ctx# --- 分割线 ---class job_detail(admin.ModelAdmin): ... ... # fields 用于控制编辑页面内,需要编辑的字段,逐个显示所有的非AutoField和editable=True # 这里可以采用二维元组的方式进行设定对应字段是否在一行显示,可通过 浏览器开发者工具进行查看确认 # fields = (('JOB_NAME', 'JOB_TYPE'), 'JOB_COMMENT') # fieldsets 是二维元组列表,用于对编辑页面的布局,与fields属性互斥 fieldsets = ( (None, { 'fields': ('JOB_NAME', 'JOB_TYPE', 'JOB_COMMENT') }), ('更多操作', { 'classes': ('wide', 'extrapretty',), 'fields': ('IN_PARA', 'OUT_PARA'), }), ) save_as_continue = False # 修改完成之后跳转到元素列表页面 # 重写方法屏蔽按钮 def change_view(self, request, object_id, form_url='', extra_context=None): extra_context = extra_context or {} extra_context['show_save_and_add_another'] = False extra_context['show_save_and_continue'] = False return super(job_detail, self).change_view(request, object_id, form_url, extra_context=extra_context)/<code>

6、页面按钮权限配置

<code># django admin 的权限模块 默认提供了增删改查的权限配置curr_per_set = {'myapp.view_job_detail','myapp.delete_job_detail','myapp.add_job_detail','myapp.change_job_detail'}# 而当我们以下图为例:仅仅给某个用户赋权 查询 权限的时候,该用户还存在新增按钮的权限,对已有数据无操作权限,此处启发我们需要去思考页面上面按钮权限如何跟具体角色进行关联# 当然 django admin 同样支持自定义权限,此处不再讨论/<code>

<code># 这里演示了根据不同用户角色决定是否在页面上显示相应的功能按钮class job_detail(admin.ModelAdmin): ... ... def has_add_permission(self, request): user_per_set = request.user.get_all_permissions() # 获取当前用户权限 # 待判断的权限范围 curr_per_set = {'myapp.view_job_detail','myapp.delete_job_detail','myapp.add_job_detail','myapp.change_job_detail'} if 'myapp.add_job_detail' in curr_per_set.intersection(user_per_set): return True else: return False/<code>

前文中我们已经介绍了简单的编辑页面的排版展示功能,接下来会更加详细的介绍编辑页面的排版,方便大家按照各自功能进行定制化开发

<code># filesets的样式及描述文案 fieldsets = ( (None, { 'fields': ('JOB_NAME', 'JOB_TYPE', 'JOB_COMMENT') }), ('更多操作', { 'classes': ('wide', 'extrapretty',), 'fields': ('IN_PARA', 'OUT_PARA'), }), )# 这里我们通过classes进行fileset的样式定制,常用的主要有 collaspe和wide,collaspe是将fieldset进行折叠,wide主要是增加水平空间,但这两个参数在simpleui中无用处(可能是我们的姿势不对)# 添加文案说明fieldsets = ( (None, { 'fields': ('JOB_NAME', 'JOB_TYPE', 'JOB_COMMENT') }), ('更多操作', { 'classes': ('collaspe',), 'fields': ('IN_PARA', 'OUT_PARA'), 'description': '

输入输出参数描述文案

', }), )# 这里可以发现 其实我们可以填充html语法,页面同样会直接进行渲染# 那针对每个单元格的描述应该怎么操作呢?这里我们需要通过对models的help_text的属性添加,一般这个功能用于针对某个具体的字段的使用说明上IN_PARA = models.CharField('输入参数定义',max_length=32,help_text=u'例如:按照这个样子')/<code>

<code># 多对多选择框 一般用于增加标签等方式上,但需要注意的是这个针对的many-to-many类型字段## 这里务必要重写__str__方法,否则页面将不会展示具体内容class job_tags(models.Model): title = models.CharField(max_length=30) def __str__(self): return self.titleclass job_detail(models.Model): ... ... TAG = models.ManyToManyField(job_tags)## 此时进行数据库迁移操作,系统会同时创建一张关系表# admin.py中添加filter_horizontal = ('TAG',)/<code>

<code># 列表页面如何显示 多对多 这种类型的字段,通过自定义函数进行返回详情以及描述 def tag_list(self): return ', '.join([a.title for a in self.TAG.all()]) tag_list.short_description = '标签'# admin.pylist_display = ('JOB_TYPE', 'JOB_NAME','JOB_COMMENT', 'IN_PARA','IN_PARA_COMMENT', 'OUT_PARA','OUT_PARA_COMMENT','tag_list')/<code>

<code># 只读字段readonly_fields = ("CREATED_BY",'CREATED_TIME',) /<code>

<code># 自动填充字段 eg:修改某条记录时,自动填充修改人为登录用户# 时间字段处理,需要在model中指定,例如自动更新时间需要增加 auto_now属性,django使用的时间函数 timezone.now CREATED_BY = models.CharField('创建人',max_length=32) # 创建人 CREATED_TIME = models.DateTimeField('创建时间',default=timezone.now) # 创建时间 UPDATED_BY = models.CharField('更新人',max_length=32) # 更新人 UPDATED_TIME = models.DateTimeField('更新时间',auto_now = True) # 更新时间# 获取登录人员信息,进行填充,需要重写save_model方法,同时记得设定成只读字段 def save_model(self, request, obj, form, change): if change: # 判断当前是修改状态还是新增状态 obj.UPDATED_BY = request.user.username obj.save() else: obj.CREATED_BY = request.user.username obj.UPDATED_BY = request.user.username obj.save() readonly_fields = ("CREATED_BY",'CREATED_TIME','UPDATED_BY','UPDATED_TIME',) ## 这里需要注意的是 获取request人员属性的方法/<code>

<code># 级联修改 一对多关联 一般用于维度表配置展示## 利用外键,在多的一方,字段指定外键即可,但定义一对多的关系时候需要指定删除方式new_column = models.ForeignKey('Rela_table', on_delete=models.PROTECT)## models.CASCADE 此为默认值,级联删除,会删除关联数据## models.PROTECT 只要存在关联数据就不能删除## models.DO_NOTHING 什么也不做### 子表中定义外键rela_id = models.ForeignKey("job_rela", on_delete=models.DO_NOTHING,default=9999, verbose_name="选择唯一一个父亲")### 定义依赖的父表class job_rela(models.Model): rela_name = models.CharField('依赖名称',max_length=128) # 任务名称 rela_id = models.CharField('任务编号', max_length=32) # 任务名称 def __str__(self): return self.rela_name### admin中增加显示该字段即可# 特殊的一对多,例如:行政区划parent = models.ForeignKey('self', verbose_name='上级行政区划')/<code>

<code># 只显示当前登录用户自己的数据,超出范围不允许查看 def get_queryset(self, request): qs = super(job_detail, self).get_queryset(request) if request.user.is_superuser: return qs return qs.filter(CREATED_BY=request.user.username)/<code>

<code># 列表页部分展示字段显示不同颜色## model中定义字段颜色,通过函数定义 def colored_name(self): if self.PRO_STATUS == 'Y': cl_name = 'red' else: cl_name = 'blue' return format_html( '{}', cl_name, self.PRO_STATUS, ) colored_name.short_description = '状态'## admin中直接引入即可 这里一个大括号代表一个入参/<code>

8、集成富文本编辑器实现数据字典展示功能

<code># 创建数据字典的model信息,包含主表以及字段表,其中主表跟字段表通过主外键进行一对多的关联class dict_table(models.Model): table_name = models.CharField(max_length=64,verbose_name='表名称') table_comment = models.CharField(max_length=256,verbose_name='描述') table_short_desc = models.CharField(max_length=128,verbose_name='简述') create_time = models.DateTimeField(verbose_name='创建时间',default=timezone.now()) owner = models.CharField(verbose_name='归属人',default='admin',max_length=64) update_time = models.DateTimeField(verbose_name='修改时间',auto_now=True) description = models.TextField(verbose_name='备注') def __str__(self): return self.table_name class Meta: verbose_name = '字典表' verbose_name_plural = '字典表'class dict_columns(models.Model): table_id = models.ForeignKey('dict_table',on_delete=models.CASCADE,verbose_name='关联表主键') column_name = models.CharField(max_length=128,verbose_name='字段名称') column_comment = models.CharField(max_length=128,verbose_name='字段含义及说明') def __str__(self): return '' class Meta: verbose_name = '字段详情' verbose_name_plural = '字段详情'# 添加测试记录/<code>

<code># admin文件中创建表名的列表页并且注册到路由中class dict_columns(admin.TabularInline): model = dict_columns can_delete = False show_change_link = False extra = 1 editable_fields = [] list_display = ['column_name','column_comment'] list_display_links = () readonly_fields = ['column_name','column_comment'] original = False def has_add_permission(self, request,obj=None): user_per_set = request.user.get_all_permissions() # 获取当前用户权限 # 待判断的权限范围 curr_per_set = {'myapp.view_dict_columns','myapp.delete_dict_columns','myapp.add_dict_columns','myapp.change_dict_columns'} if 'myapp.add_dict_columns' in curr_per_set.intersection(user_per_set): return True else: return False@admin.register(dict_table)class dict_table(admin.ModelAdmin): list_display = ('table_name', 'table_short_desc', 'create_time', 'owner') list_display_links = ('table_name',) list_filter = ('table_name',) def has_add_permission(self, request,obj=None): user_per_set = request.user.get_all_permissions() # 获取当前用户权限 # 待判断的权限范围 curr_per_set = {'myapp.view_dict_table','myapp.delete_dict_table','myapp.add_dict_table','myapp.change_dict_table'} if 'myapp.add_dict_table' in curr_per_set.intersection(user_per_set): return True else: return False def has_delete_permission(self, request, obj=None): return False save_as_continue = False # 修改完成之后跳转到元素列表页面 def change_view(self, request, object_id, form_url='', extra_context=None): extra_context = extra_context or {} extra_context['show_save_and_add_another'] = False extra_context['show_save_and_continue'] = False return super(dict_table, self).change_view(request, object_id, form_url, extra_context=extra_context) fieldsets = ( ('基本信息', { 'fields': ('table_name', 'create_time', 'owner','update_time') }), ('说明', { 'fields': ('description', ) }), ) readonly_fields = ("table_name", 'create_time', 'owner', 'update_time',) inlines = [dict_columns,]/<code>


9、自定义按钮实现导入导出功能

<code># 出处:参考simpleui官方文档 # 增加自定义按钮 actions = ['custom_button'] def custom_button(self, request, queryset): pass # 显示的文本,与django admin一致 custom_button.short_description = '测试按钮' # icon,参考element-ui icon与https://fontawesome.com custom_button.icon = 'fas fa-audio-description' # 指定element-ui的按钮类型,参考https://element.eleme.cn/#/zh-CN/component/button custom_button.type = 'danger' # 给按钮追加自定义的颜色 custom_button.style = 'color:black;' # 链接按钮,设置之后直接访问该链接 # 3中打开方式 # action_type 0=当前页内打开,1=新tab打开,2=浏览器tab打开 # 设置了action_type,不设置url,页面内将报错 # 设置成链接类型的按钮后,custom_button方法将不会执行。 custom_button.action_type = 1 custom_button.action_url = 'http://www.baidu.com'/<code>


10、Echart组件引入以及页面个性化调整

<code># django admin 支持自定义template,大部分情况下我们是需要重新修改 change页面class RecordAdmin(admin.ModelAdmin): change_form_template = 'admin/extras/record_change_form.html' ...# 这个我们定义的页面需要继承 {% extends "admin/change_form.html" %}# 在urls.py中添加一条对应的url信息以及对应的view函数进行处理渲染/<code>

四、应用部署步骤

Step1、安装ngnix

Step2、安装uwsgi

Step3、配置uwsgi

Step4、配置ngnix

Step5、启动服务

至此,我们已经基本上把django+simpleui的快速开发方式介绍了一遍,基本上能够满足一个创业团队或者小型团队的快速工具化的需求。其余基于django使用技巧以及部分源码解读也会在后续其他文章中放出。

2020.03.15