Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
itminedu
synnefo
Commits
af272c21
Commit
af272c21
authored
Oct 31, 2012
by
Kostas Papadimitriou
Browse files
Helpdesk suspend actions
parent
95cbdb84
Changes
7
Hide whitespace changes
Inline
Side-by-side
snf-cyclades-app/synnefo/app_settings/default/helpdesk.py
View file @
af272c21
...
...
@@ -4,3 +4,6 @@
# Which is the cookie name that stores the token, leave it commented out
# to use same value as UI_AUTH_COOKIE_NAME value.
#HELPDESK_AUTH_COOKIE_NAME = UI_AUTH_COOKIE_NAME
# Astakos groups which have access to helpdesk views
#HELPDESK_PERMITTED_GROUPS = ['helpdesk']
snf-cyclades-app/synnefo/helpdesk/static/css/extra.css
View file @
af272c21
...
...
@@ -185,4 +185,9 @@ h4.expanded .badge { background-position: 185px 43px}
h4
i
{
margin-top
:
4px
;
margin-right
:
10px
;}
.container-fluid
{
margin-left
:
auto
;
margin-right
:
auto
;
max-width
:
960px
;
}
h3
.info
{
cursor
:
default
;
color
:
#2956B2
;
text-align
:
center
;
font-size
:
16px
;
margin-bottom
:
0
;}
\ No newline at end of file
h3
.info
{
cursor
:
default
;
color
:
#2956B2
;
text-align
:
center
;
font-size
:
16px
;
margin-bottom
:
0
;}
.vm-suspend-form
{
margin-top
:
20px
;
text-align
:
right
;
}
.vm-suspend-form
form
{
margin-bottom
:
0px
;
}
.vm-suspend-form.suspended
form
input
{
background-color
:
#2956B2
;
color
:
#ffffff
;}
.vm-suspend-form
form
input
{
background-color
:
#F81A23
;
color
:
#ffffff
;}
snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/_suspend.html
0 → 100644
View file @
af272c21
{% if vm.suspended %}
<form
method=
"post"
action=
"{% url helpdesk-suspend-vm-release vm_id=vm.pk %}"
>
<input
type=
"hidden"
name=
"token"
value=
"{{ csrf_token }}"
/>
<input
type=
"submit"
value=
"RELEASE SUSPENSION"
/>
</form>
{% else %}
<form
method=
"post"
action=
"{% url helpdesk-suspend-vm vm_id=vm.pk %}"
>
<input
type=
"hidden"
name=
"token"
value=
"{{ csrf_token }}"
/>
<input
type=
"submit"
value=
"SUSPEND"
/>
</form>
{% endif %}
snf-cyclades-app/synnefo/helpdesk/templates/helpdesk/vms_list.html
View file @
af272c21
{% load helpdesk_tags %}
<div
class=
"object-anchor"
id=
"vm-{{vm.pk}}"
></div>
<div
class=
"vm-details object-details {{ rowcls }}"
>
<h4><em><img
src=
"{{ UI_MEDIA_URL }}images/icons/os/{{ vm|get_os }}.png"
/>
{{ vm|get_os }}
</em><i
class=
"icon-tasks"
></i>
{{ vm.name }}
<span
class=
"badge"
>
</span></h4>
{{ vm|vm_status_badge|safe }}
<span
class=
"badge badge-inverse"
>
ID: {{ vm.pk }}
</span>
<span
class=
"badge badge-inverse"
>
{{ vm|vm_public_ip }}
</span>
<span
class=
"badge badge-inverse flavor"
>
<span
class=
"cpu"
>
{{ vm.flavor.cpu }}x
</span>
<span
class=
"ram"
>
{{ vm.flavor.ram}}MB
</span>
<span
class=
"disk"
>
{{ vm.flavor.disk }}GB
</span>
<h4><em><img
src=
"{{ UI_MEDIA_URL }}images/icons/os/{{ vm|get_os }}.png"
/>
{{ vm|get_os }}
</em><i
class=
"icon-tasks"
></i>
{{ vm.name }}
<span
class=
"badge"
>
</span></h4>
{{ vm|vm_status_badge|safe }}
<span
class=
"badge badge-inverse"
>
ID: {{ vm.pk }}
</span>
<span
class=
"badge badge-inverse"
>
{{ vm|vm_public_ip }}
</span>
{% if vm.suspended %}
<span
class=
"badge badge-important"
>
SUSPENDED
</span>
{% endif %}
<span
class=
"badge badge-inverse flavor"
>
<span
class=
"cpu"
>
{{ vm.flavor.cpu }}x
</span>
<span
class=
"ram"
>
{{ vm.flavor.ram}}MB
</span>
<span
class=
"disk"
>
{{ vm.flavor.disk }}GB
</span>
</span>
<div
class=
"vm-details-content object-details-content"
>
<
/span
>
<div
class=
"
vm-details-content object-details-content"
>
<ul
class=
"nav nav-
tab
s
"
>
<li
class=
"active"
><a
href=
"#
d
et
ails
{{ vm.pk }}"
data-toggle=
"tab"
>
D
et
ail
s
</a></li>
<
li><a
href=
"#metadata{{ vm.pk }}"
data-toggle=
"tab"
>
Metadata
</a></li
>
<
li><a
href=
"#backend{{ vm.pk }}"
data-toggle=
"tab"
>
Backend info
</a></li
>
<
li><a
href=
"#network{{ vm.pk }}"
data-toggle=
"tab"
>
Network interfaces
</a></li
>
<
/ul
>
<div
class=
"tab-content"
>
<div
class=
"tab-pane active"
id=
"details{{ vm.pk }}"
>
<dl
class=
"dl-horizontal well"
>
<dt>
ID
</dt><dd>
{{ vm.
pk }}
</dd>
<dt>
Name
</dt><dd>
{{ vm.
name }}
</dd>
<dt>
User i
d
</dt><dd>
{{ vm.us
eri
d }}
</dd>
<dt>
Crea
ted
</dt><dd>
{{ vm.
created }} ({{ vm.created|timesince }}
<strong>
ago
</strong>
)
</dd>
<dt>
Update
d
</dt><dd>
{{ vm.
updated }} ({{ vm.updated|timesince }}
<strong>
ago
</strong>
)
</dd>
<dt>
Suspended
</dt><dd>
{{ vm.
suspended }}
</dd>
<dt>
Deleted
</dt><dd>
{{ vm.deleted }}
</dd>
<dt>
Image id
</dt><dd>
{{ vm.imageid }}
</dd>
<dt>
Flavor
</dt><dd>
{{ vm.flavor.cpu }},
{{ vm.flavor.disk }},
{{ vm.flavor.ram }},
{{ vm.flavor.disk_template }}
</dd
>
<
/
dl>
</div>
<div
class=
"tab-pane"
id=
"metadata{{ vm.pk }}"
>
<dl
class=
"dl-horizontal well"
>
{% for meta in vm.metadata.all %}
<dt>
{{ meta.meta_key }}
</dt><dd>
{{ meta.meta_value }}
</dd>
{% empty %}
<dt>
No metadata
</dt
>
{% endfor %}
<
/
dl>
</div
>
<div
class=
"tab-pane"
id=
"backend{{ vm.pk }}"
>
<dl
class=
"dl-horizontal well"
>
<dt>
Action
</dt><dd>
{{ vm.get_ac
tion
_display }} ({{ vm.ac
tion
}})
</dd>
<dt>
Operstate
</dt><dd>
{{ vm.get_operstate_display }} ({{ vm.operstate
}}
)
</dd>
<dt>
Backend
job
id
</dt><dd>
{{ vm.backendjob
id
}}
</dd>
<dt>
B
ackend op cod
e
</dt><dd>
{{ vm.
get_backendopcode_display }} ({{ vm.backendopcod
e }}
)
</dd>
<
dt>
Backend log msg
</dt><dd>
{{ vm.backendlogmsg }}
</dd
>
<dt>
Build backendjobstatus
</dt><dd>
{{ vm.backendjobstatus }}
</dd
>
<dt>
Build percentage
</dt><dd>
{{ vm.buildpercentage }}
</dd
>
<
/dl
>
</div
>
<div
class=
"tab-pane"
id=
"network{{ vm.pk }}"
>
<table
class=
"table well"
>
<t
hea
d>
<td>
ID
</td>
<td>
Network (ID)
</td>
<td>
Created
</td>
<td>
Updated
</td>
<td>
I
ndex
</td>
<td>
MAC
</td>
<
td>
IPv4
</t
d>
<t
d>
IPv6
</td
>
<td>
Firewall
</td>
<
/thead
>
<t
body
>
{% for nic in vm.nics.all %}
<tr
>
<td>
{{ nic.
pk
}}
</td>
<td>
{{ nic.
network }} ({{ nic.network.pk
}}
)
</td>
<td>
{{ nic.
created
}}
</td>
<td>
{{ nic.
updated
}}
</td>
<td>
{{ nic.i
ndex
}}
</td>
<td>
{{ nic.
mac }}
</td>
<td>
{{ nic.ipv4 }}
</t
d
>
<td>
{{ nic.ipv6 }}
</td>
<t
d>
{{ nic.get_firewall_profile_display }} ({{nic.firewall_profile}})
</td
>
</t
r
>
{% empty %}
<tr>
<td
colspan=
9
>
No network interface available
</td
>
</t
r
>
{% endfor %}
</tbody>
</table
>
</div
>
</div>
</div>
<
ul
class=
"nav nav-tabs"
>
<li
class=
"
active"
><a
href=
"#details{{ vm.pk }}"
data-toggle=
"tab"
>
Details
</a></li
>
<li><a
href=
"#metadata{{ vm.pk }}"
data-toggle=
"tab"
>
Metadata
</a></li>
<li><a
href=
"#backend{{ vm.pk }}"
data-toggle=
"
tab"
>
Backend info
</a></li>
<li><a
href=
"#
n
et
work
{{ vm.pk }}"
data-toggle=
"tab"
>
N
et
work interface
s
</a></li>
<
/ul
>
<
div
class=
"tab-content"
>
<
div
class=
"tab-pane active"
id=
"details{{ vm.pk }}"
>
<
dl
class=
"dl-horizontal well"
>
<dt>
ID
</dt><dd>
{{ vm.pk }}
</dd
>
<dt>
Name
</dt><dd>
{{ vm.name }}
</dd
>
<dt>
User id
</dt><dd>
{{ vm.userid }}
</dd
>
<dt>
Created
</dt><dd>
{{ vm.
created }} ({{ vm.created|timesince }}
<strong>
ago
</strong>
)
</dd>
<dt>
Updated
</dt><dd>
{{ vm.
updated }} ({{ vm.updated|timesince }}
<strong>
ago
</strong>
)
</dd>
<dt>
Suspende
d
</dt><dd>
{{ vm.
s
us
pende
d }}
</dd>
<dt>
Dele
ted
</dt><dd>
{{ vm.
deleted }}
</dd>
<dt>
Image i
d
</dt><dd>
{{ vm.
imageid }}
</dd>
<dt>
Flavor
</dt><dd>
{{ vm.
flavor.cpu }},
{{ vm.flavor.disk }},
{{ vm.flavor.ram }},
{{ vm.flavor.disk_template }}
</dd>
</dl>
</div>
<div
class=
"tab-pane"
id=
"metadata{{ vm.pk }}"
>
<dl
class=
"dl-horizontal well"
>
{% for meta in vm.metadata.all %}
<dt>
{{ meta.meta_key }}
</dt><dd>
{{ meta.meta_value }}
</dd
>
{% empty %}
<dt>
No metadata
</dt>
{% endfor %}
</dl>
</div
>
<div
class=
"tab-pane"
id=
"backend{{ vm.pk }}"
>
<dl
class=
"dl-horizontal well"
>
<dt>
Action
</dt><dd>
{{ vm.get_action_display }} ({{ vm.action }})
</dd
>
<dt>
Operstate
</dt><dd>
{{ vm.get_operstate_display }} ({{ vm.operstate }})
</dd
>
<dt>
Backend job id
</dt><dd>
{{ vm.backendjobid }}
</dd
>
<dt>
Backend op code
</dt><dd>
{{ vm.get_
b
ac
kendopcode
_display }} ({{ vm.
b
ac
kendopcode
}})
</dd>
<dt>
Backend log msg
</dt><dd>
{{ vm.backendlogmsg
}}
</dd>
<dt>
B
uild b
ackendjob
status
</dt><dd>
{{ vm.backendjob
status
}}
</dd>
<dt>
B
uild percentag
e
</dt><dd>
{{ vm.
buildpercentag
e }}
</dd>
<
/dl
>
</div
>
<div
class=
"tab-pane"
id=
"network{{ vm.pk }}"
>
<
table
class=
"table well"
>
<thead
>
<td>
ID
</td
>
<td>
Network (ID)
</td
>
<t
d>
Created
</t
d>
<td>
Updated
</td>
<td>
Index
</td>
<td>
MAC
</td>
<td>
IPv4
</td>
<td>
I
Pv6
</td>
<td>
Firewall
</td>
<
/thea
d>
<t
body
>
{% for nic in vm.nics.all %}
<
tr
>
<t
d>
{{ nic.pk }}
</td
>
<td>
{{ nic.network }} ({{ nic.network.pk }})
</td>
<td>
{{ nic.created }}
</td
>
<td>
{{ nic.
updated
}}
</td>
<td>
{{ nic.
index
}}
</td>
<td>
{{ nic.
mac
}}
</td>
<td>
{{ nic.
ipv4
}}
</td>
<td>
{{ nic.i
pv6
}}
</td>
<td>
{{ nic.
get_firewall_profile_display }} ({{nic.firewall_profile}})
</td>
</t
r
>
{% empty %}
<t
r
>
<td
colspan=
9
>
No network interface available
</t
d
>
</tr>
{% endfor %}
</tbody
>
</t
able
>
</div>
</div>
</div
>
<div
class=
"vm-suspend-form {% if vm.suspended %}suspended{% endif %}"
>
{% include "helpdesk/_suspend.html" %}
</div>
</div>
snf-cyclades-app/synnefo/helpdesk/templatetags/helpdesk_tags.py
View file @
af272c21
...
...
@@ -43,7 +43,7 @@ def network_deleted_badge(network):
Return a span badge styled based on the vm current status
"""
deleted_badge
=
""
if
network
.
state
==
"DELETED"
:
if
network
.
deleted
:
deleted_badge
=
'<span class="badge badge-important">Deleted</span>'
return
deleted_badge
...
...
@@ -55,7 +55,7 @@ def get_os(vm):
return
vm
.
metadata
.
filter
(
meta_key
=
"OS"
).
get
().
meta_value
except
:
return
"unknown"
get_os
.
is_safe
=
True
@
register
.
filter
(
name
=
"network_vms"
)
...
...
@@ -64,7 +64,7 @@ def network_vms(network, account):
for
nic
in
network
.
nics
.
filter
(
machine__userid
=
account
):
vms
.
append
(
nic
.
machine
)
return
vms
network_vms
.
is_safe
=
True
@
register
.
filter
(
name
=
"network_nics"
)
...
...
@@ -73,5 +73,5 @@ def network_nics(network, account):
for
nic
in
network
.
nics
.
filter
(
machine__userid
=
account
):
vms
.
append
(
nic
)
return
vms
network_nics
.
is_safe
=
True
\ No newline at end of file
network_nics
.
is_safe
=
True
snf-cyclades-app/synnefo/helpdesk/urls.py
View file @
af272c21
...
...
@@ -2,6 +2,11 @@ from django.conf.urls.defaults import patterns, url
urlpatterns
=
patterns
(
''
,
url
(
r
'^$'
,
'synnefo.helpdesk.views.index'
,
name
=
'helpdesk-index'
),
url
(
r
'^suspend/(?P<vm_id>[0-9]+)$'
,
'synnefo.helpdesk.views.suspend_vm'
,
name
=
'helpdesk-suspend-vm'
),
url
(
r
'^suspend_release/(?P<vm_id>[0-9]+)$'
,
'synnefo.helpdesk.views.suspend_vm_release'
,
name
=
'helpdesk-suspend-vm-release'
),
url
(
r
'^api/users'
,
'synnefo.helpdesk.views.user_list'
,
name
=
'helpdesk-userslist'
),
url
(
r
'^(?P<account_or_ip>.*)$'
,
'synnefo.helpdesk.views.account'
,
...
...
snf-cyclades-app/synnefo/helpdesk/views.py
View file @
af272c21
# Copyright 2012 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import
re
import
logging
from
itertools
import
chain
...
...
@@ -8,13 +42,17 @@ from django.db.models import get_apps
from
django.conf
import
settings
from
django.core.exceptions
import
PermissionDenied
from
django.db.models
import
Q
from
django.http
import
Http404
,
HttpResponse
from
django.http
import
Http404
,
HttpResponse
,
HttpResponseRedirect
from
django.utils
import
simplejson
as
json
from
django.core.urlresolvers
import
reverse
from
urllib
import
unquote
from
synnefo.lib.astakos
import
get_user
from
synnefo.db.models
import
*
logger
=
logging
.
getLogger
(
__name__
)
IP_SEARCH_REGEX
=
re
.
compile
(
'([0-9]+)(?:\.[0-9]+){3}'
)
def
get_token_from_cookie
(
request
,
cookiename
):
...
...
@@ -33,15 +71,31 @@ def get_token_from_cookie(request, cookiename):
return
None
# TODO: here we mix ui setting with helpdesk settings
# if sometime in the future helpdesk gets splitted from the
# cyclades api code this should change and helpdesk should provide
# its own setting HELPDESK_AUTH_COOKIE_NAME.
AUTH_COOKIE
=
getattr
(
settings
,
'UI_AUTH_COOKIE_NAME'
,
getattr
(
settings
,
'HELPDESK_AUTH_COOKIE_NAME'
,
'_pithos2_a'
))
AUTH_COOKIE_NAME
=
getattr
(
settings
,
'HELPDESK_AUTH_COOKIE_NAME'
,
getattr
(
settings
,
'UI_AUTH_COOKIE_NAME'
,
'_pithos2_a'
))
PERMITTED_GROUPS
=
getattr
(
settings
,
'HELPDESK_PERMITTED_GROUPS'
,
[
'helpdesk'
])
SHOW_DELETED_VMS
=
getattr
(
settings
,
'HELPDESK_SHOW_DELETED_VMS'
,
False
)
def
token_check
(
func
):
"""
Mimic csrf security check using user auth token.
"""
def
wrapper
(
request
,
*
args
,
**
kwargs
):
if
not
hasattr
(
request
,
'user'
):
raise
PermissionDenied
token
=
request
.
POST
.
get
(
'token'
,
None
)
if
token
and
token
!=
request
.
user
.
get
(
'auth_token'
,
None
):
return
func
(
request
,
*
args
,
**
kwargs
)
raise
PermissionDenied
return
wrapper
def
helpdesk_user_required
(
func
,
groups
=
[
'helpdesk'
]
):
def
helpdesk_user_required
(
func
,
permitted_groups
=
PERMITTED_GROUPS
):
"""
Django view wrapper that checks if identified request user has helpdesk
permissions (exists in helpdesk group)
...
...
@@ -51,7 +105,7 @@ def helpdesk_user_required(func, groups=['helpdesk']):
if
not
HELPDESK_ENABLED
:
raise
Http404
token
=
get_token_from_cookie
(
request
,
AUTH_COOKIE
)
token
=
get_token_from_cookie
(
request
,
AUTH_COOKIE
_NAME
)
get_user
(
request
,
settings
.
ASTAKOS_URL
,
fallback_token
=
token
)
if
hasattr
(
request
,
'user'
)
and
request
.
user
:
groups
=
request
.
user
.
get
(
'groups'
,
[])
...
...
@@ -59,12 +113,17 @@ def helpdesk_user_required(func, groups=['helpdesk']):
if
not
groups
:
raise
PermissionDenied
has_perm
=
False
for
g
in
groups
:
if
not
g
in
groups
:
raise
PermissionDenied
if
g
in
permitted_groups
:
has_perm
=
True
if
not
has_perm
:
raise
PermissionDenied
else
:
raise
PermissionDenied
logging
.
debug
(
"User %s accessed helpdesk view"
%
(
request
.
user_uniq
))
return
func
(
request
,
*
args
,
**
kwargs
)
return
wrapper
...
...
@@ -91,6 +150,8 @@ def account(request, account_or_ip):
Account details view.
"""
show_deleted
=
bool
(
int
(
request
.
GET
.
get
(
'deleted'
,
SHOW_DELETED_VMS
)))
account_exists
=
True
vms
=
[]
networks
=
[]
...
...
@@ -104,12 +165,18 @@ def account(request, account_or_ip):
except
NetworkInterface
.
DoesNotExist
:
account_exists
=
False
else
:
# all user vms
vms
=
VirtualMachine
.
objects
.
filter
(
userid
=
account
).
order_by
(
'deleted'
)
filter_extra
=
{}
if
not
show_deleted
:
filter_extra
[
'deleted'
]
=
False
# all user vms
vms
=
VirtualMachine
.
objects
.
filter
(
userid
=
account
,
**
filter_extra
).
order_by
(
'deleted'
)
# return all user private and public networks
public_networks
=
Network
.
objects
.
filter
(
public
=
True
).
order_by
(
'state'
)
private_networks
=
Network
.
objects
.
filter
(
userid
=
account
).
order_by
(
'state'
)
public_networks
=
Network
.
objects
.
filter
(
public
=
True
,
**
filter_extra
).
order_by
(
'state'
)
private_networks
=
Network
.
objects
.
filter
(
userid
=
account
,
**
filter_extra
).
order_by
(
'state'
)
networks
=
list
(
public_networks
)
+
list
(
private_networks
)
if
vms
.
count
()
==
0
and
private_networks
.
count
()
==
0
:
...
...
@@ -120,6 +187,7 @@ def account(request, account_or_ip):
'is_ip'
:
is_ip
,
'account'
:
account
,
'vms'
:
vms
,
'csrf_token'
:
request
.
user
[
'auth_token'
],
'networks'
:
networks
,
'UI_MEDIA_URL'
:
settings
.
UI_MEDIA_URL
}
...
...
@@ -128,6 +196,26 @@ def account(request, account_or_ip):
extra_context
=
user_context
)
@
helpdesk_user_required
@
token_check
def
suspend_vm
(
request
,
vm_id
):
vm
=
VirtualMachine
.
objects
.
get
(
pk
=
vm_id
)
vm
.
suspended
=
True
vm
.
save
()
account
=
vm
.
userid
return
HttpResponseRedirect
(
reverse
(
'helpdesk-details'
,
args
=
(
account
,)))
@
helpdesk_user_required
@
token_check
def
suspend_vm_release
(
request
,
vm_id
):
vm
=
VirtualMachine
.
objects
.
get
(
pk
=
vm_id
)
vm
.
suspended
=
False
vm
.
save
()
account
=
vm
.
userid
return
HttpResponseRedirect
(
reverse
(
'helpdesk-details'
,
args
=
(
account
,)))
@
helpdesk_user_required
def
user_list
(
request
):
"""
...
...
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