mirror of
https://github.com/retspen/webvirtcloud
synced 2024-12-24 23:25:24 +00:00
Accounts app improvements and tests
This commit is contained in:
parent
8afef36656
commit
5172a9f619
20 changed files with 622 additions and 227 deletions
|
@ -1,9 +1,10 @@
|
|||
from appsettings.settings import app_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.forms import ModelForm, ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from appsettings.models import AppSettings
|
||||
|
||||
from .models import UserInstance
|
||||
from .models import UserInstance, UserSSHKey
|
||||
from .utils import validate_ssh_key
|
||||
|
||||
|
||||
class UserInstanceForm(ModelForm):
|
||||
|
@ -18,7 +19,7 @@ class UserInstanceForm(ModelForm):
|
|||
|
||||
def clean_instance(self):
|
||||
instance = self.cleaned_data['instance']
|
||||
if AppSettings.objects.get(key="ALLOW_INSTANCE_MULTIPLE_OWNER").value == 'False':
|
||||
if app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER == 'False':
|
||||
exists = UserInstance.objects.filter(instance=instance)
|
||||
if exists:
|
||||
raise ValidationError(_('Instance owned by another user'))
|
||||
|
@ -28,3 +29,43 @@ class UserInstanceForm(ModelForm):
|
|||
class Meta:
|
||||
model = UserInstance
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class ProfileForm(ModelForm):
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = ('first_name', 'last_name', 'email')
|
||||
|
||||
|
||||
class UserSSHKeyForm(ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user', None)
|
||||
self.publickeys = UserSSHKey.objects.filter(user=self.user)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_keyname(self):
|
||||
for key in self.publickeys:
|
||||
if self.cleaned_data['keyname'] == key.keyname:
|
||||
raise ValidationError(_("Key name already exist"))
|
||||
|
||||
return self.cleaned_data['keyname']
|
||||
|
||||
def clean_keypublic(self):
|
||||
for key in self.publickeys:
|
||||
if self.cleaned_data['keypublic'] == key.keypublic:
|
||||
raise ValidationError(_("Public key already exist"))
|
||||
|
||||
if not validate_ssh_key(self.cleaned_data['keypublic']):
|
||||
raise ValidationError(_('Invalid key'))
|
||||
return self.cleaned_data['keypublic']
|
||||
|
||||
def save(self, commit=True):
|
||||
ssh_key = super().save(commit=False)
|
||||
ssh_key.user = self.user
|
||||
if commit:
|
||||
ssh_key.save()
|
||||
return ssh_key
|
||||
|
||||
class Meta:
|
||||
model = UserSSHKey
|
||||
fields = ('keyname', 'keypublic')
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from instances.models import Instance
|
||||
|
||||
|
||||
|
@ -11,6 +9,7 @@ class UserInstanceManager(models.Manager):
|
|||
def get_queryset(self):
|
||||
return super().get_queryset().select_related('instance', 'user')
|
||||
|
||||
|
||||
class UserInstance(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
||||
|
|
|
@ -5,20 +5,15 @@
|
|||
{% load qr_code %}
|
||||
|
||||
{% block title %}{% trans "User Profile" %} - {{ user }}{% endblock %}
|
||||
{% block page_header %}{% trans "User Profile" %}: {{ user }}{% endblock page_header %}
|
||||
|
||||
{% block page_header_extra %}
|
||||
<a href="{% url 'accounts:user_instance_create' user.id %}" class="btn btn-success">
|
||||
{% icon 'plus' %}
|
||||
</a>
|
||||
{% endblock page_header_extra %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<a href="{% url 'user_instance_create' user.id %}" class="btn btn-success btn-header float-right">
|
||||
{% icon 'plus' %}
|
||||
</a>
|
||||
<h2 class="page-header">{% trans "User Profile" %}: {{ user }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#instances">{% trans "Instances" %}</a>
|
||||
|
@ -55,12 +50,12 @@
|
|||
<td>{{ inst.is_change }}</td>
|
||||
<td>{{ inst.is_delete }}</td>
|
||||
<td style="width:5px;">
|
||||
<a href="{% url 'user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
|
||||
<a href="{% url 'accounts:user_instance_update' inst.id %}" class="btn btn-sm btn-secondary" title="{% trans "edit" %}">
|
||||
{% icon 'pencil' %}
|
||||
</a>
|
||||
</td>
|
||||
<td style="width:5px;">
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
|
||||
<a class="btn btn-sm btn-secondary" href="{% url 'accounts:user_instance_delete' inst.id %}" title="{% trans "Delete" %}">
|
||||
{% icon 'trash' %}
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
{% for user in users %}
|
||||
<tr class="{% if not user.is_active %}danger{% endif %}">
|
||||
<td>
|
||||
<a href="{% url 'account' user.id %}"><strong>{{ user.username }}</strong></a>
|
||||
<a href="{% url 'accounts:account' user.id %}"><strong>{{ user.username }}</strong></a>
|
||||
<a data-toggle="modal" href="#editUser{{ user.id }}" class="float-right" title="{% trans "Edit" %}">
|
||||
<span class="fa fa-cog"></span>
|
||||
</a>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<div class="card-header bg-secondary">
|
||||
{% endif %}
|
||||
<h5 class="my-0 card-title">
|
||||
<a class="card-link text-light" href="{% url 'account' user.id %}"><strong>{{ user.username }}</strong></a>
|
||||
<a class="card-link text-light" href="{% url 'accounts:account' user.id %}"><strong>{{ user.username }}</strong></a>
|
||||
<a class="card-link text-light float-right" data-toggle="modal" href="#editUser{{ user.id }}" title="{% trans "Edit" %}">
|
||||
<span class="fa fa-cog"></span>
|
||||
</a>
|
||||
|
|
|
@ -7,23 +7,28 @@
|
|||
{% block title %}{%trans "Change Password" %}{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" id="password-change">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="float-right">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" form="password-change" class="btn btn-success">
|
||||
{% icon 'check' %} {% trans "Change" %}
|
||||
</button>
|
||||
<div class="row">
|
||||
<div class="offset-2 col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">{%trans "Change Password" %}: {{ user }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" id="password-change">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout='horizontal' %}
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="float-right">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %}
|
||||
{% trans "Cancel" %}</a>
|
||||
<button type="submit" form="password-change" class="btn btn-success">
|
||||
{% icon 'check' %} {% trans "Change" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
{% endblock content %}
|
|
@ -1,93 +1,80 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap4 %}
|
||||
{% load icons %}
|
||||
{% load tags_fingerprint %}
|
||||
{% block title %}{% trans "Profile" %}{% endblock %}
|
||||
|
||||
{% block title %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock %}
|
||||
|
||||
{% block page_header %}{% trans "Profile" %}: {{ request.user.first_name }} {{ request.user.last_name}}{% endblock page_header %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Heading -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h2 class="page-header">{% trans "Profile" %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
||||
{% include 'errors_block.html' %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<h3 class="page-header">{% trans "Edit Profile" %}</h3>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" href="#edit-profile">{% trans "Edit Profile" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-toggle="tab" href="#ssh-keys">{% trans "SSH Keys" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane tab-pane-bordered active" id="edit-profile">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" action="" role="form" aria-label="Edit user info form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form profile_form layout='horizontal' %}
|
||||
{% if perms.accounts.change_password %}
|
||||
<a href="{% url 'change_password' %}" class="ml-3 btn btn-primary">{% icon 'lock' %} {% trans "Change Password" %}</a>
|
||||
<a href="{% url 'accounts:change_password' %}" class="btn btn-primary">
|
||||
{% icon 'lock' %} {% trans "Change Password" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<form method="post" action="" role="form" aria-label="Edit user info form">{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<label class="col-sm-2 col-form-label">{% trans "Login" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" value="{{ request.user.username }}" disabled>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 col-form-label">{% trans "Username" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" name="username" value="{{ request.user.first_name }}" pattern="[0-9a-zA-Z]+">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 col-form-label">{% trans "Email" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="email" class="form-control" name="email" value="{{ request.user.email }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">{% trans "Change" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<h3 class="page-header">{% trans "SSH Keys" %}</h3>
|
||||
{% if publickeys %}
|
||||
<div class="col-lg-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<tbody class="text-center">
|
||||
{% for key in publickeys %}
|
||||
<tr>
|
||||
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
|
||||
<td>
|
||||
<form action="" method="post" role="form" aria-label="Delete user public key form">{% csrf_token %}
|
||||
<input type="hidden" name="keyid" value="{{ key.id }}"/>
|
||||
<button type="submit" class="btn btn-sm btn-secondary" name="keydelete" title="{% trans "Delete" %}" onclick="return confirm('{% trans "Are you sure?" %}')">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="" role="form" aria-label="Add key to user form">{% csrf_token %}
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 col-form-label">{% trans "Key name" %}</label>
|
||||
<div class="col-sm-4">
|
||||
<input type="text" class="form-control" name="keyname" placeholder="{% trans "Enter Name" %}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group bridge_name_form_group_dhcp">
|
||||
<label class="col-sm-2 col-form-label">{% trans "Public key" %}</label>
|
||||
<div class="col-sm-8">
|
||||
<textarea name="keypublic" class="form-control" rows="6" placeholder="{% trans "Enter Public Key" %}"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-10">
|
||||
<button type="submit" class="btn btn-primary">{% trans "Add" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-group mb-0 float-right">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% icon 'pencil' %} {% trans "Update" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane tab-pane-bordered fade" id="ssh-keys">
|
||||
{% if publickeys %}
|
||||
<div class="col-lg-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<tbody class="text-center">
|
||||
{% for key in publickeys %}
|
||||
<tr>
|
||||
<td>{{ key.keyname }} ({% ssh_to_fingerprint key.keypublic %})</td>
|
||||
<td>
|
||||
<a href="{% url 'accounts:ssh_key_delete' key.id %}" title="{% trans "Delete" %}" class="btn btn-sm btn-secondary">
|
||||
{% icon 'trash' %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
{%trans "Add SSH Key" %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'accounts:ssh_key_create' %}" role="form" aria-label="Add key to user form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form ssh_key_form layout='horizontal' %}
|
||||
<div class="form-group mb-0 float-right">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% icon 'plus' %} {% trans "Add" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,45 +1,111 @@
|
|||
from django.contrib.auth.models import Permission, User
|
||||
from appsettings.settings import app_settings
|
||||
from computes.models import Compute
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
from instances.models import Instance
|
||||
from instances.utils import refr
|
||||
from libvirt import VIR_DOMAIN_UNDEFINE_NVRAM
|
||||
from vrtManager.create import wvmCreate
|
||||
|
||||
from accounts.forms import UserInstanceForm, UserSSHKeyForm
|
||||
from accounts.models import UserInstance, UserSSHKey
|
||||
from accounts.utils import validate_ssh_key
|
||||
|
||||
|
||||
class AccountsTestCase(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# Add users for testing purposes
|
||||
User = get_user_model()
|
||||
cls.admin_user = User.objects.get(pk=1)
|
||||
cls.test_user = User.objects.create_user(username='test', password='test')
|
||||
|
||||
# Add localhost compute
|
||||
cls.compute = Compute(
|
||||
name='test-compute',
|
||||
hostname='localhost',
|
||||
login='',
|
||||
password='',
|
||||
details='local',
|
||||
type=4,
|
||||
)
|
||||
cls.compute.save()
|
||||
|
||||
cls.connection = wvmCreate(
|
||||
cls.compute.hostname,
|
||||
cls.compute.login,
|
||||
cls.compute.password,
|
||||
cls.compute.type,
|
||||
)
|
||||
|
||||
# Add disks for testing
|
||||
cls.connection.create_volume(
|
||||
'default',
|
||||
'test-volume',
|
||||
1,
|
||||
'qcow2',
|
||||
False,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
|
||||
# XML for testing vm
|
||||
with open('conf/test-vm.xml', 'r') as f:
|
||||
cls.xml = f.read()
|
||||
|
||||
# Create testing vm from XML
|
||||
cls.connection._defineXML(cls.xml)
|
||||
refr(cls.compute)
|
||||
cls.instance = Instance.objects.get(pk=1)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
# Destroy testing vm
|
||||
cls.instance.proxy.delete_all_disks()
|
||||
cls.instance.proxy.delete(VIR_DOMAIN_UNDEFINE_NVRAM)
|
||||
super().tearDownClass()
|
||||
|
||||
def setUp(self):
|
||||
self.client.login(username='admin', password='admin')
|
||||
user = User.objects.create_user(username='test', password='test')
|
||||
permission = Permission.objects.get(codename='change_password')
|
||||
user.user_permissions.add(permission)
|
||||
self.test_user.user_permissions.add(permission)
|
||||
self.rsa_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6OOdbfv27QVnSC6sKxGaHb6YFc+3gxCkyVR3cTSXE/n5BEGf8aOgBpepULWa1RZfxYHY14PlKULDygdXSdrrR2kNSwoKz/Oo4d+3EE92L7ocl1+djZbptzgWgtw1OseLwbFik+iKlIdqPsH+IUQvX7yV545ZQtAP8Qj1R+uCqkw== test@test'
|
||||
self.ecdsa_key = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJc5xpT3R0iFJYNZbmWgAiDlHquX/BcV1kVTsnBfiMsZgU3lGaqz2eb7IBcir/dxGnsVENTTmPQ6sNcxLxT9kkQ= realgecko@archlinux'
|
||||
|
||||
def test_profile(self):
|
||||
response = self.client.get(reverse('profile'))
|
||||
response = self.client.get(reverse('accounts:profile'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse('account', args=[2]))
|
||||
response = self.client.get(reverse('accounts:account', args=[self.test_user.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_account_with_otp(self):
|
||||
settings.OTP_ENABLED = True
|
||||
response = self.client.get(reverse('accounts:account', args=[self.test_user.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_login_logout(self):
|
||||
user = User.objects.get(username='test')
|
||||
self.assertEqual(user.id, 2)
|
||||
|
||||
client = Client()
|
||||
|
||||
response = client.post(reverse("login"), {"username": "test", "password": "test"})
|
||||
self.assertRedirects(response, reverse('profile'))
|
||||
response = client.post(reverse("accounts:login"), {"username": "test", "password": "test"})
|
||||
self.assertRedirects(response, reverse('accounts:profile'))
|
||||
|
||||
response = client.get(reverse('logout'))
|
||||
self.assertRedirects(response, reverse('login'))
|
||||
response = client.get(reverse('accounts:logout'))
|
||||
self.assertRedirects(response, reverse('accounts:login'))
|
||||
|
||||
def test_password_change(self):
|
||||
client = Client()
|
||||
def test_change_password(self):
|
||||
self.client.force_login(self.test_user)
|
||||
|
||||
logged_in = client.login(username='test', password='test')
|
||||
self.assertTrue(logged_in)
|
||||
|
||||
response = client.get(reverse('change_password'))
|
||||
response = self.client.get(reverse('accounts:change_password'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = client.post(
|
||||
reverse('change_password'),
|
||||
response = self.client.post(
|
||||
reverse('accounts:change_password'),
|
||||
{
|
||||
'old_password': 'wrongpass',
|
||||
'new_password1': 'newpw',
|
||||
|
@ -48,17 +114,154 @@ class AccountsTestCase(TestCase):
|
|||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = client.post(
|
||||
reverse('change_password'),
|
||||
response = self.client.post(
|
||||
reverse('accounts:change_password'),
|
||||
{
|
||||
'old_password': 'test',
|
||||
'new_password1': 'newpw',
|
||||
'new_password2': 'newpw',
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse('profile'))
|
||||
self.assertRedirects(response, reverse('accounts:profile'))
|
||||
|
||||
client.logout()
|
||||
self.client.logout()
|
||||
|
||||
logged_in = client.login(username='test', password='newpw')
|
||||
logged_in = self.client.login(username='test', password='newpw')
|
||||
self.assertTrue(logged_in)
|
||||
|
||||
def test_user_instance_create_update_delete(self):
|
||||
# create
|
||||
response = self.client.get(reverse('accounts:user_instance_create', args=[self.test_user.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('accounts:user_instance_create', args=[self.test_user.id]),
|
||||
{
|
||||
'user': self.test_user.id,
|
||||
'instance': self.instance.id,
|
||||
'is_change': False,
|
||||
'is_delete': False,
|
||||
'is_vnc': False,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
|
||||
|
||||
user_instance: UserInstance = UserInstance.objects.get(pk=1)
|
||||
self.assertEqual(user_instance.user, self.test_user)
|
||||
self.assertEqual(user_instance.instance, self.instance)
|
||||
self.assertEqual(user_instance.is_change, False)
|
||||
self.assertEqual(user_instance.is_delete, False)
|
||||
self.assertEqual(user_instance.is_vnc, False)
|
||||
|
||||
# update
|
||||
response = self.client.get(reverse('accounts:user_instance_update', args=[user_instance.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('accounts:user_instance_update', args=[user_instance.id]),
|
||||
{
|
||||
'user': self.test_user.id,
|
||||
'instance': self.instance.id,
|
||||
'is_change': True,
|
||||
'is_delete': True,
|
||||
'is_vnc': True,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
|
||||
|
||||
user_instance: UserInstance = UserInstance.objects.get(pk=1)
|
||||
self.assertEqual(user_instance.user, self.test_user)
|
||||
self.assertEqual(user_instance.instance, self.instance)
|
||||
self.assertEqual(user_instance.is_change, True)
|
||||
self.assertEqual(user_instance.is_delete, True)
|
||||
self.assertEqual(user_instance.is_vnc, True)
|
||||
|
||||
# delete
|
||||
response = self.client.get(reverse('accounts:user_instance_delete', args=[user_instance.id]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse('accounts:user_instance_delete', args=[user_instance.id]))
|
||||
self.assertRedirects(response, reverse('accounts:account', args=[self.test_user.id]))
|
||||
|
||||
# test 'next' redirect during deletion
|
||||
user_instance = UserInstance.objects.create(user=self.test_user, instance=self.instance)
|
||||
response = self.client.post(
|
||||
reverse('accounts:user_instance_delete', args=[user_instance.id]) + '?next=' + reverse('index'))
|
||||
self.assertRedirects(response, reverse('index'))
|
||||
|
||||
def test_update_user_profile(self):
|
||||
self.client.force_login(self.test_user)
|
||||
|
||||
user = get_user_model().objects.get(username='test')
|
||||
self.assertEqual(user.first_name, '')
|
||||
self.assertEqual(user.last_name, '')
|
||||
self.assertEqual(user.email, '')
|
||||
|
||||
response = self.client.post(reverse('accounts:profile'), {
|
||||
'first_name': 'first name',
|
||||
'last_name': 'last name',
|
||||
'email': 'email@mail.mail',
|
||||
})
|
||||
self.assertRedirects(response, reverse('accounts:profile'))
|
||||
|
||||
user = get_user_model().objects.get(username='test')
|
||||
self.assertEqual(user.first_name, 'first name')
|
||||
self.assertEqual(user.last_name, 'last name')
|
||||
self.assertEqual(user.email, 'email@mail.mail')
|
||||
|
||||
def test_create_delete_ssh_key(self):
|
||||
response = self.client.get(reverse('accounts:ssh_key_create'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse('accounts:ssh_key_create'), {
|
||||
'keyname': 'keyname',
|
||||
'keypublic': self.rsa_key,
|
||||
})
|
||||
self.assertRedirects(response, reverse('accounts:profile'))
|
||||
|
||||
key = UserSSHKey.objects.get(pk=1)
|
||||
self.assertEqual(key.keyname, 'keyname')
|
||||
self.assertEqual(key.keypublic, self.rsa_key)
|
||||
|
||||
response = self.client.get(reverse('accounts:ssh_key_delete', args=[1]))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse('accounts:ssh_key_delete', args=[1]))
|
||||
self.assertRedirects(response, reverse('accounts:profile'))
|
||||
|
||||
def test_validate_ssh_key(self):
|
||||
self.assertFalse(validate_ssh_key(''))
|
||||
self.assertFalse(validate_ssh_key('ssh-rsa ABBA test@test'))
|
||||
self.assertFalse(validate_ssh_key('ssh-rsa AAAABwdzZGY= test@test'))
|
||||
self.assertFalse(validate_ssh_key('ssh-rsa AAA test@test'))
|
||||
# validate ecdsa key
|
||||
self.assertTrue(validate_ssh_key(self.ecdsa_key))
|
||||
|
||||
def test_forms(self):
|
||||
# raise available validation errors for maximum coverage
|
||||
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': self.rsa_key}, user=self.test_user)
|
||||
form.save()
|
||||
|
||||
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': self.rsa_key}, user=self.test_user)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
form = UserSSHKeyForm({'keyname': 'keyname', 'keypublic': 'invalid key'}, user=self.test_user)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
app_settings.ALLOW_INSTANCE_MULTIPLE_OWNER = 'False'
|
||||
form = UserInstanceForm({
|
||||
'user': self.admin_user.id,
|
||||
'instance': self.instance.id,
|
||||
'is_change': False,
|
||||
'is_delete': False,
|
||||
'is_vnc': False,
|
||||
})
|
||||
form.save()
|
||||
form = UserInstanceForm({
|
||||
'user': self.test_user.id,
|
||||
'instance': self.instance.id,
|
||||
'is_change': False,
|
||||
'is_delete': False,
|
||||
'is_vnc': False,
|
||||
})
|
||||
self.assertFalse(form.is_valid())
|
||||
|
|
|
@ -5,6 +5,8 @@ from django_otp.forms import OTPAuthenticationForm
|
|||
|
||||
from . import views
|
||||
|
||||
app_name = 'accounts'
|
||||
|
||||
urlpatterns = [
|
||||
path('logout/', LogoutView.as_view(template_name='logout.html'), name='logout'),
|
||||
path('profile/', views.profile, name='profile'),
|
||||
|
@ -13,6 +15,8 @@ urlpatterns = [
|
|||
path('user_instance/create/<int:user_id>/', views.user_instance_create, name='user_instance_create'),
|
||||
path('user_instance/<int:pk>/update/', views.user_instance_update, name='user_instance_update'),
|
||||
path('user_instance/<int:pk>/delete/', views.user_instance_delete, name='user_instance_delete'),
|
||||
path('ssh_key/create/', views.ssh_key_create, name='ssh_key_create'),
|
||||
path('ssh_key/<int:pk>/delete/', views.ssh_key_delete, name='ssh_key_delete'),
|
||||
]
|
||||
|
||||
if settings.OTP_ENABLED:
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import base64
|
||||
import binascii
|
||||
import struct
|
||||
|
||||
from django_otp import devices_for_user
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
|
||||
|
@ -7,3 +11,29 @@ def get_user_totp_device(user):
|
|||
for device in devices:
|
||||
if isinstance(device, TOTPDevice):
|
||||
return device
|
||||
|
||||
|
||||
def validate_ssh_key(key):
|
||||
array = key.encode().split()
|
||||
# Each rsa-ssh key has 3 different strings in it, first one being
|
||||
# typeofkey second one being keystring third one being username .
|
||||
if len(array) != 3:
|
||||
return False
|
||||
typeofkey = array[0]
|
||||
string = array[1]
|
||||
username = array[2]
|
||||
# must have only valid rsa-ssh key characters ie binascii characters
|
||||
try:
|
||||
data = base64.decodestring(string)
|
||||
except binascii.Error:
|
||||
return False
|
||||
# unpack the contents of data, from data[:4] , property of ssh key .
|
||||
try:
|
||||
str_len = struct.unpack('>I', data[:4])[0]
|
||||
except struct.error:
|
||||
return False
|
||||
# data[4:str_len] must have string which matches with the typeofkey, another ssh key property.
|
||||
if data[4:4 + str_len] == typeofkey:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
from admin.decorators import superuser_only
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.contrib.auth.forms import PasswordChangeForm
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from instances.models import Instance
|
||||
|
||||
from accounts.forms import ProfileForm, UserSSHKeyForm
|
||||
from accounts.models import *
|
||||
|
||||
from . import forms
|
||||
|
@ -16,49 +17,50 @@ from .utils import get_user_totp_device
|
|||
|
||||
|
||||
def profile(request):
|
||||
error_messages = []
|
||||
publickeys = UserSSHKey.objects.filter(user_id=request.user.id)
|
||||
profile_form = ProfileForm(request.POST or None, instance=request.user)
|
||||
ssh_key_form = UserSSHKeyForm()
|
||||
|
||||
if request.method == "POST":
|
||||
if "username" in request.POST:
|
||||
username = request.POST.get("username", "")
|
||||
email = request.POST.get("email", "")
|
||||
request.user.first_name = username
|
||||
request.user.email = email
|
||||
request.user.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if "keyname" in request.POST:
|
||||
keyname = request.POST.get("keyname", "")
|
||||
keypublic = request.POST.get("keypublic", "")
|
||||
for key in publickeys:
|
||||
if keyname == key.keyname:
|
||||
msg = _("Key name already exist")
|
||||
error_messages.append(msg)
|
||||
if keypublic == key.keypublic:
|
||||
msg = _("Public key already exist")
|
||||
error_messages.append(msg)
|
||||
if "\n" in keypublic or "\r" in keypublic:
|
||||
msg = _("Invalid characters in public key")
|
||||
error_messages.append(msg)
|
||||
if not error_messages:
|
||||
addkeypublic = UserSSHKey(
|
||||
user_id=request.user.id,
|
||||
keyname=keyname,
|
||||
keypublic=keypublic,
|
||||
)
|
||||
addkeypublic.save()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
if "keydelete" in request.POST:
|
||||
keyid = request.POST.get("keyid", "")
|
||||
delkeypublic = UserSSHKey.objects.get(id=keyid)
|
||||
delkeypublic.delete()
|
||||
return HttpResponseRedirect(request.get_full_path())
|
||||
return render(request, "profile.html", locals())
|
||||
if profile_form.is_valid():
|
||||
profile_form.save()
|
||||
messages.success(request, _('Profile updated'))
|
||||
return redirect('accounts:profile')
|
||||
|
||||
return render(request, "profile.html", {
|
||||
'publickeys': publickeys,
|
||||
'profile_form': profile_form,
|
||||
'ssh_key_form': ssh_key_form,
|
||||
})
|
||||
|
||||
|
||||
def ssh_key_create(request):
|
||||
key_form = UserSSHKeyForm(request.POST or None, user=request.user)
|
||||
if key_form.is_valid():
|
||||
key_form.save()
|
||||
messages.success(request, _('SSH key added'))
|
||||
return redirect('accounts:profile')
|
||||
|
||||
return render(request, 'common/form.html', {
|
||||
'form': key_form,
|
||||
'title': _('Add SSH key'),
|
||||
})
|
||||
|
||||
|
||||
def ssh_key_delete(request, pk):
|
||||
ssh_key = get_object_or_404(UserSSHKey, pk=pk, user=request.user)
|
||||
if request.method == 'POST':
|
||||
ssh_key.delete()
|
||||
messages.success(request, _('SSH key deleted'))
|
||||
return redirect('accounts:profile')
|
||||
|
||||
return render(request, 'common/confirm_delete.html', {
|
||||
'object': ssh_key,
|
||||
'title': _('Delete SSH key'),
|
||||
})
|
||||
|
||||
|
||||
@superuser_only
|
||||
def account(request, user_id):
|
||||
error_messages = []
|
||||
user = User.objects.get(id=user_id)
|
||||
user_insts = UserInstance.objects.filter(user_id=user_id)
|
||||
instances = Instance.objects.all().order_by("name")
|
||||
|
@ -74,17 +76,14 @@ def account(request, user_id):
|
|||
|
||||
@permission_required("accounts.change_password", raise_exception=True)
|
||||
def change_password(request):
|
||||
if request.method == "POST":
|
||||
form = PasswordChangeForm(request.user, request.POST)
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
update_session_auth_hash(request, user) # Important!
|
||||
messages.success(request, _("Password Changed"))
|
||||
return redirect("profile")
|
||||
else:
|
||||
messages.error(request, _("Wrong Data Provided"))
|
||||
else:
|
||||
form = PasswordChangeForm(request.user)
|
||||
form = PasswordChangeForm(request.user, request.POST or None)
|
||||
|
||||
if form.is_valid():
|
||||
user = form.save()
|
||||
update_session_auth_hash(request, user) # Important!
|
||||
messages.success(request, _("Password Changed"))
|
||||
return redirect("accounts:profile")
|
||||
|
||||
return render(request, "accounts/change_password_form.html", {"form": form})
|
||||
|
||||
|
||||
|
@ -95,7 +94,7 @@ def user_instance_create(request, user_id):
|
|||
form = forms.UserInstanceForm(request.POST or None, initial={"user": user})
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(reverse("account", args=[user.id]))
|
||||
return redirect(reverse("accounts:account", args=[user.id]))
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -113,7 +112,7 @@ def user_instance_update(request, pk):
|
|||
form = forms.UserInstanceForm(request.POST or None, instance=user_instance)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect(reverse("account", args=[user_instance.user.id]))
|
||||
return redirect(reverse("accounts:account", args=[user_instance.user.id]))
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -135,7 +134,7 @@ def user_instance_delete(request, pk):
|
|||
if next:
|
||||
return redirect(next)
|
||||
else:
|
||||
return redirect(reverse("account", args=[user.id]))
|
||||
return redirect(reverse("accounts:account", args=[user.id]))
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
<td>{% if can_clone %}{% icon 'check' %}{% endif %}</td>
|
||||
<td>
|
||||
<div class="float-right btn-group">
|
||||
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'account' user.id %}">{% icon 'eye' %}</a>
|
||||
<a class="btn btn-success" title="{%trans "View Profile" %}" href="{% url 'accounts:account' user.id %}">{% icon 'eye' %}</a>
|
||||
<a class="btn btn-primary" title="{%trans "Edit" %}" href="{% url 'admin:user_update' user.id %}">{% icon 'pencil' %}</a>
|
||||
{% if user.is_active %}
|
||||
<a class="btn btn-warning" title="{%trans "Block" %}" href="{% url 'admin:user_block' user.id %}">{% icon 'stop' %}</a>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from .models import AppSettings
|
||||
|
||||
|
||||
class Settings(object):
|
||||
class Settings:
|
||||
pass
|
||||
|
||||
|
||||
|
|
120
conf/test-vm.xml
Normal file
120
conf/test-vm.xml
Normal file
|
@ -0,0 +1,120 @@
|
|||
<domain type="kvm">
|
||||
<name>test-vm</name>
|
||||
<uuid>1bd3c1f2-dd12-4b8d-a298-dff387cb9f93</uuid>
|
||||
<description>None</description>
|
||||
<memory unit="KiB">131072</memory>
|
||||
<currentMemory unit="KiB">131072</currentMemory>
|
||||
<vcpu placement="static">1</vcpu>
|
||||
<os>
|
||||
<type arch="x86_64" machine="pc-q35-5.1">hvm</type>
|
||||
<boot dev="hd" />
|
||||
<boot dev="cdrom" />
|
||||
<bootmenu enable="yes" />
|
||||
</os>
|
||||
<features>
|
||||
<acpi />
|
||||
<apic />
|
||||
</features>
|
||||
<cpu mode="host-model" check="partial" />
|
||||
<clock offset="utc" />
|
||||
<on_poweroff>destroy</on_poweroff>
|
||||
<on_reboot>restart</on_reboot>
|
||||
<on_crash>restart</on_crash>
|
||||
<devices>
|
||||
<emulator>/usr/bin/qemu-system-x86_64</emulator>
|
||||
<disk type="file" device="disk">
|
||||
<driver name="qemu" type="qcow2" cache="directsync" />
|
||||
<source file="/var/lib/libvirt/images/test-volume.qcow2" />
|
||||
<target dev="vda" bus="virtio" />
|
||||
<address type="pci" domain="0x0000" bus="0x03" slot="0x00" function="0x0" />
|
||||
</disk>
|
||||
<disk type="file" device="cdrom">
|
||||
<driver name="qemu" type="raw" />
|
||||
<target dev="sda" bus="sata" />
|
||||
<readonly />
|
||||
<address type="drive" controller="0" bus="0" target="0" unit="0" />
|
||||
</disk>
|
||||
<controller type="usb" index="0" model="qemu-xhci">
|
||||
<address type="pci" domain="0x0000" bus="0x02" slot="0x00" function="0x0" />
|
||||
</controller>
|
||||
<controller type="sata" index="0">
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x1f" function="0x2" />
|
||||
</controller>
|
||||
<controller type="pci" index="0" model="pcie-root" />
|
||||
<controller type="pci" index="1" model="pcie-root-port">
|
||||
<model name="pcie-root-port" />
|
||||
<target chassis="1" port="0x10" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x0" multifunction="on" />
|
||||
</controller>
|
||||
<controller type="pci" index="2" model="pcie-root-port">
|
||||
<model name="pcie-root-port" />
|
||||
<target chassis="2" port="0x11" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x1" />
|
||||
</controller>
|
||||
<controller type="pci" index="3" model="pcie-root-port">
|
||||
<model name="pcie-root-port" />
|
||||
<target chassis="3" port="0x12" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x2" />
|
||||
</controller>
|
||||
<controller type="pci" index="4" model="pcie-root-port">
|
||||
<model name="pcie-root-port" />
|
||||
<target chassis="4" port="0x13" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x3" />
|
||||
</controller>
|
||||
<controller type="pci" index="5" model="pcie-root-port">
|
||||
<model name="pcie-root-port" />
|
||||
<target chassis="5" port="0x14" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x4" />
|
||||
</controller>
|
||||
<controller type="pci" index="6" model="pcie-root-port">
|
||||
<model name="pcie-root-port" />
|
||||
<target chassis="6" port="0x15" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x5" />
|
||||
</controller>
|
||||
<controller type="pci" index="7" model="pcie-root-port">
|
||||
<model name="pcie-root-port" />
|
||||
<target chassis="7" port="0x16" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x6" />
|
||||
</controller>
|
||||
<controller type="pci" index="8" model="pcie-root-port">
|
||||
<model name="pcie-root-port" />
|
||||
<target chassis="8" port="0x17" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x02" function="0x7" />
|
||||
</controller>
|
||||
<interface type="network">
|
||||
<mac address="52:54:00:a2:3c:e7" />
|
||||
<source network="default" />
|
||||
<model type="virtio" />
|
||||
<address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x0" />
|
||||
</interface>
|
||||
<serial type="pty">
|
||||
<target type="isa-serial" port="0">
|
||||
<model name="isa-serial" />
|
||||
</target>
|
||||
</serial>
|
||||
<console type="pty">
|
||||
<target type="serial" port="0" />
|
||||
</console>
|
||||
<input type="mouse" bus="virtio">
|
||||
<address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0" />
|
||||
</input>
|
||||
<input type="keyboard" bus="virtio">
|
||||
<address type="pci" domain="0x0000" bus="0x06" slot="0x00" function="0x0" />
|
||||
</input>
|
||||
<input type="tablet" bus="virtio">
|
||||
<address type="pci" domain="0x0000" bus="0x07" slot="0x00" function="0x0" />
|
||||
</input>
|
||||
<input type="mouse" bus="ps2" />
|
||||
<input type="keyboard" bus="ps2" />
|
||||
<graphics type="spice" autoport="yes" listen="0.0.0.0">
|
||||
<listen type="address" address="0.0.0.0" />
|
||||
</graphics>
|
||||
<video>
|
||||
<model type="vga" vram="16384" heads="1" primary="yes" />
|
||||
<address type="pci" domain="0x0000" bus="0x00" slot="0x01" function="0x0" />
|
||||
</video>
|
||||
<memballoon model="virtio">
|
||||
<address type="pci" domain="0x0000" bus="0x04" slot="0x00" function="0x0" />
|
||||
</memballoon>
|
||||
</devices>
|
||||
</domain>
|
|
@ -204,6 +204,10 @@ class Instance(models.Model):
|
|||
def formats(self):
|
||||
return self.proxy.get_image_formats()
|
||||
|
||||
@cached_property
|
||||
def interfaces(self):
|
||||
return self.proxy.get_ifaces()
|
||||
|
||||
|
||||
class PermissionSet(models.Model):
|
||||
"""
|
||||
|
@ -211,8 +215,9 @@ class PermissionSet(models.Model):
|
|||
"""
|
||||
class Meta:
|
||||
default_permissions = ()
|
||||
permissions = [('clone_instances', 'Can clone instances'),
|
||||
('passwordless_console', _('Can access console without password')),
|
||||
]
|
||||
permissions = [
|
||||
('clone_instances', 'Can clone instances'),
|
||||
('passwordless_console', _('Can access console without password')),
|
||||
]
|
||||
|
||||
managed = False
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
{% for c_net in networks_host %}
|
||||
<option value="net:{{ c_net }}">Network {{ c_net }}</option>
|
||||
{% endfor %}
|
||||
{% for c_iface in interfaces_host %}
|
||||
{% for c_iface in instance.interfaces %}
|
||||
<option value="iface:{{ c_iface }}">Interface {{ c_iface }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
|
|
@ -393,7 +393,7 @@
|
|||
{% for c_net in networks_host %}
|
||||
<option value="net:{{ c_net }}" {% if c_net == network.nic %} selected {% endif %}>{% trans 'Network' %} {{ c_net }}</option>
|
||||
{% endfor %}
|
||||
{% for c_iface in interfaces_host %}
|
||||
{% for c_iface in instance.interfaces %}
|
||||
<option value="iface:{{ c_iface }}" {% if c_iface == network.nic %} selected {% endif %}>{% trans 'Interface' %} {{ c_iface }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
@ -657,9 +657,9 @@
|
|||
<tbody class="searchable">
|
||||
{% for userinstance in userinstances %}
|
||||
<tr>
|
||||
<td><a href="{% url 'account' userinstance.user.id %}">{{ userinstance.user }}</a></td>
|
||||
<td><a href="{% url 'accounts:account' userinstance.user.id %}">{{ userinstance.user }}</a></td>
|
||||
<td style="width:30px;">
|
||||
<a href="{% url 'user_instance_delete' userinstance.id %}?next={% url 'instances:instance' instance.id %}#users">
|
||||
<a href="{% url 'accounts:user_instance_delete' userinstance.id %}?next={% url 'instances:instance' instance.id %}#users">
|
||||
{% icon 'trash' %}
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -116,7 +116,7 @@ class InstancesTestCase(TestCase):
|
|||
self.assertEqual(instance.disks[0]['size'], 1024**3)
|
||||
|
||||
response = self.client.post(reverse('instances:resize_disk', args=[instance.id]), {
|
||||
'disk_size_vda': '2.0 GB',
|
||||
'disk_size_vda': '2',
|
||||
})
|
||||
self.assertRedirects(response, reverse('instances:instance', args=[instance.id]) + '#resize')
|
||||
|
||||
|
@ -449,9 +449,8 @@ class InstancesTestCase(TestCase):
|
|||
response = self.client.post(
|
||||
reverse('instances:destroy', args=[instance.id]),
|
||||
{'delete_disk': True},
|
||||
HTTP_REFERER=reverse('index'),
|
||||
)
|
||||
self.assertRedirects(response, reverse('instances', args=[compute.id]))
|
||||
self.assertRedirects(response, reverse('instances:index'))
|
||||
|
||||
# # create volume
|
||||
# response = self.client.post(
|
||||
|
|
|
@ -3,17 +3,25 @@
|
|||
{% load icons %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{%trans "Delete" %}{% endblock %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block page_header %}{{ title }}{% endblock page_header %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="alert alert-warning">
|
||||
{%trans "Are you sure you want to delete" %} "{{ object }}"?
|
||||
<div class="row">
|
||||
<div class="offset-3 col-6">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="alert alert-warning">
|
||||
{%trans "Are you sure you want to delete" %} "{{ object }}"?
|
||||
</div>
|
||||
<div class="form-group mb-0 float-right">
|
||||
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
{% icon 'trash' %} {% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<a class="btn btn-primary" href="javascript:history.back()">{% icon 'times' %} {% trans "Cancel" %}</a>
|
||||
<button type="submit" class="btn btn-danger">
|
||||
{% icon 'check' %} {% trans "Delete" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -42,9 +42,9 @@
|
|||
<a class="dropdown-item disabled" href="#">
|
||||
{% trans "Language" %}: <span class="badge badge-secondary">{{ LANGUAGE_CODE }}</span>
|
||||
</a>
|
||||
<a class="dropdown-item {% view_active request 'profile' %}" href="{% url 'profile' %}">{% icon 'vcard' %} {% trans "Profile" %}</a>
|
||||
<a class="dropdown-item {% view_active request 'accounts:profile' %}" href="{% url 'accounts:profile' %}">{% icon 'vcard' %} {% trans "Profile" %}</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" href="{% url 'logout' %}"><i class="fa fa-fw fa-power-off"></i> {% trans "Log Out" %}</a>
|
||||
<a class="dropdown-item" href="{% url 'accounts:logout' %}"><i class="fa fa-fw fa-power-off"></i> {% trans "Log Out" %}</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
Loading…
Reference in a new issue