20200319 代码发布之任务发布钩子脚本
目录
昨日内容
* ### 代码发布整体工作流程
参考qq截图
代码发布这个功能可以基于很多方式很多语言来实现
我们这里主要用的是python相关的知识点来完成的,大体逻辑流程都是大差不差的
额外补充:p2p(比特流技术),为了减轻服务器的压力,将所有人即是下载者又是资源的上传者
* ### 服务器表的增删改查
我们从头到位将增删改查自己实现了一遍,目的是为了搭建项目增删改查基本业务逻辑,方便后续其他表的操作
#### **modelform使用**
```python
from django.forms import ModelForm
class MyModelForm(ModelForm):
class Meta:
model = models.UserInfo
fields = '__all__' # 前端展示用户表所有的字段
exclude = ['id'] # 排除id字段 不展示到前端
# 渲染标签
form_obj = MyModelForm()
# 校验数据
form_obj = MyModelForm(data=request.POST)
# 新增数据
form_obj.save()
# 编辑数据 渲染标签
form_obj = MyModelForm(instance=edit_obj)
# 修改数据库中的数据
form_obj = MyModelForm(instance=edit_obj,data=request.POST)
form_obj.save()
针对数据的删除功能,一般情况下都需要有一个二次确认的操作
我们是直接利用BOM操作里面的confirm确认框实现的
你还可以借助于第三方插件效果更好一点比如sweetalert插件
### 项目表的增删改查
直接拷贝服务器表所有的代码,修改变量名即可
### 项目优化
将modelform单独放到一个文件夹中
然后再将各个模型表对应的modelform中相同的代码抽取出来形成基类
给项目表再增两个字段
线上项目地址、关联服务器
### 发布任务
由于发布任务是针对项目的,所以为了之后新增任务的时候不需要自己选择项目,所以我们将发布任务做成项目表中的一个字段,点击该字段跳转到该项目对应的所有发布纪录中,之后在该发布纪录页面上开设新增按钮,将当前项目的主键值传递到后端,这样的话新增任务就无需自己选择项目了
今日
- 发布任务单新增页面
- 发布流程前端实时展示
任务发布数据
任务列表的展示为了更加人性化,可以增加当前项目的项目名和环境
formmodel
# 剔除参与展示的字段
exclude = ['uid','project','status']
# form_obj.instance 就是当前的数据对象
form_obj.instance.uid = '唯一标识'
# tasklist有名分组需要传递参数,使用reverse反向解析
url = reverse('task_list',args=(project_id,))
return redirect(url)
初步进行封装重写modelform类的save方法,实现数据的添加
class TaskModelForm(BaseModelForm):
class Meta:
model = models.DeployTask
fields = '__all__'
# 剔除参与展示的字段
exclude = ['uid','project','status']
# 利用初始化获取project_id
def __init__(self,project_id,*args,**kwargs):
super().__init__(*args,**kwargs)
self.project_id = project_id
def save(self, commit=True):
# 添加数据 (重写init方法进行数据的获取)
# .instance 就是数据对象本身,重写save进行保存
self.instance.uid = '唯一标识'
self.instance.project_id = self.project_obj.pk
# 调用父类save方法保存数据
super().save(commit=True)
接下来,我们针对任务的添加页面,单独开设一个html (task_form.html)
并在该html页面上划分三块区域展示不同的信息
-
当前任务关联的项目基本信息展示
-
基本配置
-
脚本书写
获取用户输入的数据cleaned_data
tag = self.cleaned_data.get('tag')
钩子脚本展示
针对四个钩子脚本,我们想要实现可以保存的功能
# 初始化字段
def __init__(self,project_obj,*args,**kwargs):
super().__init__(*args,**kwargs)
self.project_obj = project_obj
# 初始化选择框内容
self.init_hook()
def init_hook(self):
# 给所有的下拉框先添加一个 请选择选项
# <option value="0">请选择</option> (0,'请选择')
self.fields['before_download_select'].choices = [(0,'请选择')]
self.fields['after_download_select'].choices = [(0,'请选择')]
self.fields['before_deploy_select'].choices = [(0,'请选择')]
self.fields['after_deploy_select'].choices = [(0,'请选择')]
生成新字段
需要新添加字段
# 利用之前定义的基类中的`exclude_bootstrap=[]`来控制样式的添加
# checkbox无需添加样式
exclude_bootstrap = [
'before_download_template',
'after_download_template',
'before_deploy_template',
'after_deploy_template'
]
# 自己定义新的字段
# 下拉框 checkbox 文本框
before_download_select = forms.ChoiceField(required=False, label='下载前')
before_download_title = forms.CharField(required=False, label='模板名称')
before_download_template = forms.BooleanField(required=False, widget=forms.CheckboxInput, label='是否保存为模板')
after_download_select = forms.ChoiceField(required=False, label='下载后')
after_download_title = forms.CharField(required=False, label='模板名称')
after_download_template = forms.BooleanField(required=False, widget=forms.CheckboxInput, label='是否保存为模板')
before_deploy_select = forms.ChoiceField(required=False, label='发布前')
before_deploy_title = forms.CharField(required=False, label='模板名称')
before_deploy_template = forms.BooleanField(required=False, widget=forms.CheckboxInput, label='是否保存为模板')
after_deploy_select = forms.ChoiceField(required=False, label='下载后')
after_deploy_title = forms.CharField(required=False, label='模板名称')
after_deploy_template = forms.BooleanField(required=False, widget=forms.CheckboxInput, label='是否保存为模板')
下拉框的展示
初始化选择框内容,是的前端进行展示下拉框内容 choices=
# 初始化字段
def __init__(self,project_obj,*args,**kwargs):
super().__init__(*args,**kwargs)
self.project_obj = project_obj
# 初始化选择框内容
self.init_hook()
def init_hook(self):
# 给所有的下拉框先添加一个 请选择选项
# <option value="0">请选择</option> (0,'请选择')
self.fields['before_download_select'].choices = [(0,'请选择')]
self.fields['after_download_select'].choices = [(0,'请选择')]
self.fields['before_deploy_select'].choices = [(0,'请选择')]
self.fields['after_deploy_select'].choices = [(0,'请选择')]
保存模板数据
需要新建保存用户输入的模板数据表
class HookTemplate(models.Model):
"""保存钩子脚本"""
title = models.CharField(verbose_name='标题',max_length=32)
content = models.TextField(verbose_name='脚本内容')
# 我想实现钩子与钩子之间模版互不影响
hook_type_choices = (
(2,'下载前'),
(4,'下载后'),
(6,'发布前'),
(8,'发布后')
)
hook_type = models.IntegerField(verbose_name='钩子类型',choices=hook_type_choices)
什么时候操作数据表保存数据?
- 判断用户是否点击了checkbox按钮
- 在重写的
save()
方法中判断
def save(self, commit=True):
# 添加数据
self.instance.uid = self.create_uid()
self.instance.project_id = self.project_obj.pk
# 调用父类的save方法保存数据
super().save(commit=True)
# 判断用户是否点击checkbox (保存模板到数据库)
if self.cleaned_data.get("before_download_template"):
# 获取模版名称
title = self.cleaned_data.get("before_download_title")
# 获取脚本内容
content = self.cleaned_data.get("before_download_script")
# 保存到表中
models.HookTemplate.objects.create(
title=title,
content=content,
hook_type=2
)
if self.cleaned_data.get("after_download_template"):
# 获取模版名称
title = self.cleaned_data.get("after_download_title")
# 获取脚本内容
content = self.cleaned_data.get("after_download_script")
# 保存到表中
models.HookTemplate.objects.create(
title=title,
content=content,
hook_type=4
)
if self.cleaned_data.get("before_deploy_template"):
# 获取模版名称
title = self.cleaned_data.get("before_deploy_title")
# 获取脚本内容
content = self.cleaned_data.get("before_deploy_script")
# 保存到表中
models.HookTemplate.objects.create(
title=title,
content=content,
hook_type=6
)
if self.cleaned_data.get("after_deploy_template"):
# 获取模版名称
title = self.cleaned_data.get("after_deploy_title")
# 获取脚本内容
content = self.cleaned_data.get("after_deploy_script")
# 保存到表中
models.HookTemplate.objects.create(
title=title,
content=content,
hook_type=8
)
下拉框显示钩子模板数据
# 给所有的下拉框先添加一个 请选择选项
# <option value="0">请选择</option> (0,'请选择')
before_download = [(0,'请选择')]
# 还应该去数据库中查询是否有对应的模版名称
extra_list = models.HookTemplate.objects.filter(hook_type=2).values_list('pk','title')
# 利用extend扩展列表 (append只是添加)
before_download.extend(extra_list)
self.fields['before_download_select'].choices = before_download
after_download = [(0,'请选择')]
extra_list = models.HookTemplate.objects.filter(hook_type=4).values_list('pk', 'title')
after_download.extend(extra_list)
self.fields['after_download_select'].choices = after_download
before_deploy = [(0,'请选择')]
extra_list = models.HookTemplate.objects.filter(hook_type=6).values_list('pk', 'title')
before_deploy.extend(extra_list)
self.fields['before_deploy_select'].choices = before_deploy
实时获取数据动态展示
给前端所有的select标签绑定文本域变化事件(change事件)
<script>
// 直接给hooks类标签内所有的select绑定事件
$('.hooks').find('select').change(function () {
{#alert($(this).val()) 获取用户输入的模版主键值 #}
var $that = $(this);
// 朝后端发送请求 获取对应的脚本内容
$.ajax({
url:'/hook/template/'+$that.val()+'/',
type:'get',
dataType:'JSON',
success:function (args) {
// 获取脚本内容 渲染到对应下拉框下面的textarea框中
{#alert(args.content)#}
// 标签查找
$that.parent().parent().next().find('textarea').val(args.content);
}
})
})
</script>
def hook_template(request,hook_id):
# 根据hook_id查询出hook对象
hook_obj = models.HookTemplate.objects.filter(pk=hook_id).first()
back_dic = {'status':1000,'content':''}
back_dic['content'] = hook_obj.content
return JsonResponse(back_dic)
优化
用户一旦点击了checkbox按钮,那么就必须填写模版名称(进行校验)def clean()
钩子函数进行校验 # 钩子函数 全局钩子 局部钩子
def clean(self):
if self.cleaned_data.get('before_download_template'):
# 获取用户输入的模版名称 判断是否有值
title = self.cleaned_data.get("before_download_title")
if not title:
# 展示提示信息
self.add_error('before_download_title','请输入模版名称')
if self.cleaned_data.get('after_download_template'):
# 获取用户输入的模版名称 判断是否有值
title = self.cleaned_data.get("after_download_title")
if not title:
# 展示提示信息
self.add_error('after_download_title','请输入模版名称')
if self.cleaned_data.get('before_deploy_template'):
# 获取用户输入的模版名称 判断是否有值
title = self.cleaned_data.get("before_deploy_title")
if not title:
# 展示提示信息
self.add_error('before_deploy_title','请输入模版名称')
if self.cleaned_data.get('after_deploy_template'):
# 获取用户输入的模版名称 判断是否有值
title = self.cleaned_data.get("after_deploy_title")
if not title:
# 展示提示信息
self.add_error('after_deploy_title','请输入模版名称')
注意,前端需要预留一部分内容展示错误信息否则会出现布局错乱的问题
<div class="form-group" style="height: 60px">
<div class="col-sm-3">
<div class="checkbox">
<label for="">{{ form_obj.after_deploy_template }}保存模版</label>
</div>
</div>
<div class="col-sm-9">
{{ form_obj.after_deploy_title }}
<span style="color: red">{{ form_obj.after_deploy_title.errors.0 }}</span>
</div>
</div>
发布任务
Ps:静态文件可以全局也可以在局部
- 静态文件的配置
# 1 配置文件中直接配置
STATICFILES_DIRS = [
os.path.join(BASE_DIR,'static1'),
os.path.join(BASE_DIR,'static2'),
]
# 2 模版语法直接配置
{% load staticfiles %}
<script src="{% static 'js/go.js' %}"></script>
新建发布任务接口
url(r'^deploy/(?P<task_id>\d+)/$',deploy.deploy_task,name='deploy_task')
from django.shortcuts import HttpResponse,render,redirect,reverse
from app01 import models
def deploy_task(request,task_id):
task_obj = models.DeployTask.objects.filter(pk=task_id).first()
return render(request,'deploy.html',locals())
使用gojs展示流程图
<script>
// 由于ws和diagram需要在其他函数内使用 所以定义成全局变量
var ws;
var diagram;
function initWebSocket() {
ws = new WebSocket('ws://127.0.0.1:8000/publish/{{ task_obj.pk }}/');
// 一旦服务端有消息 会自动触发onmessage方法
ws.onmessage = function (args) {
// args.data
var res = JSON.parse(args.data);
if (res.code==='init'){
// 操作gojs渲染图表
diagram.model = new go.TreeModel(res.data)
}
}
}
function initTable() {
var $ = go.GraphObject.make;
diagram = $(go.Diagram, "diagramDiv", {
layout: $(go.TreeLayout, {
angle: 0,
nodeSpacing: 20,
layerSpacing: 70
})
});
// 生成一个节点模版
diagram.nodeTemplate = $(go.Node, "Auto",
$(go.Shape, {
figure: "RoundedRectangle",
fill: 'yellow',
stroke: 'yellow'
}, new go.Binding("figure", "figure"), new go.Binding("fill", "color"), new go.Binding("stroke", "color")),
$(go.TextBlock, {margin: 8}, new go.Binding("text", "text"))
);
// 生成一个箭头模版
diagram.linkTemplate = $(go.Link,
{routing: go.Link.Orthogonal},
$(go.Shape, {stroke: 'yellow'}, new go.Binding('stroke', 'link_color')),
$(go.Shape, {toArrow: "OpenTriangle", stroke: 'yellow'}, new go.Binding('stroke', 'link_color'))
);
// 数据集合 以后替换ajax请求 注意使用key和parent来规定箭头的指向
{#var nodeDataArray = [#}
{# {key: "start", text: '开始', figure: 'Ellipse', color: "lightgreen"},#}
{# {key: "download", parent: 'start', text: '下载代码', color: "lightgreen", link_text: '执行中...'},#}
{# {key: "compile", parent: 'download', text: '本地编译', color: "lightgreen"},#}
{# {key: "zip", parent: 'compile', text: '打包', color: "red", link_color: 'red'},#}
{# {key: "c1", text: '服务器1', parent: "zip"},#}
{# {key: "c11", text: '服务重启', parent: "c1"},#}
{# {key: "c2", text: '服务器2', parent: "zip"},#}
{# {key: "c21", text: '服务重启', parent: "c2"},#}
{# {key: "c3", text: '服务器3', parent: "zip"},#}
{# {key: "c31", text: '服务重启', parent: "c3"},#}
{#];#}
{#diagram.model = new go.TreeModel(nodeDataArray);#}
// 动态控制节点颜色变化 后端给一个key值 即可查找图表并修改
/*
var node = diagram.model.findNodeDataForKey("zip");
diagram.model.setDataProperty(node, "color", "lightgreen");
}
*/
}
// 页面加载完毕 先自动执行两个函数 给全局变量赋值
$(function () {
initWebSocket();
initTable()
});
function createDiagram() {
ws.send('init')
}
</script>
利用channels实现群发的功能
- 注册
INSTALLED_APPS = [
'django.contrib.admin',
...
'channels',
]
- 配置
ASGI_APPLICATION = 'dm_fabu03.routing.application'
- 新建routing
from channels.routing import ProtocolTypeRouter,URLRouter
from django.conf.urls import url
from app01 import consumers
"""consumers.py 当逻辑也非常多的时候 你也可以建成文件夹里面包含多个文件的形式"""
application = ProtocolTypeRouter({
'websocket':URLRouter([
url(r'^publish/(?P<task_id>\d+)/$',consumers.PublishConsumer)
])
})
前端钩子脚本
task_form.html
{% extends 'base.html' %}
{% block css %}
<style>
.outline .series .module {
line-height: 100px;
vertical-align: middle;
width: 940px;
margin: 0 auto;
padding-bottom: 10px;
}
.outline .series .module .item .line {
float: left;
width: 80px;
}
.outline .series .module .item .line hr {
margin-top: 49px
}
.outline .series .module .item .icon {
float: left;
color: #dddddd;
position: relative;
}
.outline .series .module .item .icon .up, .outline .series .module .item .icon .down {
position: absolute;
line-height: 49px;
min-width: 90px;
left: 0;
text-align: center;
margin-left: -38px;
color: #337ab7;
}
.outline .series .module .item:hover .icon, .outline .series .module .item.active .icon {
color: green;
}
.outline .series .module .item .icon .up {
top: 0;
}
.outline .series .module .item .icon .down {
bottom: 0;
}
</style>
{% endblock %}
{% block content %}
{# 1 基本信息展示#}
<table class="table table-hover table-striped table-bordered">
<tbody>
<tr>
<td>项目名称:{{ project_obj.title }}</td>
<td>环境:{{ project_obj.get_env_display }}</td>
</tr>
<tr>
<td colspan="2">仓库地址:{{ project_obj.repo }}</td>
</tr>
<tr>
<td colspan="2">线上地址:{{ project_obj.path }}</td>
</tr>
<tr>
<td colspan="2">
<div>关联服务器</div>
<ul>
{% for server_obj in project_obj.servers.all %}
<li>{{ server_obj.hostname }}</li>
{% endfor %}
</ul>
</td>
</tr>
</tbody>
</table>
<form action="" method="post" novalidate>
{% csrf_token %}
{# 2 基本配置#}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-cog"></span>基本配置</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
<label for="{{ form_obj.tag.id_for_label }}"
class="col-sm-2 control-label">{{ form_obj.tag.label }}</label>
<div class="col-sm-10">
{{ form_obj.tag }}
<span>{{ form_obj.tag.errors.0 }}</span>
</div>
</div>
</div>
</div>
</div>
{# 3 脚本钩子渲染#}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-tasks"></span>发布流程&脚本</h3>
</div>
<div class="panel-body">
{# 4 执行流程图即钩子脚本作用地展示#}
<div class="outline">
<div class="series">
<div class="module clearfix">
<div class="item left">
<div class="line">
<hr>
</div>
<div class="icon">
<span class="glyphicon glyphicon-record" aria-hidden="true"></span>
<a class="down">01 开始</a>
</div>
</div>
<div class="item left active">
<div class="line">
<hr>
</div>
<div class="icon">
<span class="glyphicon glyphicon-record" aria-hidden="true"></span>
<a class="up">02 下载前</a>
</div>
</div>
<div class="item left">
<div class="line">
<hr>
</div>
<div class="icon">
<span class="glyphicon glyphicon-record" aria-hidden="true"></span>
<a class="down">03 下载代码</a>
</div>
</div>
<div class="item left active">
<div class="line">
<hr>
</div>
<div class="icon">
<span class="glyphicon glyphicon-record" aria-hidden="true"></span>
<a class="up">04 下载后</a>
</div>
</div>
<div class="item left">
<div class="line">
<hr>
</div>
<div class="icon">
<span class="glyphicon glyphicon-record" aria-hidden="true"></span>
<a class="down">05 打包上传</a>
</div>
</div>
<div class="item left active">
<div class="line">
<hr>
</div>
<div class="icon">
<span class="glyphicon glyphicon-record" aria-hidden="true"></span>
<a class="up">06 发布前</a>
</div>
</div>
<div class="item left">
<div class="line">
<hr>
</div>
<div class="icon">
<span class="glyphicon glyphicon-record" aria-hidden="true"></span>
<a class="down">07 发布</a>
</div>
</div>
<div class="item left active">
<div class="line">
<hr>
</div>
<div class="icon">
<span class="glyphicon glyphicon-record" aria-hidden="true"></span>
<a class="up">08 发布后</a>
</div>
</div>
<div class="item left">
<div class="line">
<hr>
</div>
</div>
</div>
</div>
</div>
{# 5 四个脚本展示#}
<div class="hooks">
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">02 下载前</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
{# 下拉框#}
<div class="col-sm-12">
{{ form_obj.before_download_select }}
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
{{ form_obj.before_download_script }}
</div>
</div>
<div class="form-group" style="height: 60px">
<div class="col-sm-3">
<div class="checkbox">
<label for="">{{ form_obj.before_download_template }}保存模版</label>
</div>
</div>
<div class="col-sm-9">
{{ form_obj.before_download_title }}
<span style="color: red">{{ form_obj.before_download_title.errors.0 }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">04 下载后</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
{# 下拉框#}
<div class="col-sm-12">
{{ form_obj.after_download_select }}
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
{{ form_obj.after_download_script }}
</div>
</div>
<div class="form-group" style="height: 60px">
<div class="col-sm-3">
<div class="checkbox">
<label for="">{{ form_obj.after_download_template }}保存模版</label>
</div>
</div>
<div class="col-sm-9">
{{ form_obj.after_download_title }}
<span style="color: red">{{ form_obj.after_download_title.errors.0 }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">06 发布前</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
{# 下拉框#}
<div class="col-sm-12">
{{ form_obj.before_deploy_select }}
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
{{ form_obj.before_deploy_script }}
</div>
</div>
<div class="form-group" style="height: 60px">
<div class="col-sm-3">
<div class="checkbox">
<label for="">{{ form_obj.before_deploy_template }}保存模版</label>
</div>
</div>
<div class="col-sm-9">
{{ form_obj.before_deploy_title }}
<span style="color: red">{{ form_obj.before_deploy_title.errors.0 }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">08 发布后</h3>
</div>
<div class="panel-body">
<div class="form-horizontal">
<div class="form-group">
{# 下拉框#}
<div class="col-sm-12">
{{ form_obj.after_deploy_select }}
</div>
</div>
<div class="form-group">
<div class="col-sm-12">
{{ form_obj.after_deploy_script }}
</div>
</div>
<div class="form-group" style="height: 60px">
<div class="col-sm-3">
<div class="checkbox">
<label for="">{{ form_obj.after_deploy_template }}保存模版</label>
</div>
</div>
<div class="col-sm-9">
{{ form_obj.after_deploy_title }}
<span style="color: red">{{ form_obj.after_deploy_title.errors.0 }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<input type="submit" class="btn btn-success">
</form>
{% endblock %}
{% block js %}
<script>
// 直接给hooks类标签内所有的select绑定事件
$('.hooks').find('select').change(function () {
{#alert($(this).val()) 获取用户输入的模版主键值 #}
var $that = $(this);
// 朝后端发送请求 获取对应的脚本内容
$.ajax({
url:'/hook/template/'+$that.val()+'/',
type:'get',
dataType:'JSON',
success:function (args) {
// 获取脚本内容 渲染到对应下拉框下面的textarea框中
{#alert(args.content)#}
// 标签查找
$that.parent().parent().next().find('textarea').val(args.content);
}
})
})
</script>
{% endblock %}