Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
CIRCLE
/
cloud
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
94
Merge Requests
10
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
1e514d5b
authored
Feb 06, 2015
by
Kálmán Viktor
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'feature-useradmin' into 'master'
Useradmin
✅
Closes
#372
See merge request
!286
parents
8c7786a6
ca4fcef5
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
470 additions
and
239 deletions
+470
-239
circle/dashboard/forms.py
+49
-2
circle/dashboard/static/dashboard/dashboard.js
+96
-170
circle/dashboard/static/dashboard/dashboard.less
+23
-1
circle/dashboard/tables.py
+25
-18
circle/dashboard/templates/dashboard/group-detail.html
+1
-1
circle/dashboard/templates/dashboard/index-nodes.html
+1
-1
circle/dashboard/templates/dashboard/index-templates.html
+19
-7
circle/dashboard/templates/dashboard/index-users.html
+53
-0
circle/dashboard/templates/dashboard/index.html
+6
-0
circle/dashboard/templates/dashboard/profile.html
+19
-2
circle/dashboard/templates/dashboard/user-create.html
+1
-2
circle/dashboard/templates/dashboard/user-list.html
+45
-0
circle/dashboard/tests/test_views.py
+9
-6
circle/dashboard/urls.py
+6
-3
circle/dashboard/views/index.py
+9
-1
circle/dashboard/views/template.py
+13
-1
circle/dashboard/views/user.py
+94
-24
circle/dashboard/views/vm.py
+1
-0
circle/locale/hu/LC_MESSAGES/django.po
+0
-0
No files found.
circle/dashboard/forms.py
View file @
1e514d5b
...
...
@@ -59,7 +59,7 @@ from django.utils.translation import string_concat
from
.validators
import
domain_validator
from
dashboard.models
import
ConnectCommand
from
dashboard.models
import
ConnectCommand
,
create_profile
LANGUAGES_WITH_CODE
=
((
l
[
0
],
string_concat
(
l
[
1
],
" ("
,
l
[
0
],
")"
))
for
l
in
LANGUAGES
)
...
...
@@ -1257,10 +1257,19 @@ class CirclePasswordChangeForm(PasswordChangeForm):
class
UserCreationForm
(
OrgUserCreationForm
):
def
__init__
(
self
,
*
args
,
**
kwargs
):
choices
=
kwargs
.
pop
(
'choices'
)
group
=
kwargs
.
pop
(
'default'
)
super
(
UserCreationForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
'groups'
]
=
forms
.
ModelMultipleChoiceField
(
queryset
=
choices
,
initial
=
[
group
],
required
=
False
,
label
=
_
(
'Groups'
))
class
Meta
:
model
=
User
fields
=
(
"username"
,
'email'
,
'first_name'
,
'last_name'
)
fields
=
(
"username"
,
'email'
,
'first_name'
,
'last_name'
,
'groups'
)
@property
def
helper
(
self
):
...
...
@@ -1275,8 +1284,39 @@ class UserCreationForm(OrgUserCreationForm):
user
.
set_password
(
self
.
cleaned_data
[
"password1"
])
if
commit
:
user
.
save
()
create_profile
(
user
)
user
.
groups
.
add
(
*
self
.
cleaned_data
[
"groups"
])
return
user
class
UserEditForm
(
forms
.
ModelForm
):
instance_limit
=
forms
.
IntegerField
(
label
=
_
(
'Instance limit'
),
min_value
=
0
,
widget
=
NumberInput
)
def
__init__
(
self
,
*
args
,
**
kwargs
):
super
(
UserEditForm
,
self
)
.
__init__
(
*
args
,
**
kwargs
)
self
.
fields
[
"instance_limit"
]
.
initial
=
(
self
.
instance
.
profile
.
instance_limit
)
class
Meta
:
model
=
User
fields
=
(
'email'
,
'first_name'
,
'last_name'
,
'instance_limit'
,
'is_active'
)
def
save
(
self
,
commit
=
True
):
user
=
super
(
UserEditForm
,
self
)
.
save
()
user
.
profile
.
instance_limit
=
(
self
.
cleaned_data
[
'instance_limit'
]
or
None
)
user
.
profile
.
save
()
return
user
@property
def
helper
(
self
):
helper
=
FormHelper
()
helper
.
add_input
(
Submit
(
"submit"
,
_
(
"Save"
)))
return
helper
class
AclUserOrGroupAddForm
(
forms
.
Form
):
name
=
forms
.
CharField
(
widget
=
autocomplete_light
.
TextWidget
(
...
...
@@ -1497,3 +1537,10 @@ class TemplateListSearchForm(forms.Form):
data
=
self
.
data
.
copy
()
data
[
'stype'
]
=
"owned"
self
.
data
=
data
class
UserListSearchForm
(
forms
.
Form
):
s
=
forms
.
CharField
(
widget
=
forms
.
TextInput
(
attrs
=
{
'class'
:
"form-control input-tags"
,
'placeholder'
:
_
(
"Search..."
)
}))
circle/dashboard/static/dashboard/dashboard.js
View file @
1e514d5b
...
...
@@ -154,156 +154,70 @@ $(function () {
addSliderMiscs
();
/* search for vms */
var
my_vms
=
[];
$
(
"#dashboard-vm-search-input"
).
keyup
(
function
(
e
)
{
// if my_vms is empty get a list of our vms
if
(
my_vms
.
length
<
1
)
{
$
(
"#dashboard-vm-search-form button i"
).
addClass
(
"fa-spinner fa-spin"
);
$
.
get
(
"/dashboard/vm/list/"
,
function
(
result
)
{
for
(
var
i
in
result
)
{
my_vms
.
push
({
'pk'
:
result
[
i
].
pk
,
'name'
:
result
[
i
].
name
,
'state'
:
result
[
i
].
state
,
'fav'
:
result
[
i
].
fav
,
'host'
:
result
[
i
].
host
,
'icon'
:
result
[
i
].
icon
,
'status'
:
result
[
i
].
status
,
'owner'
:
result
[
i
].
owner
,
});
}
$
(
"#dashboard-vm-search-input"
).
trigger
(
"keyup"
);
$
(
"#dashboard-vm-search-form button i"
).
removeClass
(
"fa-spinner fa-spin"
).
addClass
(
"fa-search"
);
});
return
;
}
/* search */
function
register_search
(
form
,
list
,
generateHTML
)
{
var
my_vms
=
[];
var
search_in_progress
=
false
;
form
.
find
(
'input'
).
keyup
(
function
(
e
)
{
if
(
search_in_progress
)
{
return
;
}
// if my_vms is empty get a list of our vms
if
(
my_vms
.
length
<
1
)
{
search_in_progress
=
true
;
var
btn
=
form
.
find
(
'button'
);
btn
.
find
(
'i'
).
addClass
(
"fa-spinner fa-spin"
);
$
.
get
(
form
.
prop
(
'action'
),
function
(
result
)
{
search_in_progress
=
false
;
my_vms
=
result
;
$
(
this
).
trigger
(
"keyup"
);
btn
.
find
(
'i'
).
removeClass
(
"fa-spinner fa-spin"
).
addClass
(
"fa-search"
);
});
return
;
}
input
=
$
(
"#dashboard-vm-search-input"
).
val
().
toLowerCase
();
var
search_result
=
[];
var
html
=
''
;
for
(
var
i
in
my_vms
)
{
if
(
my_vms
[
i
].
name
.
toLowerCase
().
indexOf
(
input
)
!=
-
1
||
my_vms
[
i
].
host
.
indexOf
(
input
)
!=
-
1
)
{
search_result
.
push
(
my_vms
[
i
]);
input
=
$
(
this
).
val
().
toLowerCase
();
var
search_result
=
[];
for
(
var
i
in
my_vms
)
{
if
(
my_vms
[
i
].
name
.
toLowerCase
().
indexOf
(
input
)
!=
-
1
||
(
my_vms
[
i
].
host
&&
my_vms
[
i
].
host
.
indexOf
(
input
)
!=
-
1
)
||
(
my_vms
[
i
].
org_id
&&
my_vms
[
i
].
org_id
.
toLowerCase
().
indexOf
(
input
)
!=
-
1
)
)
{
search_result
.
push
(
my_vms
[
i
]);
}
}
}
search_result
.
sort
(
compareVmByFav
);
for
(
i
=
0
;
i
<
5
&&
i
<
search_result
.
length
;
i
++
)
html
+=
generateVmHTML
(
search_result
[
i
].
pk
,
search_result
[
i
].
name
,
search_result
[
i
].
owner
?
search_result
[
i
].
owner
:
search_result
[
i
].
host
,
search_result
[
i
].
icon
,
search_result
[
i
].
status
,
search_result
[
i
].
fav
,
(
search_result
.
length
<
5
));
if
(
search_result
.
length
===
0
)
html
+=
'<div class="list-group-item list-group-item-last">'
+
gettext
(
"No result"
)
+
'</div>'
;
$
(
"#dashboard-vm-list"
).
html
(
html
);
$
(
'.title-favourite'
).
tooltip
({
'placement'
:
'right'
});
});
search_result
.
sort
(
compareVmByFav
);
$
(
"#dashboard-vm-search-form"
).
submit
(
function
()
{
var
vm_list_items
=
$
(
"#dashboard-vm-list .list-group-item"
);
if
(
vm_list_items
.
length
==
1
&&
vm_list_items
.
first
().
prop
(
"href"
))
{
window
.
location
.
href
=
vm_list_items
.
first
().
prop
(
"href"
);
return
false
;
}
return
true
;
});
var
html
=
''
;
var
is_last
=
search_result
.
length
<
5
;
/* search for nodes */
var
my_nodes
=
[];
$
(
"#dashboard-node-search-input"
).
keyup
(
function
(
e
)
{
// if my_nodes is empty get a list of our nodes
if
(
my_nodes
.
length
<
1
)
{
$
.
ajaxSetup
(
{
"async"
:
false
}
);
$
.
get
(
"/dashboard/node/list/"
,
function
(
result
)
{
for
(
var
i
in
result
)
{
my_nodes
.
push
({
'name'
:
result
[
i
].
name
,
'icon'
:
result
[
i
].
icon
,
'status'
:
result
[
i
].
status
,
'label'
:
result
[
i
].
label
,
'url'
:
result
[
i
].
url
,
});
}
});
$
.
ajaxSetup
(
{
"async"
:
true
}
);
}
for
(
i
=
0
;
i
<
5
&&
i
<
search_result
.
length
;
i
++
)
html
+=
generateHTML
(
search_result
[
i
],
is_last
);
input
=
$
(
"#dashboard-node-search-input"
).
val
().
toLowerCase
();
var
search_result
=
[];
var
html
=
''
;
for
(
var
i
in
my_nodes
)
{
if
(
my_nodes
[
i
].
name
.
toLowerCase
().
indexOf
(
input
)
!=
-
1
)
{
search_result
.
push
(
my_nodes
[
i
]);
}
}
for
(
i
=
0
;
i
<
5
&&
i
<
search_result
.
length
;
i
++
)
html
+=
generateNodeHTML
(
search_result
[
i
].
name
,
search_result
[
i
].
icon
,
search_result
[
i
].
status
,
search_result
[
i
].
url
,
(
search_result
.
length
<
5
));
if
(
search_result
.
length
===
0
)
html
+=
'<div class="list-group-item list-group-item-last">'
+
gettext
(
"No result"
)
+
'</div>'
;
$
(
"#dashboard-node-list"
).
html
(
html
);
html
=
''
;
for
(
i
=
0
;
i
<
5
&&
i
<
search_result
.
length
;
i
++
)
html
+=
generateNodeTagHTML
(
search_result
[
i
].
name
,
search_result
[
i
].
icon
,
search_result
[
i
].
status
,
search_result
[
i
].
label
,
search_result
[
i
].
url
);
if
(
search_result
.
length
===
0
)
html
+=
'<div class="list-group-item list-group-item-last">'
+
gettext
(
"No result"
)
+
'</div>'
;
$
(
"#dashboard-node-taglist"
).
html
(
html
);
// if there is only one result and ENTER is pressed redirect
if
(
e
.
keyCode
==
13
&&
search_result
.
length
==
1
)
{
window
.
location
.
href
=
search_result
[
0
].
url
;
}
if
(
e
.
keyCode
==
13
&&
search_result
.
length
>
1
&&
input
.
length
>
0
)
{
window
.
location
.
href
=
"/dashboard/node/list/?s="
+
input
;
}
});
if
(
search_result
.
length
===
0
)
html
+=
'<div class="list-group-item list-group-item-last">'
+
gettext
(
"No result"
)
+
'</div>'
;
/* search for groups */
var
my_groups
=
[];
$
(
"#dashboard-group-search-input"
).
keyup
(
function
(
e
)
{
// if my_groups is empty get a list of our groups
if
(
my_groups
.
length
<
1
)
{
$
.
ajaxSetup
(
{
"async"
:
false
}
);
$
.
get
(
"/dashboard/group/list/"
,
function
(
result
)
{
for
(
var
i
in
result
)
{
my_groups
.
push
({
'url'
:
result
[
i
].
url
,
'name'
:
result
[
i
].
name
,
});
}
});
$
.
ajaxSetup
(
{
"async"
:
true
}
);
}
list
.
html
(
html
);
$
(
'.title-favourite'
).
tooltip
({
'placement'
:
'right'
});
});
input
=
$
(
"#dashboard-group-search-input"
).
val
().
toLowerCase
();
var
search_result
=
[];
var
html
=
''
;
for
(
var
i
in
my_groups
)
{
if
(
my_groups
[
i
].
name
.
toLowerCase
().
indexOf
(
input
)
!=
-
1
)
{
search_result
.
push
(
my_groups
[
i
]);
form
.
submit
(
function
()
{
var
vm_list_items
=
list
.
find
(
".list-group-item"
);
if
(
vm_list_items
.
length
==
1
&&
vm_list_items
.
first
().
prop
(
"href"
))
{
window
.
location
.
href
=
vm_list_items
.
first
().
prop
(
"href"
);
return
false
;
}
}
for
(
i
=
0
;
i
<
5
&&
i
<
search_result
.
length
;
i
++
)
html
+=
generateGroupHTML
(
search_result
[
i
].
url
,
search_result
[
i
].
name
,
search_result
.
length
<
5
);
if
(
search_result
.
length
===
0
)
html
+=
'<div class="list-group-item list-group-item-last">'
+
gettext
(
"No result"
)
+
'</div>'
;
$
(
"#dashboard-group-list"
).
html
(
html
);
// if there is only one result and ENTER is pressed redirect
if
(
e
.
keyCode
==
13
&&
search_result
.
length
==
1
)
{
window
.
location
.
href
=
search_result
[
0
].
url
;
}
if
(
e
.
keyCode
==
13
&&
search_result
.
length
>
1
&&
input
.
length
>
0
)
{
window
.
location
.
href
=
"/dashboard/group/list/?s="
+
input
;
}
});
return
true
;
});
}
register_search
(
$
(
"#dashboard-vm-search-form"
),
$
(
"#dashboard-vm-list"
),
generateVmHTML
);
register_search
(
$
(
"#dashboard-node-search-form"
),
$
(
"#dashboard-node-list"
),
generateNodeHTML
);
register_search
(
$
(
"#dashboard-group-search-form"
),
$
(
"#dashboard-group-list"
),
generateGroupHTML
);
register_search
(
$
(
"#dashboard-user-search-form"
),
$
(
"#dashboard-user-list"
),
generateUserHTML
);
register_search
(
$
(
"#dashboard-template-search-form"
),
$
(
"#dashboard-template-list"
),
generateTemplateHTML
);
/* notification message toggle */
$
(
document
).
on
(
'click'
,
".notification-message-subject"
,
function
()
{
...
...
@@ -325,7 +239,7 @@ $(function () {
favicon
.
reset
();
});
/* on the client confirmation button fire the clientInstalledAction */
$
(
document
).
on
(
"click"
,
"#client-check-button"
,
function
(
event
)
{
var
connectUri
=
$
(
'#connect-uri'
).
val
();
...
...
@@ -364,40 +278,52 @@ $(function () {
});
});
function
generateVmHTML
(
pk
,
name
,
host
,
icon
,
_status
,
fav
,
is_last
)
{
return
'<a href="
/dashboard/vm/'
+
pk
+
'/
" class="list-group-item'
+
(
is_last
?
' list-group-item-last'
:
''
)
+
'">'
+
'<span class="index-vm-list-name">'
+
'<i class="fa '
+
icon
+
'" title="'
+
_status
+
'"></i> '
+
safe_tags_replace
(
name
)
+
'</span>'
+
'<small class="text-muted index-vm-list-host"> '
+
host
+
'</small>'
+
'<div class="pull-right dashboard-vm-favourite" data-vm="'
+
pk
+
'">'
+
(
fav
?
'<i class="fa fa-star text-primary title-favourite" title="'
+
gettext
(
"Unfavourite"
)
+
'"></i>'
:
'<i class="fa fa-star-o text-primary title-favourite" title="'
+
gettext
(
"Mark as favorite"
)
+
'"></i>'
)
+
'</div>'
+
'<div style="clear: both;"></div>'
+
'</a>'
;
function
generateVmHTML
(
data
,
is_last
)
{
return
'<a href="
'
+
data
.
url
+
'
" class="list-group-item'
+
(
is_last
?
' list-group-item-last'
:
''
)
+
'">'
+
'<span class="index-vm-list-name">'
+
'<i class="fa '
+
data
.
icon
+
'" title="'
+
data
.
status
+
'"></i> '
+
safe_tags_replace
(
data
.
name
)
+
'</span>'
+
'<small class="text-muted index-vm-list-host"> '
+
data
.
host
+
'</small>'
+
'<div class="pull-right dashboard-vm-favourite" data-vm="'
+
data
.
pk
+
'">'
+
(
data
.
fav
?
'<i class="fa fa-star text-primary title-favourite" title="'
+
gettext
(
"Unfavourite"
)
+
'"></i>'
:
'<i class="fa fa-star-o text-primary title-favourite" title="'
+
gettext
(
"Mark as favorite"
)
+
'"></i>'
)
+
'</div>'
+
'<div style="clear: both;"></div>'
+
'</a>'
;
}
function
generateGroupHTML
(
url
,
name
,
is_last
)
{
return
'<a href="'
+
url
+
'" class="list-group-item real-link'
+
(
is_last
?
" list-group-item-last"
:
""
)
+
'">'
+
'<i class="fa fa-users"></i> '
+
safe_tags_replace
(
name
)
+
function
generateGroupHTML
(
data
,
is_last
)
{
return
'<a href="'
+
data
.
url
+
'" class="list-group-item real-link'
+
(
is_last
?
" list-group-item-last"
:
""
)
+
'">'
+
'<i class="fa fa-users"></i> '
+
safe_tags_replace
(
data
.
name
)
+
'</a>'
;
}
function
generateNodeHTML
(
name
,
icon
,
_status
,
url
,
is_last
)
{
return
'<a href="'
+
url
+
'" class="list-group-item real-link'
+
(
is_last
?
' list-group-item-last'
:
''
)
+
'">'
+
'<span class="index-node-list-name">'
+
'<i class="fa '
+
icon
+
'" title="'
+
_status
+
'"></i> '
+
safe_tags_replace
(
name
)
+
'</span>'
+
'<div style="clear: both;"></div>'
+
'</a>'
;
function
generateUserHTML
(
data
,
is_last
)
{
return
'<a href="'
+
data
.
url
+
'" class="list-group-item real-link'
+
(
is_last
?
" list-group-item-last"
:
""
)
+
'">'
+
'<span class="index-user-list-name"><i class="fa fa-user"></i> '
+
safe_tags_replace
(
data
.
name
)
+
'</span>'
+
'<span class="index-user-list-org">'
+
'<small class="text-muted"> '
+
(
data
.
org_id
?
safe_tags_replace
(
data
.
org_id
)
:
""
)
+
'</small>'
+
'</span></a>'
;
}
function
generateNodeTagHTML
(
name
,
icon
,
_status
,
label
,
url
)
{
return
'<a href="'
+
url
+
'" class="label '
+
label
+
'" >'
+
'<i class="fa '
+
icon
+
'" title="'
+
_status
+
'"></i> '
+
safe_tags_replace
(
name
)
+
'</a> '
;
function
generateTemplateHTML
(
data
,
is_last
)
{
return
'<a href="'
+
data
.
url
+
'" class="list-group-item real-link'
+
(
is_last
?
" list-group-item-last"
:
""
)
+
'">'
+
' <span class="index-template-list-name">'
+
' <i class="fa fa-'
+
data
.
icon
+
'"></i> '
+
safe_tags_replace
(
data
.
name
)
+
' </span>'
+
' <small class="text-muted index-template-list-system">'
+
safe_tags_replace
(
data
.
system
)
+
'</small>'
+
' <div class="clearfix"></div>'
+
'</a>'
;
}
function
generateNodeHTML
(
data
,
is_last
)
{
return
'<a href="'
+
data
.
url
+
'" class="list-group-item real-link'
+
(
is_last
?
' list-group-item-last'
:
''
)
+
'">'
+
'<span class="index-node-list-name">'
+
'<i class="fa '
+
data
.
icon
+
'" title="'
+
data
.
status
+
'"></i> '
+
safe_tags_replace
(
data
.
name
)
+
'</span>'
+
'<div style="clear: both;"></div>'
+
'</a>'
;
}
/* copare vm-s by fav, pk order */
...
...
circle/dashboard/static/dashboard/dashboard.less
View file @
1e514d5b
...
...
@@ -562,7 +562,7 @@ footer a, footer a:hover, footer a:visited {
}
#dashboard-vm-list, #dashboard-node-list, #dashboard-group-list,
#dashboard-template-list, #dashboard-files-toplist {
#dashboard-template-list, #dashboard-files-toplist
, #dashboard-user-list
{
min-height: 200px;
}
...
...
@@ -1168,6 +1168,28 @@ textarea[name="new_members"] {
}
}
#dashboard-user-list {
.list-group-item {
display: flex;
}
.index-user-list-name, .index-user-list-org {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.index-user-list-name {
max-width: 80%;
}
.index-user-list-org {
padding-left: 5px;
flex: 1;
}
}
.fa-fw-12 {
/* fa-fw is too wide */
width: 12px;
...
...
circle/dashboard/tables.py
View file @
1e514d5b
...
...
@@ -18,13 +18,16 @@
from
__future__
import
absolute_import
from
django.contrib.auth.models
import
Group
,
User
from
django.utils.translation
import
ugettext_lazy
as
_
from
django.utils.html
import
mark_safe
from
django_tables2
import
Table
,
A
from
django_tables2.columns
import
(
TemplateColumn
,
Column
,
LinkColumn
,
BooleanColumn
)
from
django_tables2.columns
import
(
TemplateColumn
,
Column
,
LinkColumn
,
BooleanColumn
)
from
django_sshkey.models
import
UserKey
from
vm.models
import
Node
,
InstanceTemplate
,
Lease
from
django.utils.translation
import
ugettext_lazy
as
_
from
django_sshkey.models
import
UserKey
from
dashboard.models
import
ConnectCommand
...
...
@@ -123,26 +126,30 @@ class GroupListTable(Table):
class
UserListTable
(
Table
):
pk
=
TemplateColumn
(
template_name
=
'dashboard/vm-list/column-id.html'
,
verbose_name
=
"ID"
,
attrs
=
{
'th'
:
{
'class'
:
'vm-list-table-thin'
}},
username
=
LinkColumn
(
'dashboard.views.profile'
,
args
=
[
A
(
'username'
)],
)
username
=
TemplateColumn
(
template_name
=
"dashboard/group-list/column-username.html"
profile__org_id
=
LinkColumn
(
'dashboard.views.profile'
,
accessor
=
'profile.org_id'
,
args
=
[
A
(
'username'
)],
verbose_name
=
_
(
'Organization ID'
)
)
class
Meta
:
model
=
User
attrs
=
{
'class'
:
(
'table table-bordered table-striped table-hover '
'vm-list-table'
)}
fields
=
(
'pk'
,
'username'
,
)
is_superuser
=
BooleanColumn
(
verbose_name
=
mark_safe
(
_
(
'<abbr data-placement="left" title="Superuser status">SU</abbr>'
)
)
)
is_active
=
BooleanColumn
()
class
UserListTablex
(
Table
):
class
Meta
:
model
=
User
template
=
"django_tables2/table_no_page.html"
attrs
=
{
'class'
:
(
'table table-bordered table-striped table-hover'
)}
fields
=
(
'username'
,
'last_name'
,
'first_name'
,
'profile__org_id'
,
'email'
,
'is_active'
,
'is_superuser'
)
class
TemplateListTable
(
Table
):
...
...
circle/dashboard/templates/dashboard/group-detail.html
View file @
1e514d5b
...
...
@@ -78,7 +78,7 @@
<h3>
{% trans "User list" %}
{% if perms.auth.add_user %}
<a
href=
"{% url "
dashboard
.
views
.
create-user
"
group
.
pk
%
}"
class=
"btn btn-success pull-right"
>
<a
href=
"{% url "
dashboard
.
views
.
user-create
"
%}?
group_pk=
{{
group
.
pk
}
}"
class=
"btn btn-success pull-right"
>
{% trans "Create user" %}
</a>
{% endif %}
...
...
circle/dashboard/templates/dashboard/index-nodes.html
View file @
1e514d5b
...
...
@@ -37,7 +37,7 @@
id=
"dashboard-node-search-form"
>
<div
class=
"input-group input-group-sm"
>
<input
id=
"dashboard-node-search-input"
type=
"text"
class=
"form-control"
placeholder=
"{% trans "
Search
..."
%}"
/>
name=
"s"
placeholder=
"{% trans "
Search
..."
%}"
/>
<div
class=
"input-group-btn"
>
<button
type=
"submit"
class=
"btn btn-primary"
title=
"{% trans "
Search
"
%}"
data-container=
"body"
>
<i
class=
"fa fa-search"
></i>
...
...
circle/dashboard/templates/dashboard/index-templates.html
View file @
1e514d5b
...
...
@@ -33,13 +33,25 @@
{% endfor %}
</div>
<div
class=
"list-group-item list-group-footer"
>
<div
class=
"text-right"
>
<a
href=
"{% url "
dashboard
.
views
.
template-list
"
%}"
class=
"btn btn-primary btn-xs"
>
<i
class=
"fa fa-chevron-circle-right"
></i>
{% trans "show all" %}
</a>
<a
href=
"{% url "
dashboard
.
views
.
template-choose
"
%}"
class=
"btn btn-success btn-xs template-choose"
>
<i
class=
"fa fa-plus-circle"
></i>
{% trans "new" %}
</a>
<div
class=
"row"
>
<div
class=
"col-xs-6"
>
<form
action=
"{% url "
dashboard
.
views
.
template-list
"
%}"
method=
"GET"
id=
"dashboard-template-search-form"
>
<div
class=
"input-group input-group-sm"
>
<input
id=
"dashboard-group-search-input"
name=
"s"
type=
"text"
class=
"form-control"
placeholder=
"{% trans "
Search
..."
%}"
/>
<div
class=
"input-group-btn"
>
<button
type=
"submit"
class=
"btn btn-primary"
><i
class=
"fa fa-search"
></i></button>
</div>
</div>
</form>
</div>
<div
class=
"col-xs-6 text-right"
>
<a
href=
"{% url "
dashboard
.
views
.
template-list
"
%}"
class=
"btn btn-primary btn-xs"
>
<i
class=
"fa fa-chevron-circle-right"
></i>
{% trans "show all" %}
</a>
<a
href=
"{% url "
dashboard
.
views
.
template-choose
"
%}"
class=
"btn btn-success btn-xs template-choose"
>
<i
class=
"fa fa-plus-circle"
></i>
{% trans "new" %}
</a>
</div>
</div>
</div>
</div>
...
...
circle/dashboard/templates/dashboard/index-users.html
0 → 100644
View file @
1e514d5b
{% load i18n %}
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<div
class=
"pull-right toolbar"
>
<span
class=
"btn btn-default btn-xs infobtn"
data-container=
"body"
title=
"{% trans "
List
of
CIRCLE
users
."
%}"
><i
class=
"fa fa-info-circle"
></i></span>
</div>
<h3
class=
"no-margin"
><i
class=
"fa fa-users"
></i>
{% trans "Users" %}
</h3>
</div>
<div
class=
"list-group"
id=
"user-list-view"
>
<div
id=
"dashboard-user-list"
>
{% for i in users %}
<a
href=
"{% url "
dashboard
.
views
.
profile
"
username=
i.username
%}"
class=
"list-group-item real-link
{% if forloop.last and users|length < 5 %} list-group-item-last{% endif %}"
>
<span
class=
"index-user-list-name"
>
<i
class=
"fa fa-user"
></i>
{% firstof i.get_full_name|safe i.username|safe %}
</span>
<span
class=
"index-user-list-org"
>
<small
class=
"text-muted"
>
{{ i.profile.org_id|default:"" }}
</small>
</span>
</a>
{% endfor %}
</div>
<div
class=
"list-group-item list-group-footer text-right"
>
<div
class=
"row"
>
<div
class=
"col-xs-6"
>
<form
action=
"{% url "
dashboard
.
views
.
user-list
"
%}"
method=
"GET"
id=
"dashboard-user-search-form"
>
<div
class=
"input-group input-group-sm"
>
<input
id=
"dashboard-group-search-input"
name=
"s"
type=
"text"
class=
"form-control"
placeholder=
"{% trans "
Search
..."
%}"
/>
<div
class=
"input-group-btn"
>
<button
type=
"submit"
class=
"btn btn-primary"
><i
class=
"fa fa-search"
></i></button>
</div>
</div>
</form>
</div>
<div
class=
"col-xs-6 text-right"
>
<a
class=
"btn btn-primary btn-xs"
href=
"{% url "
dashboard
.
views
.
user-list
"
%}"
>
<i
class=
"fa fa-chevron-circle-right"
></i>
{% if more_users > 0 %}
{% blocktrans count more=more_users %}
<strong>
{{ more }}
</strong>
more
{% plural %}
<strong>
{{ more }}
</strong>
more
{% endblocktrans %}
{% else %}
{% trans "list" %}
{% endif %}
</a>
<a
class=
"btn btn-success btn-xs user-create"
href=
"{% url "
dashboard
.
views
.
user-create
"
%}"
><i
class=
"fa fa-plus-circle"
></i>
{% trans "new" %}
</a>
</div>
</div>
</div>
</div>
</div>
circle/dashboard/templates/dashboard/index.html
View file @
1e514d5b
...
...
@@ -48,6 +48,12 @@
{% include "dashboard/index-nodes.html" %}
</div>
{% endif %}
{% if perms.auth.change_user %}
<div
class=
"col-lg-4 col-sm-6"
>
{% include "dashboard/index-users.html" %}
</div>
{% endif %}
</div>
</div>
{% endblock %}
circle/dashboard/templates/dashboard/profile.html
View file @
1e514d5b
...
...
@@ -8,7 +8,7 @@
{% block content %}
<div
class=
"row"
>
<div
class=
"col-md-
12
"
>
<div
class=
"col-md-
{% if perms.auth.change_user %}8{% else %}12{% endif %}
"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
{% if request.user.is_superuser %}
...
...
@@ -17,7 +17,7 @@
title=
"{% trans "
Log
in
as
this
user
.
Recommended
to
open
in
an
incognito
window
."
%}"
>
{% trans "Login as this user" %}
</a>
{% endif %}
<a
class=
"pull-right btn btn-default btn-xs"
href=
"{% url "
dashboard
.
index
"
%}"
>
{% trans "Back" %}
</a>
<a
class=
"pull-right btn btn-default btn-xs"
href=
"{% url "
dashboard
.
views
.
user-list
"
%}"
>
{% trans "Back" %}
</a>
<h3
class=
"no-margin"
>
<i
class=
"fa fa-user"
></i>
{% include "dashboard/_display-name.html" with user=profile show_org=True %}
...
...
@@ -109,6 +109,23 @@
</div>
</div>
</div>
{% if perms.auth.change_user %}
<div
class=
"col-md-4"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<h3
class=
"no-margin"
>
<i
class=
"fa fa-user"
></i>
{% trans "Edit user" %}
</h3>
</div>
<div
class=
"panel-body"
>
{% crispy form %}
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}
circle/dashboard/templates/dashboard/user-create.html
View file @
1e514d5b
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
{% crispy form %}
{% crispy form %}
{% endblock %}
circle/dashboard/templates/dashboard/user-list.html
0 → 100644
View file @
1e514d5b
{% extends "dashboard/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% load render_table from django_tables2 %}
{% block title-page %}{% trans "Users" %}{% endblock %}
{% block content %}
<div
class=
"row"
>
<div
class=
"col-md-12"
>
<div
class=
"panel panel-default"
>
<div
class=
"panel-heading"
>
<a
href=
"{% url "
dashboard
.
views
.
user-create
"
%}"
class=
"pull-right btn btn-success btn-xs"
>
<i
class=
"fa fa-plus"
></i>
{% trans "new user" %}
</a>
<h3
class=
"no-margin"
><i
class=
"fa fa-user"
></i>
{% trans "Users" %}
</h3>
</div>
<div
class=
"panel-body"
>
<div
class=
"row"
>
<div
class=
"col-md-offset-8 col-md-4"
id=
"user-list-search"
>
<form
action=
""
method=
"GET"
>
<div
class=
"input-group"
>
{{ search_form.s }}
<div
class=
"input-group-btn"
>
{{ search_form.stype }}
<button
type=
"submit"
class=
"btn btn-primary input-tags"
>
<i
class=
"fa fa-search"
></i>
</button>
</div>
</div>
<!-- .input-group -->
</form>
</div>
<!-- .col-md-4 #user-list-search -->
</div>
</div>
<div
class=
"panel-body"
>
<div
class=
"table-responsive"
>
{% render_table table %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
circle/dashboard/tests/test_views.py
View file @
1e514d5b
...
...
@@ -1293,24 +1293,26 @@ class GroupDetailTest(LoginMixin, TestCase):
self
.
login
(
c
,
'user1'
)
self
.
u1
.
user_permissions
.
add
(
Permission
.
objects
.
get
(
name
=
'Can add user'
))
response
=
c
.
post
(
'/dashboard/
group/
%
d/create/'
%
self
.
g1
.
pk
,
response
=
c
.
post
(
'/dashboard/
profile/create/'
,
{
'username'
:
'userx1'
,
'groups'
:
self
.
g1
.
pk
,
'password1'
:
'test123'
,
'password2'
:
'test123'
})
self
.
assertEqual
(
response
.
status_code
,
403
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
user_count
,
self
.
g1
.
user_set
.
count
())
def
test_permitted_user_add_wo_can_add_user_perm
(
self
):
user_count
=
self
.
g1
.
user_set
.
count
()
c
=
Client
()
self
.
login
(
c
,
'user0'
)
response
=
c
.
post
(
'/dashboard/
group/
%
d/create/'
%
self
.
g1
.
pk
,
response
=
c
.
post
(
'/dashboard/
profile/create/'
,
{
'username'
:
'userx2'
,
'groups'
:
self
.
g1
.
pk
,
'password1'
:
'test123'
,
'password2'
:
'test123'
})
self
.
assertRedirects
(
response
,
'/accounts/login/?next=/dashboard/
group/
%
d/create/'
%
self
.
g1
.
pk
)
'/accounts/login/?next=/dashboard/
profile/create/'
)
self
.
assertEqual
(
response
.
status_code
,
302
)
self
.
assertEqual
(
user_count
,
self
.
g1
.
user_set
.
count
())
...
...
@@ -1320,11 +1322,12 @@ class GroupDetailTest(LoginMixin, TestCase):
name
=
'Can add user'
))
c
=
Client
()
self
.
login
(
c
,
'user0'
)
response
=
c
.
post
(
'/dashboard/
group/
%
d/create/'
%
self
.
g1
.
pk
,
response
=
c
.
post
(
'/dashboard/
profile/create/'
,
{
'username'
:
'userx2'
,
'groups'
:
self
.
g1
.
pk
,
'password1'
:
'test123'
,
'password2'
:
'test123'
})
self
.
assertRedirects
(
response
,
'/dashboard/
group/
%
d/'
%
self
.
g1
.
pk
)
self
.
assertRedirects
(
response
,
'/dashboard/
profile/userx2/'
)
self
.
assertEqual
(
user_count
+
1
,
self
.
g1
.
user_set
.
count
())
self
.
assertEqual
(
response
.
status_code
,
302
)
...
...
circle/dashboard/urls.py
View file @
1e514d5b
...
...
@@ -52,6 +52,7 @@ from .views import (
TransferTemplateOwnershipView
,
TransferTemplateOwnershipConfirmView
,
OpenSearchDescriptionView
,
NodeActivityView
,
UserList
,
)
from
.views.vm
import
vm_ops
,
vm_mass_ops
from
.views.node
import
node_ops
...
...
@@ -61,6 +62,11 @@ autocomplete_light.autodiscover()
urlpatterns
=
patterns
(
''
,
url
(
r'^$'
,
IndexView
.
as_view
(),
name
=
"dashboard.index"
),
url
(
r"^profile/list/$"
,
UserList
.
as_view
(),
name
=
"dashboard.views.user-list"
),
url
(
r'^profile/create/$'
,
UserCreationView
.
as_view
(),
name
=
"dashboard.views.user-create"
),
url
(
r'^lease/(?P<pk>\d+)/$'
,
LeaseDetail
.
as_view
(),
name
=
"dashboard.views.lease-detail"
),
url
(
r'^lease/create/$'
,
LeaseCreate
.
as_view
(),
...
...
@@ -174,9 +180,6 @@ urlpatterns = patterns(
name
=
"dashboard.views.remove-future-user"
),
url
(
r'^group/create/$'
,
GroupCreate
.
as_view
(),
name
=
'dashboard.views.group-create'
),
url
(
r'^group/(?P<group_pk>\d+)/create/$'
,
UserCreationView
.
as_view
(),
name
=
"dashboard.views.create-user"
),
url
(
r'^group/(?P<group_pk>\d+)/permissions/$'
,
GroupPermissionsView
.
as_view
(),
name
=
"dashboard.views.group-permissions"
),
...
...
circle/dashboard/views/index.py
View file @
1e514d5b
...
...
@@ -21,7 +21,7 @@ import logging
from
django.core.cache
import
get_cache
from
django.core.urlresolvers
import
reverse
from
django.conf
import
settings
from
django.contrib.auth.models
import
Group
from
django.contrib.auth.models
import
Group
,
User
from
django.views.generic
import
TemplateView
from
braces.views
import
LoginRequiredMixin
...
...
@@ -86,6 +86,14 @@ class IndexView(LoginRequiredMixin, TemplateView):
'more_groups'
:
groups
.
count
()
-
len
(
groups
[:
5
]),
})
# users
if
user
.
has_module_perms
(
'auth.change_user'
):
users
=
User
.
objects
.
all
()
context
.
update
({
'users'
:
users
[:
5
],
'more_users'
:
users
.
count
()
-
len
(
users
[:
5
]),
})
# template
if
user
.
has_perm
(
'vm.create_template'
):
context
[
'templates'
]
=
InstanceTemplate
.
get_objects_with_level
(
...
...
circle/dashboard/views/template.py
View file @
1e514d5b
...
...
@@ -207,7 +207,19 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
def
get
(
self
,
*
args
,
**
kwargs
):
self
.
search_form
=
TemplateListSearchForm
(
self
.
request
.
GET
)
self
.
search_form
.
full_clean
()
return
super
(
TemplateList
,
self
)
.
get
(
*
args
,
**
kwargs
)
if
self
.
request
.
is_ajax
():
templates
=
[{
'icon'
:
i
.
os_type
,
'system'
:
i
.
system
,
'url'
:
reverse
(
"dashboard.views.template-detail"
,
kwargs
=
{
'pk'
:
i
.
pk
}),
'name'
:
i
.
name
}
for
i
in
self
.
get_queryset
()]
return
HttpResponse
(
json
.
dumps
(
templates
),
content_type
=
"application/json"
,
)
else
:
return
super
(
TemplateList
,
self
)
.
get
(
*
args
,
**
kwargs
)
def
create_acl_queryset
(
self
,
model
):
queryset
=
super
(
TemplateList
,
self
)
.
create_acl_queryset
(
model
)
...
...
circle/dashboard/views/user.py
View file @
1e514d5b
...
...
@@ -31,25 +31,31 @@ from django.core.exceptions import (
)
from
django.core.urlresolvers
import
reverse
,
reverse_lazy
from
django.core.paginator
import
Paginator
,
InvalidPage
from
django.db.models
import
Q
from
django.http
import
HttpResponse
,
HttpResponseRedirect
,
Http404
from
django.shortcuts
import
redirect
,
get_object_or_404
from
django.utils.translation
import
ugettext
as
_
from
django.views.decorators.http
import
require_POST
from
django.views.generic
import
(
TemplateView
,
DetailView
,
View
,
UpdateView
,
CreateView
,
TemplateView
,
View
,
UpdateView
,
CreateView
,
)
from
django_sshkey.models
import
UserKey
from
braces.views
import
LoginRequiredMixin
,
PermissionRequiredMixin
from
django_tables2
import
SingleTableView
from
vm.models
import
Instance
,
InstanceTemplate
from
..forms
import
(
CircleAuthenticationForm
,
MyProfileForm
,
UserCreationForm
,
UnsubscribeForm
,
UserKeyForm
,
CirclePasswordChangeForm
,
ConnectCommandForm
,
UserListSearchForm
,
UserEditForm
,
)
from
..models
import
Profile
,
GroupProfile
,
ConnectCommand
from
..tables
import
(
UserKeyListTable
,
ConnectCommandListTable
,
UserListTable
,
)
from
..models
import
Profile
,
GroupProfile
,
ConnectCommand
,
create_profile
from
..tables
import
UserKeyListTable
,
ConnectCommandListTable
from
.util
import
saml_available
,
DeleteViewBase
...
...
@@ -267,36 +273,46 @@ class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin,
template_name
=
'dashboard/user-create.html'
permission_required
=
"auth.add_user"
def
get_
success_url
(
self
):
re
verse
(
'dashboard.views.group-detail'
,
args
=
[
self
.
group
.
pk
])
def
get_
template_names
(
self
):
re
turn
[
'dashboard/nojs-wrapper.html'
]
def
get_group
(
self
,
group_pk
):
self
.
group
=
get_object_or_404
(
Group
,
pk
=
group_pk
)
if
not
self
.
group
.
profile
.
has_level
(
self
.
request
.
user
,
'owner'
):
raise
PermissionDenied
()
def
get_context_data
(
self
,
*
args
,
**
kwargs
):
context
=
super
(
UserCreationView
,
self
)
.
get_context_data
(
*
args
,
**
kwargs
)
context
.
update
({
'template'
:
self
.
template_name
,
'box_title'
:
_
(
'Create a User'
),
})
return
context
def
get
(
self
,
*
args
,
**
kwargs
):
self
.
get_group
(
kwargs
.
pop
(
'group_pk'
)
)
return
super
(
UserCreationView
,
self
)
.
get
(
*
args
,
**
kwargs
)
def
post
(
self
,
*
args
,
**
kwargs
):
group_pk
=
kwargs
.
pop
(
'group_pk'
)
self
.
get_group
(
group_pk
)
ret
=
super
(
UserCreationView
,
self
)
.
post
(
*
args
,
**
kwargs
)
if
self
.
object
:
create_profile
(
self
.
object
)
self
.
object
.
groups
.
add
(
self
.
group
)
return
redirect
(
r
everse
(
'dashboard.views.group-detail'
,
args
=
[
group_pk
])
)
def
get
_success_url
(
self
):
return
reverse
(
'dashboard.views.profile'
,
args
=
[
self
.
object
.
username
]
)
def
get_form_kwargs
(
self
):
profiles
=
GroupProfile
.
get_objects_with_level
(
'owner'
,
self
.
request
.
user
)
choices
=
Group
.
objects
.
filter
(
groupprofile__in
=
profiles
)
group_pk
=
self
.
request
.
GET
.
get
(
'group_pk'
)
if
group_pk
:
try
:
default
=
choices
.
get
(
pk
=
group_pk
)
except
(
ValueError
,
Group
.
DoesNotExist
):
r
aise
Http404
(
)
else
:
return
ret
default
=
None
val
=
super
(
UserCreationView
,
self
)
.
get_form_kwargs
()
val
.
update
({
'choices'
:
choices
,
'default'
:
default
})
return
val
class
ProfileView
(
LoginRequiredMixin
,
DetailView
):
class
ProfileView
(
LoginRequiredMixin
,
SuccessMessageMixin
,
UpdateView
):
template_name
=
"dashboard/profile.html"
model
=
User
slug_field
=
"username"
slug_url_kwarg
=
"username"
form_class
=
UserEditForm
success_message
=
_
(
"Successfully modified user."
)
def
get
(
self
,
*
args
,
**
kwargs
):
user
=
self
.
request
.
user
...
...
@@ -357,6 +373,15 @@ class ProfileView(LoginRequiredMixin, DetailView):
user
,
self
.
request
.
user
)
return
context
def
post
(
self
,
request
,
*
args
,
**
kwargs
):
if
not
request
.
user
.
has_perm
(
'auth.change_user'
):
raise
PermissionDenied
()
return
super
(
ProfileView
,
self
)
.
post
(
self
,
request
,
*
args
,
**
kwargs
)
def
get_success_url
(
self
):
return
reverse
(
'dashboard.views.profile'
,
kwargs
=
self
.
kwargs
)
@require_POST
def
toggle_use_gravatar
(
request
,
**
kwargs
):
...
...
@@ -480,3 +505,48 @@ class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
kwargs
=
super
(
ConnectCommandCreate
,
self
)
.
get_form_kwargs
()
kwargs
[
'user'
]
=
self
.
request
.
user
return
kwargs
class
UserList
(
LoginRequiredMixin
,
PermissionRequiredMixin
,
SingleTableView
):
template_name
=
"dashboard/user-list.html"
permission_required
=
"auth.change_user"
model
=
User
table_class
=
UserListTable
table_pagination
=
True
def
get_context_data
(
self
,
*
args
,
**
kwargs
):
context
=
super
(
UserList
,
self
)
.
get_context_data
(
*
args
,
**
kwargs
)
context
[
'search_form'
]
=
self
.
search_form
return
context
def
get
(
self
,
*
args
,
**
kwargs
):
self
.
search_form
=
UserListSearchForm
(
self
.
request
.
GET
)
self
.
search_form
.
full_clean
()
if
self
.
request
.
is_ajax
():
users
=
[
{
'url'
:
reverse
(
"dashboard.views.profile"
,
args
=
[
i
.
username
]),
'name'
:
i
.
get_full_name
()
or
i
.
username
,
'org_id'
:
i
.
profile
.
org_id
,
}
for
i
in
self
.
get_queryset
()]
return
HttpResponse
(
json
.
dumps
(
users
),
content_type
=
"application/json"
)
else
:
return
super
(
UserList
,
self
)
.
get
(
*
args
,
**
kwargs
)
def
get_queryset
(
self
):
logger
.
debug
(
'UserList.get_queryset() called. User:
%
s'
,
unicode
(
self
.
request
.
user
))
qs
=
User
.
objects
.
all
()
.
order_by
(
"-pk"
)
q
=
self
.
search_form
.
cleaned_data
.
get
(
's'
)
if
q
:
filters
=
(
Q
(
username__icontains
=
q
)
|
Q
(
email__icontains
=
q
)
|
Q
(
profile__org_id__icontains
=
q
))
for
w
in
q
.
split
()[:
3
]:
filters
|=
(
Q
(
first_name__icontains
=
w
)
|
Q
(
last_name__icontains
=
w
))
qs
=
qs
.
filter
(
filters
)
return
qs
.
select_related
(
"profile"
)
circle/dashboard/views/vm.py
View file @
1e514d5b
...
...
@@ -950,6 +950,7 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
destroyed_at
=
None
)
.
all
()
instances
=
[{
'pk'
:
i
.
pk
,
'url'
:
reverse
(
'dashboard.views.detail'
,
args
=
[
i
.
pk
]),
'name'
:
i
.
name
,
'icon'
:
i
.
get_status_icon
(),
'host'
:
i
.
short_hostname
,
...
...
circle/locale/hu/LC_MESSAGES/django.po
View file @
1e514d5b
This source diff could not be displayed because it is too large. You can
view the blob
instead.
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