Skip to content
Projects
Groups
Snippets
Help
Loading...
Sign in
Toggle navigation
J
jumpserver
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ops
jumpserver
Commits
823e8794
Commit
823e8794
authored
Apr 13, 2018
by
ibuler
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[Update] 更新节点移动交互
parent
739932b0
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
210 additions
and
125 deletions
+210
-125
node.py
apps/assets/api/node.py
+14
-0
_add_assets_to_node_modal.html
apps/assets/templates/assets/_add_assets_to_node_modal.html
+8
-0
_asset_list_modal.html
apps/assets/templates/assets/_asset_list_modal.html
+99
-105
asset_list.html
apps/assets/templates/assets/asset_list.html
+43
-13
api_urls.py
apps/assets/urls/api_urls.py
+1
-0
asset.py
apps/assets/views/asset.py
+1
-0
urls.py
apps/jumpserver/urls.py
+3
-2
jumpserver.js
apps/static/js/jumpserver.js
+39
-5
_modal.html
apps/templates/_modal.html
+2
-0
No files found.
apps/assets/api/node.py
View file @
823e8794
...
...
@@ -32,6 +32,7 @@ __all__ = [
'NodeViewSet'
,
'NodeChildrenApi'
,
'NodeAssetsApi'
,
'NodeWithAssetsApi'
,
'NodeAddAssetsApi'
,
'NodeRemoveAssetsApi'
,
'NodeReplaceAssetsApi'
,
'NodeAddChildrenApi'
,
'RefreshNodeHardwareInfoApi'
,
'TestNodeConnectiveApi'
]
...
...
@@ -191,6 +192,19 @@ class NodeRemoveAssetsApi(generics.UpdateAPIView):
instance
.
assets
.
remove
(
*
tuple
(
assets
))
class
NodeReplaceAssetsApi
(
generics
.
UpdateAPIView
):
serializer_class
=
serializers
.
NodeAssetsSerializer
queryset
=
Node
.
objects
.
all
()
permission_classes
=
(
IsSuperUser
,)
instance
=
None
def
perform_update
(
self
,
serializer
):
assets
=
serializer
.
validated_data
.
get
(
'assets'
)
instance
=
self
.
get_object
()
for
asset
in
assets
:
asset
.
nodes
.
set
([
instance
])
class
RefreshNodeHardwareInfoApi
(
APIView
):
permission_classes
=
(
IsSuperUser
,)
model
=
Node
...
...
apps/assets/templates/assets/_add_assets_to_node_modal.html
0 → 100644
View file @
823e8794
{% extends 'assets/_asset_list_modal.html' %}
{% load i18n %}
{% block modal_button %}
<button
class=
"btn btn-white btn-node-update"
type=
"button"
id=
"btn_move"
>
{% trans "Move to" %}
</button>
<button
class=
"btn btn-primary btn-node-update"
type=
"button"
id=
"btn_copy"
>
{% trans 'Copy to' %}
</button>
{% endblock %}
\ No newline at end of file
apps/assets/templates/assets/_asset_list_modal.html
View file @
823e8794
{% extends '_modal.html' %}
{% load i18n %}
{% load static %}
{% block modal_class %}modal-lg{% endblock %}
{% block modal_id %}asset_list_modal{% endblock %}
{
#{% block modal_title%}{% trans "Please select assets" %}{% endblock %}#
}
{
% block modal_title%}{% trans "Asset list" %}{% endblock %
}
{% block modal_body %}
{#
<div
class=
"btn-group"
style=
"float: right"
>
#}
{#
<button
data-toggle=
"dropdown"
class=
"btn btn-default btn-sm dropdown-toggle"
>
{% trans 'Label' %}
<span
class=
"caret"
></span></button>
#}
{#
<ul
class=
"dropdown-menu labels"
>
#}
{# {% for label in labels %}#}
{#
<li><a
style=
"font-weight: bolder"
>
{{ label.name }}:{{ label.value }}
</a></li>
#}
{# {% endfor %}#}
{#
</ul>
#}
{#
</div>
#}
<table
class=
"table table-striped table-bordered table-hover "
id=
"asset_modal_table"
width=
"100%"
>
<thead>
<tr>
<th
class=
"text-center"
><input
type=
"checkbox"
class=
"ipt_check_all"
></th>
<th
class=
"text-center"
>
{% trans 'Hostname' %}
</th>
<th
class=
"text-center"
>
{% trans 'IP' %}
</th>
<th
class=
"text-center"
>
{% trans 'Hardware' %}
</th>
<th
class=
"text-center"
>
{% trans 'Active' %}
</th>
<th
class=
"text-center"
>
{% trans 'Reachable' %}
</th>
<th
class=
"text-center"
>
{% trans 'Action' %}
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div
id=
"actions"
class=
"hide"
>
<div
class=
"input-group"
>
<select
class=
"form-control m-b"
style=
"width: auto"
id=
"slct_bulk_update"
>
<option
value=
"delete"
>
{% trans 'Delete selected' %}
</option>
<option
value=
"update"
>
{% trans 'Update selected' %}
</option>
<option
value=
"deactive"
>
{% trans 'Deactive selected' %}
</option>
<option
value=
"active"
>
{% trans 'Active selected' %}
</option>
</select>
<div
class=
"input-group-btn pull-left"
style=
"padding-left: 5px;"
>
<button
id=
'btn_bulk_update'
style=
"height: 32px;"
class=
"btn btn-sm btn-primary"
>
{% trans 'Submit' %}
</button>
<link
href=
"{% static 'css/plugins/ztree/awesomeStyle/awesome.css' %}"
rel=
"stylesheet"
>
<script
type=
"text/javascript"
src=
"{% static 'js/plugins/ztree/jquery.ztree.all.min.js' %}"
></script>
<script
src=
"{% static 'js/jquery.form.min.js' %}"
></script>
<style>
.inmodal
.modal-header
{
padding
:
10px
10px
;
text-align
:
center
;
}
#assetTree2
.ztree
*
{
background-color
:
#f8fafb
;
}
#assetTree2
.ztree
{
background-color
:
#f8fafb
;
}
</style>
<div
class=
"wrapper wrapper-content"
>
<div
class=
"row"
>
<div
class=
"col-lg-3"
id=
"split-left"
style=
"padding-left: 3px"
>
<div
class=
"ibox float-e-margins"
>
<div
class=
"ibox-content mailbox-content"
style=
"padding-top: 0;padding-left: 1px"
>
<div
class=
"file-manager "
>
<div
id=
"assetTree2"
class=
"ztree"
>
</div>
<div
class=
"clearfix"
></div>
</div>
</div>
</div>
</div>
<div
class=
"col-lg-9 animated fadeInRight"
id=
"split-right"
>
<div
class=
"mail-box-header"
>
<table
class=
"table table-striped table-bordered table-hover "
id=
"asset_list_modal_table"
style=
"width: 100%"
>
<thead>
<tr>
<th
class=
"text-center"
><input
type=
"checkbox"
class=
"ipt_check_all"
></th>
<th
class=
"text-center"
>
{% trans 'Hostname' %}
</th>
<th
class=
"text-center"
>
{% trans 'IP' %}
</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
var
modal_table
;
function
initModalTable
()
{
<script>
var
zTree2
,
asset_table2
=
0
;
function
initTable2
()
{
var
options
=
{
ele
:
$
(
'#asset_modal_table'
),
columnDefs
:
[
{
targets
:
1
,
createdCell
:
function
(
td
,
cellData
,
rowData
)
{
{
%
url
'assets:asset-detail'
pk
=
DEFAULT_PK
as
the_url
%
}
var
detail_btn
=
'<a href="{{ the_url }}">'
+
cellData
+
'</a>'
;
$
(
td
).
html
(
detail_btn
.
replace
(
'{{ DEFAULT_PK }}'
,
rowData
.
id
));
}},
{
targets
:
3
,
createdCell
:
function
(
td
,
cellData
,
rowData
)
{
$
(
td
).
html
(
rowData
.
hardware_info
)
}},
{
targets
:
4
,
createdCell
:
function
(
td
,
cellData
)
{
if
(
!
cellData
)
{
$
(
td
).
html
(
'<i class="fa fa-times text-danger"></i>'
)
}
else
{
$
(
td
).
html
(
'<i class="fa fa-check text-navy"></i>'
)
}
}},
{
targets
:
5
,
createdCell
:
function
(
td
,
cellData
)
{
if
(
cellData
===
'Unknown'
){
$
(
td
).
html
(
'<i class="fa fa-circle text-warning"></i>'
)
}
else
if
(
!
cellData
)
{
$
(
td
).
html
(
'<i class="fa fa-circle text-danger"></i>'
)
}
else
{
$
(
td
).
html
(
'<i class="fa fa-circle text-navy"></i>'
)
}
}},
{
targets
:
6
,
createdCell
:
function
(
td
,
cellData
,
rowData
)
{
var
update_btn
=
'<a href="{% url "assets:asset-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'
.
replace
(
"{{ DEFAULT_PK }}"
,
cellData
);
var
del_btn
=
'<a class="btn btn-xs btn-danger m-l-xs btn_asset_delete" data-uid="{{ DEFAULT_PK }}">{% trans "Delete" %}</a>'
.
replace
(
'{{ DEFAULT_PK }}'
,
cellData
);
$
(
td
).
html
(
update_btn
+
del_btn
)
}}
],
ele
:
$
(
'#asset_list_modal_table'
),
ajax_url
:
'{% url "api-assets:asset-list" %}'
,
columns
:
[
{
data
:
"id"
},
{
data
:
"hostname"
},
{
data
:
"ip"
},
{
data
:
"cpu_cores"
},
{
data
:
"is_active"
,
orderable
:
false
},
{
data
:
"is_connective"
,
orderable
:
false
},
{
data
:
"id"
,
orderable
:
false
}
{
data
:
"id"
},
{
data
:
"hostname"
},
{
data
:
"ip"
}
],
op_html
:
$
(
'#actions'
).
html
()
pageLength
:
10
};
modal_table
=
jumpserver
.
initServerSideDataTable
(
options
);
return
modal_table
;
asset_table2
=
jumpserver
.
initServerSideDataTable
(
options
);
return
asset_table2
}
$
(
document
).
ready
(
function
(){
initModalTable
();
}).
on
(
'click'
,
'#btn_select_assets'
,
function
()
{
var
data_table
=
$
(
'#asset_modal_table'
).
DataTable
();
var
id_list
=
[];
data_table
.
rows
({
selected
:
true
}).
every
(
function
(){
id_list
.
push
(
this
.
data
().
id
);
});
var
current_node
;
var
nodes
=
zTree
.
getSelectedNodes
();
if
(
nodes
&&
nodes
.
length
===
1
)
{
current_node
=
nodes
[
0
]
}
else
{
return
}
function
onSelected2
(
event
,
treeNode
)
{
var
url
=
asset_table2
.
ajax
.
url
();
url
=
setUrlParam
(
url
,
"node_id"
,
treeNode
.
id
);
setCookie
(
'node_selected'
,
treeNode
.
id
);
asset_table2
.
ajax
.
url
(
url
);
asset_table2
.
ajax
.
reload
();
}
var
data
=
{
'assets'
:
id_list
};
var
success
=
function
()
{
modal_table
.
ajax
.
reload
()
function
initTree2
()
{
var
setting
=
{
view
:
{
dblClickExpand
:
false
,
showLine
:
true
},
data
:
{
simpleData
:
{
enable
:
true
}
},
callback
:
{
onSelected
:
onSelected2
}
};
APIUpdateAttr
({
'url'
:
'/api/assets/v1/nodes/'
+
current_node
.
id
+
'/assets/add/'
,
'method'
:
'PUT'
,
'body'
:
JSON
.
stringify
(
data
),
'success'
:
success
})
var
zNodes
=
[];
$
.
get
(
"{% url 'api-assets:node-list' %}"
,
function
(
data
,
status
){
$
.
each
(
data
,
function
(
index
,
value
)
{
value
[
"pId"
]
=
value
[
"parent"
];
{
#
if
(
value
[
"key"
]
===
"0"
)
{
#
}
value
[
"open"
]
=
true
;
{
#
}
#
}
value
[
"name"
]
=
value
[
"value"
]
+
' ('
+
value
[
'assets_amount'
]
+
')'
;
value
[
'value'
]
=
value
[
'value'
];
});
zNodes
=
data
;
$
.
fn
.
zTree
.
init
(
$
(
"#assetTree2"
),
setting
,
zNodes
);
zTree2
=
$
.
fn
.
zTree
.
getZTreeObj
(
"assetTree2"
);
});
}
$
(
document
).
ready
(
function
(){
initTable2
();
initTree2
();
})
</script>
{% endblock %}
{% block modal_button %}
{{ block.super }}
{% endblock %}
{% block modal_confirm_id %}btn_select_assets{% endblock %}
{% block modal_confirm_id %}btn_asset_modal_confirm{% endblock %}
apps/assets/templates/assets/asset_list.html
View file @
823e8794
...
...
@@ -130,7 +130,7 @@
</div>
{% include 'assets/_asset_import_modal.html' %}
{% include 'assets/_a
sset_list
_modal.html' %}
{% include 'assets/_a
dd_assets_to_node
_modal.html' %}
{% endblock %}
{% block custom_foot_js %}
...
...
@@ -210,10 +210,11 @@ function removeTreeNode() {
if
(
!
current_node
){
return
}
if
(
current_node
.
children
&&
current_node
.
children
.
length
>
0
)
{
alert
(
"{% trans 'Have child node, cancel' %}"
)
}
else
{
toastr
.
error
(
"{% trans 'Have child node, cancel' %}"
);
}
else
if
(
current_node
.
assets_amount
!==
0
)
{
toastr
.
error
(
"{% trans 'Have assets, cancel' %}"
);
}
else
{
var
url
=
"{% url 'api-assets:node-detail' pk=DEFAULT_PK %}"
.
replace
(
"{{ DEFAULT_PK }}"
,
current_node
.
id
);
$
.
ajax
({
url
:
url
,
...
...
@@ -249,13 +250,6 @@ function OnRightClick(event, treeId, treeNode) {
function
showRMenu
(
type
,
x
,
y
)
{
$
(
"#rMenu ul"
).
show
();
{
#
if
(
type
===
"root"
)
{
#
}
{
#
return
#
}
{
#
}
else
{
#
}
{
#
$
(
"#m_del"
).
show
();
#
}
{
#
$
(
"#m_check"
).
show
();
#
}
{
#
$
(
"#m_unCheck"
).
show
();
#
}
{
#
}
#
}
x
-=
220
;
rMenu
.
css
({
"top"
:
y
+
"px"
,
"left"
:
x
+
"px"
,
"visibility"
:
"visible"
});
...
...
@@ -459,7 +453,7 @@ $(document).ready(function(){
var
current_node
;
if
(
nodes
&&
nodes
.
length
===
1
){
current_node
=
nodes
[
0
];
action
=
setUrlParam
(
action
,
'node_id'
,
current_node
.
id
)
action
=
setUrlParam
(
action
,
'node_id'
,
current_node
.
id
)
;
{
#
action
+=
"?node_id="
+
current_node
.
id
;
#
}
$form
.
attr
(
"action"
,
action
)
}
...
...
@@ -674,7 +668,42 @@ $(document).ready(function(){
break
;
}
$
(
".ipt_check_all"
).
prop
(
"checked"
,
false
)
});
})
.
on
(
'click'
,
'.btn-node-update'
,
function
()
{
var
assets_selected
=
asset_table2
.
selected
;
var
current_node
;
var
nodes
=
zTree
.
getSelectedNodes
();
if
(
nodes
&&
nodes
.
length
===
1
)
{
current_node
=
nodes
[
0
]
}
else
{
return
}
var
data
=
{
'assets'
:
assets_selected
};
var
success
=
function
()
{
asset_table2
.
selected
=
[];
asset_table2
.
ajax
.
reload
()
};
var
btn_id
=
$
(
this
).
attr
(
'id'
);
var
url
=
''
;
if
(
btn_id
===
"btn_move"
)
{
url
=
"{% url 'api-assets:node-replace-assets' pk=DEFAULT_PK %}"
.
replace
(
"{{ DEFAULT_PK }}"
,
current_node
.
id
);
}
else
{
url
=
"{% url 'api-assets:node-add-assets' pk=DEFAULT_PK %}"
.
replace
(
"{{ DEFAULT_PK }}"
,
current_node
.
id
);
}
APIUpdateAttr
({
'url'
:
url
,
'method'
:
'PUT'
,
'body'
:
JSON
.
stringify
(
data
),
'success'
:
success
})
}).
on
(
'hidden.bs.modal'
,
'#asset_list_modal'
,
function
()
{
window
.
location
.
reload
();
})
</script>
{% endblock %}
\ No newline at end of file
apps/assets/urls/api_urls.py
View file @
823e8794
...
...
@@ -40,6 +40,7 @@ urlpatterns = [
url
(
r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/children/add/$'
,
api
.
NodeAddChildrenApi
.
as_view
(),
name
=
'node-add-children'
),
url
(
r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/$'
,
api
.
NodeAssetsApi
.
as_view
(),
name
=
'node-assets'
),
url
(
r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/add/$'
,
api
.
NodeAddAssetsApi
.
as_view
(),
name
=
'node-add-assets'
),
url
(
r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/replace/$'
,
api
.
NodeReplaceAssetsApi
.
as_view
(),
name
=
'node-replace-assets'
),
url
(
r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/assets/remove/$'
,
api
.
NodeRemoveAssetsApi
.
as_view
(),
name
=
'node-remove-assets'
),
url
(
r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/refresh-hardware-info/$'
,
api
.
RefreshNodeHardwareInfoApi
.
as_view
(),
name
=
'node-refresh-hardware-info'
),
url
(
r'^v1/nodes/(?P<pk>[0-9a-zA-Z\-]{36})/test-connective/$'
,
api
.
TestNodeConnectiveApi
.
as_view
(),
name
=
'node-test-connective'
),
...
...
apps/assets/views/asset.py
View file @
823e8794
...
...
@@ -49,6 +49,7 @@ class AssetListView(AdminUserRequiredMixin, TemplateView):
'app'
:
_
(
'Assets'
),
'action'
:
_
(
'Asset list'
),
'labels'
:
Label
.
objects
.
all
()
.
order_by
(
'name'
),
'nodes'
:
Node
.
objects
.
all
()
.
order_by
(
'-key'
),
}
kwargs
.
update
(
context
)
return
super
()
.
get_context_data
(
**
kwargs
)
...
...
apps/jumpserver/urls.py
View file @
823e8794
...
...
@@ -36,11 +36,12 @@ urlpatterns = [
url
(
r'^captcha/'
,
include
(
'captcha.urls'
)),
]
urlpatterns
+=
static
(
settings
.
STATIC_URL
,
document_root
=
settings
.
STATIC_ROOT
)
\
+
static
(
settings
.
MEDIA_URL
,
document_root
=
settings
.
MEDIA_ROOT
)
urlpatterns
+=
static
(
settings
.
MEDIA_URL
,
document_root
=
settings
.
MEDIA_ROOT
)
if
settings
.
DEBUG
:
urlpatterns
+=
[
url
(
r'^docs/'
,
schema_view
,
name
=
"docs"
),
]
if
not
settings
.
DEBUG
:
urlpatterns
+=
static
(
settings
.
STATIC_URL
,
document_root
=
settings
.
STATIC_ROOT
)
apps/static/js/jumpserver.js
View file @
823e8794
...
...
@@ -307,7 +307,7 @@ jumpserver.initDataTable = function (options) {
last
:
"»"
}
},
lengthMenu
:
[[
1
5
,
25
,
50
,
-
1
],
[
15
,
25
,
50
,
"All"
]]
lengthMenu
:
[[
1
0
,
15
,
25
,
50
,
-
1
],
[
10
,
15
,
25
,
50
,
"All"
]]
});
table
.
on
(
'select'
,
function
(
e
,
dt
,
type
,
indexes
)
{
var
$node
=
table
[
type
](
indexes
).
nodes
().
to$
();
...
...
@@ -446,22 +446,56 @@ jumpserver.initServerSideDataTable = function (options) {
last
:
"»"
}
},
lengthMenu
:
[[
1
5
,
25
,
50
],
[
15
,
25
,
50
]]
lengthMenu
:
[[
1
0
,
15
,
25
,
50
],
[
10
,
15
,
25
,
50
]]
});
table
.
selected
=
[];
table
.
on
(
'select'
,
function
(
e
,
dt
,
type
,
indexes
)
{
var
$node
=
table
[
type
](
indexes
).
nodes
().
to$
();
$node
.
find
(
'input.ipt_check'
).
prop
(
'checked'
,
true
);
jumpserver
.
selected
[
$node
.
find
(
'input.ipt_check'
).
prop
(
'id'
)]
=
true
jumpserver
.
selected
[
$node
.
find
(
'input.ipt_check'
).
prop
(
'id'
)]
=
true
;
if
(
type
===
'row'
)
{
var
rows
=
table
.
rows
(
indexes
).
data
();
$
.
each
(
rows
,
function
(
id
,
row
)
{
if
(
row
.
id
){
table
.
selected
.
push
(
row
.
id
)
}
})
}
}).
on
(
'deselect'
,
function
(
e
,
dt
,
type
,
indexes
)
{
var
$node
=
table
[
type
](
indexes
).
nodes
().
to$
();
$node
.
find
(
'input.ipt_check'
).
prop
(
'checked'
,
false
);
jumpserver
.
selected
[
$node
.
find
(
'input.ipt_check'
).
prop
(
'id'
)]
=
false
jumpserver
.
selected
[
$node
.
find
(
'input.ipt_check'
).
prop
(
'id'
)]
=
false
;
if
(
type
===
'row'
)
{
var
rows
=
table
.
rows
(
indexes
).
data
();
$
.
each
(
rows
,
function
(
id
,
row
)
{
if
(
row
.
id
){
var
index
=
table
.
selected
.
indexOf
(
row
.
id
);
if
(
index
>
-
1
){
table
.
selected
.
splice
(
index
,
1
)
}
}
})
}
}).
on
(
'draw'
,
function
(){
$
(
'#op'
).
html
(
options
.
op_html
||
''
);
$
(
'#uc'
).
html
(
options
.
uc_html
||
''
);
var
table_data
=
[];
$
.
each
(
table
.
rows
().
data
(),
function
(
id
,
row
)
{
if
(
row
.
id
)
{
table_data
.
push
(
row
.
id
)
}
});
$
.
each
(
table
.
selected
,
function
(
id
,
data
)
{
var
index
=
table_data
.
indexOf
(
data
);
if
(
index
>
-
1
){
table
.
rows
(
index
).
select
()
}
});
});
$
(
'.ipt_check_all'
).
on
(
'click'
,
function
()
{
var
table_id
=
table
.
settings
()[
0
].
sTableId
;
$
(
'#'
+
table_id
+
' .ipt_check_all'
).
on
(
'click'
,
function
()
{
if
(
$
(
this
).
prop
(
"checked"
))
{
$
(
this
).
closest
(
'table'
).
find
(
'.ipt_check'
).
prop
(
'checked'
,
true
);
table
.
rows
({
search
:
'applied'
,
page
:
'current'
}).
select
();
...
...
apps/templates/_modal.html
View file @
823e8794
...
...
@@ -12,8 +12,10 @@
{% endblock %}
</div>
<div
class=
"modal-footer"
>
{% block modal_button %}
<button
data-dismiss=
"modal"
class=
"btn btn-white"
type=
"button"
>
{% trans "Close" %}
</button>
<button
class=
"btn btn-primary"
type=
"button"
id=
"{% block modal_confirm_id %}{% endblock %}"
>
{% trans 'Confirm' %}
</button>
{% endblock %}
</div>
</div>
</div>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment