mirror of https://github.com/jumpserver/jumpserver
Merge branch 'master' of code.simcu.com:jumpserver/jumpserver
commit
7cff5cbf61
|
@ -1,6 +1,7 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyo
|
*.pyo
|
||||||
|
*.swp
|
||||||
env
|
env
|
||||||
env*
|
env*
|
||||||
dist
|
dist
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# coding:utf-8
|
# coding:utf-8
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from .models import IDC, Asset, AssetGroup
|
from .models import IDC, Asset, AssetGroup, AdminUser, SystemUser
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ class AssetForm(forms.ModelForm):
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
"ip", "other_ip", "remote_card_ip", "hostname", "port", "groups", "username", "password",
|
"ip", "other_ip", "remote_card_ip", "hostname", "port", "groups", "username", "password",
|
||||||
"idc", "mac_addr", "brand", "cpu", "memory", "disk", "os", "cabinet_no", "cabinet_pos",
|
"idc", "mac_address", "brand", "cpu", "memory", "disk", "os", "cabinet_no", "cabinet_pos",
|
||||||
"number", "status", "type", "env", "sn", "is_active", "comment"
|
"number", "status", "type", "env", "sn", "is_active", "comment"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ class AssetForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class AssetGroupForm(forms.ModelForm):
|
class AssetGroupForm(forms.ModelForm):
|
||||||
|
# See AdminUserForm comment same it
|
||||||
assets = forms.ModelMultipleChoiceField(queryset=Asset.objects.all(),
|
assets = forms.ModelMultipleChoiceField(queryset=Asset.objects.all(),
|
||||||
label=_('Asset'),
|
label=_('Asset'),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -51,6 +52,7 @@ class AssetGroupForm(forms.ModelForm):
|
||||||
|
|
||||||
|
|
||||||
class IDCForm(forms.ModelForm):
|
class IDCForm(forms.ModelForm):
|
||||||
|
# See AdminUserForm comment same it
|
||||||
assets = forms.ModelMultipleChoiceField(queryset=Asset.objects.all(),
|
assets = forms.ModelMultipleChoiceField(queryset=Asset.objects.all(),
|
||||||
label=_('Asset'),
|
label=_('Asset'),
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -78,3 +80,135 @@ class IDCForm(forms.ModelForm):
|
||||||
'network': forms.Textarea(
|
'network': forms.Textarea(
|
||||||
attrs={'placeholder': '192.168.1.0/24\n192.168.2.0/24'})
|
attrs={'placeholder': '192.168.1.0/24\n192.168.2.0/24'})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AdminUserForm(forms.ModelForm):
|
||||||
|
# Admin user assets define, let user select, save it in form not in view
|
||||||
|
assets = forms.ModelMultipleChoiceField(queryset=Asset.objects.all(),
|
||||||
|
label=_('Asset'),
|
||||||
|
required=False,
|
||||||
|
widget=forms.SelectMultiple(
|
||||||
|
attrs={'class': 'select2', 'data-placeholder': _('Select assets')})
|
||||||
|
)
|
||||||
|
auto_generate_key = forms.BooleanField(required=True, initial=True)
|
||||||
|
# Form field name can not start with `_`, so redefine it,
|
||||||
|
password = forms.CharField(widget=forms.PasswordInput, max_length=100, min_length=8, strip=True,
|
||||||
|
help_text=_('If also set private key, use that first'), required=False)
|
||||||
|
# Need use upload private key file except paste private key content
|
||||||
|
private_key_file = forms.FileField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# When update a admin user instance, initial it
|
||||||
|
if kwargs.get('instance'):
|
||||||
|
initial = kwargs.get('initial', {})
|
||||||
|
initial['assets'] = kwargs['instance'].assets.all()
|
||||||
|
super(AdminUserForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _save_m2m(self):
|
||||||
|
# Save assets relation with admin user
|
||||||
|
super(AdminUserForm, self)._save_m2m()
|
||||||
|
assets = self.cleaned_data['assets']
|
||||||
|
self.instance.assets.clear()
|
||||||
|
self.instance.assets.add(*tuple(assets))
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
# Because we define custom field, so we need rewrite :method: `save`
|
||||||
|
admin_user = super(AdminUserForm, self).save(commit=commit)
|
||||||
|
password = self.cleaned_data['password']
|
||||||
|
private_key_file = self.cleaned_data['private_key_file']
|
||||||
|
|
||||||
|
if password:
|
||||||
|
admin_user.password = password
|
||||||
|
print(password)
|
||||||
|
# Todo: Validate private key file, and generate public key
|
||||||
|
# Todo: Auto generate private key and public key
|
||||||
|
if private_key_file:
|
||||||
|
admin_user.private_key = private_key_file.read()
|
||||||
|
admin_user.save()
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = AdminUser
|
||||||
|
fields = ['name', 'username', 'auto_generate_key', 'password', 'private_key_file', 'as_default', 'comment']
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||||
|
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
'name': '* required',
|
||||||
|
'username': '* required',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserForm(forms.ModelForm):
|
||||||
|
# Admin user assets define, let user select, save it in form not in view
|
||||||
|
assets = forms.ModelMultipleChoiceField(queryset=Asset.objects.all(),
|
||||||
|
label=_('Asset'),
|
||||||
|
required=False,
|
||||||
|
widget=forms.SelectMultiple(
|
||||||
|
attrs={'class': 'select2', 'data-placeholder': _('Select assets')})
|
||||||
|
)
|
||||||
|
asset_groups = forms.ModelMultipleChoiceField(queryset=AssetGroup.objects.all(),
|
||||||
|
label=_('Asset group'),
|
||||||
|
required=False,
|
||||||
|
widget=forms.SelectMultiple(
|
||||||
|
attrs={'class': 'select2',
|
||||||
|
'data-placeholder': _('Select asset groups')})
|
||||||
|
)
|
||||||
|
auto_generate_key = forms.BooleanField(initial=True)
|
||||||
|
# Form field name can not start with `_`, so redefine it,
|
||||||
|
password = forms.CharField(widget=forms.PasswordInput, max_length=100, min_length=8, strip=True,
|
||||||
|
help_text=_('If also set private key, use that first'), required=False)
|
||||||
|
# Need use upload private key file except paste private key content
|
||||||
|
private_key_file = forms.FileField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# When update a admin user instance, initial it
|
||||||
|
if kwargs.get('instance'):
|
||||||
|
initial = kwargs.get('initial', {})
|
||||||
|
initial['assets'] = kwargs['instance'].assets.all()
|
||||||
|
initial['asset_groups'] = kwargs['instance'].asset_groups.all()
|
||||||
|
super(SystemUserForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _save_m2m(self):
|
||||||
|
# Save assets relation with admin user
|
||||||
|
super(SystemUserForm, self)._save_m2m()
|
||||||
|
assets = self.cleaned_data['assets']
|
||||||
|
asset_groups = self.cleaned_data['asset_groups']
|
||||||
|
self.instance.assets.clear()
|
||||||
|
self.instance.assets.add(*tuple(assets))
|
||||||
|
self.instance.asset_groups.clear()
|
||||||
|
self.instance.asset_groups.add(*tuple(asset_groups))
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
# Because we define custom field, so we need rewrite :method: `save`
|
||||||
|
system_user = super(SystemUserForm, self).save(commit=commit)
|
||||||
|
password = self.cleaned_data['password']
|
||||||
|
private_key_file = self.cleaned_data['private_key_file']
|
||||||
|
|
||||||
|
if password:
|
||||||
|
system_user.password = password
|
||||||
|
print(password)
|
||||||
|
# Todo: Validate private key file, and generate public key
|
||||||
|
# Todo: Auto generate private key and public key
|
||||||
|
if private_key_file:
|
||||||
|
system_user.private_key = private_key_file.read()
|
||||||
|
system_user.save()
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SystemUser
|
||||||
|
fields = [
|
||||||
|
'name', 'username', 'protocol', 'auto_generate_key', 'password', 'private_key_file', 'as_default',
|
||||||
|
'auto_push', 'auto_update', 'sudo', 'comment', 'shell', 'home', 'uid',
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={'placeholder': _('Name')}),
|
||||||
|
'username': forms.TextInput(attrs={'placeholder': _('Username')}),
|
||||||
|
}
|
||||||
|
help_texts = {
|
||||||
|
'name': '* required',
|
||||||
|
'username': '* required',
|
||||||
|
'auth_push': 'Auto push system user to asset',
|
||||||
|
'auth_update': 'Auto update system user ssh key',
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.10 on 2016-09-07 15:11
|
# Generated by Django 1.10 on 2016-09-08 03:02
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
@ -18,15 +18,15 @@ class Migration(migrations.Migration):
|
||||||
name='AdminUser',
|
name='AdminUser',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('name', models.CharField(blank=True, max_length=128, null=True, unique=True, verbose_name='Name')),
|
('name', models.CharField(max_length=128, unique=True, verbose_name='Name')),
|
||||||
('username', models.CharField(blank=True, max_length=16, null=True, verbose_name='Username')),
|
('username', models.CharField(max_length=16, verbose_name='Username')),
|
||||||
('password', models.CharField(blank=True, max_length=256, null=True, verbose_name='Password')),
|
('_password', models.CharField(blank=True, max_length=256, verbose_name='Password')),
|
||||||
('private_key', models.CharField(blank=True, max_length=4096, null=True, verbose_name='SSH private key')),
|
('_private_key', models.CharField(blank=True, max_length=4096, verbose_name='SSH private key')),
|
||||||
('is_default', models.BooleanField(default=True, verbose_name='As default')),
|
('_public_key', models.CharField(blank=True, max_length=4096, verbose_name='SSH public key')),
|
||||||
('auto_update', models.BooleanField(default=True, verbose_name='Auto update pass/key')),
|
('as_default', models.BooleanField(default=False, verbose_name='As default')),
|
||||||
('date_created', models.DateTimeField(auto_now=True, null=True)),
|
|
||||||
('create_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
|
||||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||||
|
('date_created', models.DateTimeField(auto_now=True, null=True)),
|
||||||
|
('created_by', models.CharField(max_length=32, null=True, verbose_name='Created by')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'db_table': 'admin_user',
|
'db_table': 'admin_user',
|
||||||
|
@ -142,7 +142,7 @@ class Migration(migrations.Migration):
|
||||||
('home', models.CharField(blank=True, max_length=64, verbose_name='Home')),
|
('home', models.CharField(blank=True, max_length=64, verbose_name='Home')),
|
||||||
('uid', models.IntegerField(blank=True, verbose_name='Uid')),
|
('uid', models.IntegerField(blank=True, verbose_name='Uid')),
|
||||||
('date_created', models.DateTimeField(auto_now=True, null=True)),
|
('date_created', models.DateTimeField(auto_now=True, null=True)),
|
||||||
('create_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
|
('created_by', models.CharField(blank=True, max_length=32, verbose_name='Created by')),
|
||||||
('comment', models.CharField(blank=True, max_length=128, verbose_name='Comment')),
|
('comment', models.CharField(blank=True, max_length=128, verbose_name='Comment')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
|
|
@ -61,22 +61,39 @@ class AssetExtend(models.Model):
|
||||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return '%(key)s: %(value)s' % {'key': self.key, 'value': self.value}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def initial(cls):
|
||||||
|
for k, v in (
|
||||||
|
(_('status'), _('In use')),
|
||||||
|
(_('status'), _('Out of use')),
|
||||||
|
(_('type'), _('Server')),
|
||||||
|
(_('type'), _('VM')),
|
||||||
|
(_('type'), _('Switch')),
|
||||||
|
(_('type'), _('Router')),
|
||||||
|
(_('type'), _('Firewall')),
|
||||||
|
(_('type'), _('Storage')),
|
||||||
|
(_('env'), _('Production')),
|
||||||
|
(_('env'), _('Development')),
|
||||||
|
(_('env'), _('Testing')),
|
||||||
|
):
|
||||||
|
cls.objects.create(key=k, value=v, created_by='System')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'asset_extend'
|
db_table = 'asset_extend'
|
||||||
|
|
||||||
|
|
||||||
class AdminUser(models.Model):
|
class AdminUser(models.Model):
|
||||||
name = models.CharField(max_length=128, unique=True, null=True, blank=True, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||||
username = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Username'))
|
username = models.CharField(max_length=16, verbose_name=_('Username'))
|
||||||
_password = models.CharField(max_length=256, null=True, blank=True, verbose_name=_('Password'))
|
_password = models.CharField(max_length=256, blank=True, verbose_name=_('Password'))
|
||||||
_private_key = models.CharField(max_length=4096, null=True, blank=True, verbose_name=_('SSH private key'))
|
_private_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH private key'))
|
||||||
_public_key = models.CharField(max_length=4096, null=True, blank=True, verbose_name=_('SSH public key'))
|
_public_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
|
||||||
as_default = models.BooleanField(default=True, verbose_name=_('As default'))
|
as_default = models.BooleanField(default=False, verbose_name=_('As default'))
|
||||||
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(blank=True, verbose_name=_('Comment'))
|
||||||
date_created = models.DateTimeField(auto_now=True, null=True, blank=True)
|
date_created = models.DateTimeField(auto_now=True, null=True)
|
||||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
created_by = models.CharField(max_length=32, null=True, verbose_name=_('Created by'))
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -110,7 +127,7 @@ class AdminUser(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_fake(cls, count=100):
|
def generate_fake(cls, count=100):
|
||||||
from random import seed, choice
|
from random import seed
|
||||||
import forgery_py
|
import forgery_py
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
|
||||||
|
@ -132,31 +149,74 @@ class AdminUser(models.Model):
|
||||||
class SystemUser(models.Model):
|
class SystemUser(models.Model):
|
||||||
PROTOCOL_CHOICES = (
|
PROTOCOL_CHOICES = (
|
||||||
('ssh', 'ssh'),
|
('ssh', 'ssh'),
|
||||||
('telnet', 'telnet'),
|
|
||||||
)
|
)
|
||||||
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
name = models.CharField(max_length=128, unique=True, verbose_name=_('Name'))
|
||||||
username = models.CharField(max_length=16, blank=True, verbose_name=_('Username'))
|
username = models.CharField(max_length=16, verbose_name=_('Username'))
|
||||||
password = models.CharField(max_length=256, blank=True, verbose_name=_('Password'))
|
_password = models.CharField(max_length=256, blank=True, verbose_name=_('Password'))
|
||||||
protocol = models.CharField(max_length=16, default='ssh', verbose_name=_('Protocol'))
|
protocol = models.CharField(max_length=16, choices=PROTOCOL_CHOICES, default='ssh', verbose_name=_('Protocol'))
|
||||||
private_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH private key'))
|
_private_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH private key'))
|
||||||
public_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
|
_public_key = models.CharField(max_length=4096, blank=True, verbose_name=_('SSH public key'))
|
||||||
is_default = models.BooleanField(default=True, verbose_name=_('As default'))
|
as_default = models.BooleanField(default=False, verbose_name=_('As default'))
|
||||||
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
auto_push = models.BooleanField(default=True, verbose_name=_('Auto push'))
|
||||||
auto_update = models.BooleanField(default=True, verbose_name=_('Auto update pass/key'))
|
auto_update = models.BooleanField(default=True, verbose_name=_('Auto update pass/key'))
|
||||||
sudo = models.TextField(max_length=4096, blank=True, verbose_name=_('Sudo'))
|
sudo = models.TextField(max_length=4096, default='/user/bin/whoami', verbose_name=_('Sudo'))
|
||||||
shell = models.CharField(max_length=64, blank=True, verbose_name=_('Shell'))
|
shell = models.CharField(max_length=64, default='/bin/bash', verbose_name=_('Shell'))
|
||||||
home = models.CharField(max_length=64, blank=True, verbose_name=_('Home'))
|
home = models.CharField(max_length=64, blank=True, verbose_name=_('Home'))
|
||||||
uid = models.IntegerField(blank=True, verbose_name=_('Uid'))
|
uid = models.IntegerField(null=True, blank=True, verbose_name=_('Uid'))
|
||||||
date_created = models.DateTimeField(auto_now=True, null=True)
|
date_created = models.DateTimeField(auto_now=True)
|
||||||
created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))
|
created_by = models.CharField(max_length=32, blank=True, verbose_name=_('Created by'))
|
||||||
comment = models.CharField(max_length=128, blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(max_length=128, blank=True, verbose_name=_('Comment'))
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def password(self):
|
||||||
|
return decrypt(self._password)
|
||||||
|
|
||||||
|
@password.setter
|
||||||
|
def password(self, password_raw):
|
||||||
|
self._password = encrypt(password_raw)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def private_key(self):
|
||||||
|
return decrypt(self._private_key)
|
||||||
|
|
||||||
|
@private_key.setter
|
||||||
|
def private_key(self, private_key_raw):
|
||||||
|
self._private_key = encrypt(private_key_raw)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def public_key(self):
|
||||||
|
return decrypt(self._public_key)
|
||||||
|
|
||||||
|
@public_key.setter
|
||||||
|
def public_key(self, public_key_raw):
|
||||||
|
self._public_key = encrypt(public_key_raw)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = 'system_user'
|
db_table = 'system_user'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_fake(cls, count=100):
|
||||||
|
from random import seed
|
||||||
|
import forgery_py
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
|
seed()
|
||||||
|
for i in range(count):
|
||||||
|
obj = cls(name=forgery_py.name.full_name(),
|
||||||
|
username=forgery_py.internet.user_name(),
|
||||||
|
password=forgery_py.lorem_ipsum.word(),
|
||||||
|
comment=forgery_py.lorem_ipsum.sentence(),
|
||||||
|
created_by='Fake')
|
||||||
|
try:
|
||||||
|
obj.save()
|
||||||
|
logger.debug('Generate fake asset group: %s' % obj.name)
|
||||||
|
except IntegrityError:
|
||||||
|
print('Error continue')
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
class AssetGroup(models.Model):
|
class AssetGroup(models.Model):
|
||||||
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))
|
name = models.CharField(max_length=64, unique=True, verbose_name=_('Name'))
|
||||||
|
@ -204,10 +264,11 @@ class Asset(models.Model):
|
||||||
groups = models.ManyToManyField(AssetGroup, related_name='assets', verbose_name=_('Asset groups'))
|
groups = models.ManyToManyField(AssetGroup, related_name='assets', verbose_name=_('Asset groups'))
|
||||||
username = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Admin user'))
|
username = models.CharField(max_length=16, null=True, blank=True, verbose_name=_('Admin user'))
|
||||||
password = models.CharField(max_length=256, null=True, blank=True, verbose_name=_("Admin password"))
|
password = models.CharField(max_length=256, null=True, blank=True, verbose_name=_("Admin password"))
|
||||||
admin_user = models.ForeignKey(AdminUser, null=True, on_delete=models.SET_NULL, verbose_name=_("Admin user"))
|
admin_user = models.ForeignKey(AdminUser, null=True, related_name='assets',
|
||||||
system_user = models.ManyToManyField(SystemUser, blank=True, verbose_name=_("System User"))
|
on_delete=models.SET_NULL, verbose_name=_("Admin user"))
|
||||||
|
system_user = models.ManyToManyField(SystemUser, blank=True, related_name='assets', verbose_name=_("System User"))
|
||||||
idc = models.ForeignKey(IDC, null=True, related_name='assets', on_delete=models.SET_NULL, verbose_name=_('IDC'))
|
idc = models.ForeignKey(IDC, null=True, related_name='assets', on_delete=models.SET_NULL, verbose_name=_('IDC'))
|
||||||
mac_addr = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("Mac address"))
|
mac_address = models.CharField(max_length=20, null=True, blank=True, verbose_name=_("Mac address"))
|
||||||
brand = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Brand'))
|
brand = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('Brand'))
|
||||||
cpu = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU'))
|
cpu = models.CharField(max_length=64, null=True, blank=True, verbose_name=_('CPU'))
|
||||||
memory = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Memory'))
|
memory = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Memory'))
|
||||||
|
@ -226,7 +287,7 @@ class Asset(models.Model):
|
||||||
comment = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Comment'))
|
comment = models.CharField(max_length=128, null=True, blank=True, verbose_name=_('Comment'))
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return '%(ip)s:%(port)d' % {'ip': self.ip, 'port': self.port}
|
return '%(ip)s:%(port)s' % {'ip': self.ip, 'port': self.port}
|
||||||
|
|
||||||
def initial(self):
|
def initial(self):
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
{% block custom_head_css_js %}
|
||||||
|
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||||
|
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper wrapper-content animated fadeInRight">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="ibox-title">
|
||||||
|
<h5>{% trans 'Create admin user' %}</h5>
|
||||||
|
<div class="ibox-tools">
|
||||||
|
<a class="collapse-link">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</a>
|
||||||
|
<a class="close-link">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ibox-content">
|
||||||
|
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.name|bootstrap_horizontal }}
|
||||||
|
{{ form.username|bootstrap_horizontal }}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{{ form.auto_generate_key}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ form.password|bootstrap_horizontal }}
|
||||||
|
{{ form.private_key_file|bootstrap_horizontal }}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.as_default.id_for_label }}" class="col-sm-2 control-label">{% trans 'As default' %}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{{ form.as_default}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ form.assets|bootstrap_horizontal }}
|
||||||
|
{{ form.comment|bootstrap_horizontal }}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-4 col-sm-offset-2">
|
||||||
|
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||||
|
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_foot_js %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('.select2').select2();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,220 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load common_tags %}
|
||||||
|
{% load users_tags %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block custom_head_css_js %}
|
||||||
|
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||||
|
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper wrapper-content animated fadeInRight">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="panel-options">
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="active"><a href="" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||||
|
</li>
|
||||||
|
<li><a href="" class="text-center"><i class="fa fa-bar-chart-o"></i> {% trans 'Associate assets' %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="col-sm-7" style="padding-left: 0;">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="ibox-title">
|
||||||
|
<span class="label"><b>{{ admin_user.name }}</b></span>
|
||||||
|
<div class="ibox-tools">
|
||||||
|
<a class="collapse-link">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-user">
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a class="close-link">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ibox-content">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td>{% trans 'Name' %}:</td>
|
||||||
|
<td><b>{{ admin_user.name }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Username' %}:</td>
|
||||||
|
<td><b>{{ admin_user.username }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Date created' %}:</td>
|
||||||
|
<td><b>{{ admin_user.date_created }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Created by' %}:</td>
|
||||||
|
<td><b>{{ asset_group.created_by }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Comment' %}:</td>
|
||||||
|
<td><b>{{ admin_user.comment }}</b></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="ibox-title">
|
||||||
|
<span style="float: left">{% trans 'Asset list of ' %} <b>{{ admin_user.name }}</b></span>
|
||||||
|
<div class="ibox-tools">
|
||||||
|
<a class="collapse-link">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-user">
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a class="close-link">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ibox-content">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans 'Hostname' %}</th>
|
||||||
|
<th>{% trans 'IP' %}</th>
|
||||||
|
<th>{% trans 'Port' %}</th>
|
||||||
|
<th>{% trans 'Alive' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for asset in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ asset.hostname }}</td>
|
||||||
|
<td>{{ asset.ip }}</td>
|
||||||
|
<td>{{ asset.port }}</td>
|
||||||
|
<td>Alive</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="row">
|
||||||
|
{% include '_pagination.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td width="50%">{% trans 'Get install script' %}:</td>
|
||||||
|
<td>
|
||||||
|
<span style="float: right">
|
||||||
|
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Get' %}</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%">{% trans 'Retest asset connectivity' %}:</td>
|
||||||
|
<td>
|
||||||
|
<span style="float: right">
|
||||||
|
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Start' %}</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%">{% trans 'Reset private key' %}:</td>
|
||||||
|
<td>
|
||||||
|
<span style="float: right">
|
||||||
|
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Reset' %}</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-info">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<i class="fa fa-info-circle"></i> {% trans 'Replace asset admin user with this' %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<form>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td colspan="2">
|
||||||
|
<select data-placeholder="{% trans 'Select asset' %}" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||||
|
{% for group in groups %}
|
||||||
|
<option value="{{ group.id }}">{{ group.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td colspan="2">
|
||||||
|
<button type="button" class="btn btn-info btn-sm">{% trans 'Replace' %}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</form>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_foot_js %}
|
||||||
|
<script>
|
||||||
|
{# function switch_user_status(obj) {#}
|
||||||
|
{# var status = $(obj).prop('checked');#}
|
||||||
|
{##}
|
||||||
|
{# $.ajax({#}
|
||||||
|
{# url: "{% url 'users:user-active-api' pk=user.id %}",#}
|
||||||
|
{# type: "PUT",#}
|
||||||
|
{# data: {#}
|
||||||
|
{# 'is_active': status#}
|
||||||
|
{# },#}
|
||||||
|
{# success: function (data, status) {#}
|
||||||
|
{# console.log(data)#}
|
||||||
|
{# },#}
|
||||||
|
{# error: function () {#}
|
||||||
|
{# console.log('error')#}
|
||||||
|
{# }#}
|
||||||
|
{# })#}
|
||||||
|
{# }#}
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('.select2').select2();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -20,7 +20,7 @@
|
||||||
<tr class="gradeX">
|
<tr class="gradeX">
|
||||||
<td class="text-center">{{ admin_user.id }}</td>
|
<td class="text-center">{{ admin_user.id }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'users:user-detail' pk=user.id %}">
|
<a href="{% url 'assets:admin-user-detail' pk=admin_user.id %}">
|
||||||
{{ admin_user.name }}
|
{{ admin_user.name }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -69,7 +69,7 @@
|
||||||
|
|
||||||
<div class="ibox float-e-margins">
|
<div class="ibox float-e-margins">
|
||||||
<div class="ibox-title">
|
<div class="ibox-title">
|
||||||
<span>{% trans 'Asset list of ' %} <b>{{ asset_group.name }}</b></span>
|
<span style="float: left"></span>{% trans 'Asset list of ' %} <b>{{ asset_group.name }}</b></span>
|
||||||
<div class="ibox-tools">
|
<div class="ibox-tools">
|
||||||
<a class="collapse-link">
|
<a class="collapse-link">
|
||||||
<i class="fa fa-chevron-up"></i>
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||||
<div class="panel panel-primary">
|
<div class="panel panel-primary">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<i class="fa fa-info-circle"></i> {% trans 'Associate asset user(system/admin)' %}
|
<i class="fa fa-info-circle"></i> {% trans 'Associate system user' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
@ -207,23 +207,23 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block custom_foot_js %}
|
{% block custom_foot_js %}
|
||||||
<script>
|
<script>
|
||||||
function switch_user_status(obj) {
|
{# function switch_user_status(obj) {#}
|
||||||
var status = $(obj).prop('checked');
|
{# var status = $(obj).prop('checked');#}
|
||||||
|
{##}
|
||||||
$.ajax({
|
{# $.ajax({#}
|
||||||
url: "{% url 'users:user-active-api' pk=user.id %}",
|
{# url: "{% url 'users:user-active-api' pk=user.id %}",#}
|
||||||
type: "PUT",
|
{# type: "PUT",#}
|
||||||
data: {
|
{# data: {#}
|
||||||
'is_active': status
|
{# 'is_active': status#}
|
||||||
},
|
{# },#}
|
||||||
success: function (data, status) {
|
{# success: function (data, status) {#}
|
||||||
console.log(data)
|
{# console.log(data)#}
|
||||||
},
|
{# },#}
|
||||||
error: function () {
|
{# error: function () {#}
|
||||||
console.log('error')
|
{# console.log('error')#}
|
||||||
}
|
{# }#}
|
||||||
})
|
{# })#}
|
||||||
}
|
{# }#}
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('.select2').select2();
|
$('.select2').select2();
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load common_tags %}
|
||||||
|
{% load users_tags %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block custom_head_css_js %}
|
||||||
|
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||||
|
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper wrapper-content animated fadeInRight">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="panel-options">
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||||
|
</li>
|
||||||
|
<li class="active"><a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
|
||||||
|
<i class="fa fa-bar-chart-o"></i> {% trans 'Associate assets' %}</a>
|
||||||
|
</li>
|
||||||
|
<li><a href="{% url 'assets:system-user-asset-group' pk=system_user.id %}" class="text-center">
|
||||||
|
<i class="fa fa-bar-chart-o"></i> {% trans 'Associate asset groups' %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="col-sm-7" style="padding-left: 0;">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="ibox-title">
|
||||||
|
<span style="float: left">{% trans 'Asset list of ' %} <b>{{ admin_user.name }}</b></span>
|
||||||
|
<div class="ibox-tools">
|
||||||
|
<a class="collapse-link">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-user">
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a class="close-link">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ibox-content">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans 'Hostname' %}</th>
|
||||||
|
<th>{% trans 'IP' %}</th>
|
||||||
|
<th>{% trans 'Port' %}</th>
|
||||||
|
<th>{% trans 'Alive' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for asset in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ asset.hostname }}</td>
|
||||||
|
<td>{{ asset.ip }}</td>
|
||||||
|
<td>{{ asset.port }}</td>
|
||||||
|
<td>Alive</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="row">
|
||||||
|
{% include '_pagination.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<i class="fa fa-info-circle"></i> {% trans 'Add asset to this system user' %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<form>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td colspan="2">
|
||||||
|
<select data-placeholder="{% trans 'Select asset' %}" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||||
|
{% for asset in assets %}
|
||||||
|
<option value="{{ asset.id }}">{{ asset.ip}}:{{ asset.port }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td colspan="2">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm">{% trans 'Add' %}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</form>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_foot_js %}
|
||||||
|
<script>
|
||||||
|
{# function switch_user_status(obj) {#}
|
||||||
|
{# var status = $(obj).prop('checked');#}
|
||||||
|
{##}
|
||||||
|
{# $.ajax({#}
|
||||||
|
{# url: "{% url 'users:user-active-api' pk=user.id %}",#}
|
||||||
|
{# type: "PUT",#}
|
||||||
|
{# data: {#}
|
||||||
|
{# 'is_active': status#}
|
||||||
|
{# },#}
|
||||||
|
{# success: function (data, status) {#}
|
||||||
|
{# console.log(data)#}
|
||||||
|
{# },#}
|
||||||
|
{# error: function () {#}
|
||||||
|
{# console.log('error')#}
|
||||||
|
{# }#}
|
||||||
|
{# })#}
|
||||||
|
{# }#}
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('.select2').select2();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,140 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load common_tags %}
|
||||||
|
{% load users_tags %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block custom_head_css_js %}
|
||||||
|
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||||
|
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper wrapper-content animated fadeInRight">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="panel-options">
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||||
|
</li>
|
||||||
|
<li><a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
|
||||||
|
<i class="fa fa-bar-chart-o"></i> {% trans 'Associate assets' %}</a>
|
||||||
|
</li>
|
||||||
|
<li class="active">
|
||||||
|
<a href="{% url 'assets:system-user-asset-group' pk=system_user.id %}" class="text-center">
|
||||||
|
<i class="fa fa-bar-chart-o"></i> {% trans 'Associate asset groups' %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="col-sm-7" style="padding-left: 0;">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="ibox-title">
|
||||||
|
<span style="float: left">{% trans 'Asset list of ' %} <b>{{ admin_user.name }}</b></span>
|
||||||
|
<div class="ibox-tools">
|
||||||
|
<a class="collapse-link">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-user">
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a class="close-link">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ibox-content">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans 'Name' %}</th>
|
||||||
|
<th>{% trans 'Asset num' %}</th>
|
||||||
|
<th>{% trans 'Unavailable num' %}</th>
|
||||||
|
<th>{% trans 'Comment' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for asset_group in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ asset_group.name }}</td>
|
||||||
|
<td>{{ asset_group_group.assets.count }}</td>
|
||||||
|
<td>{{ asset_group_group.assets.count }}</td>
|
||||||
|
<td>{{ asset_group.comment|truncatewords:4 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="row">
|
||||||
|
{% include '_pagination.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<i class="fa fa-info-circle"></i> {% trans 'Add asset group to this system user' %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<form>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td colspan="2">
|
||||||
|
<select data-placeholder="{% trans 'Select asset group' %}" class="select2" style="width: 100%" multiple="" tabindex="4">
|
||||||
|
{% for asset_group in asset_groups %}
|
||||||
|
<option value="{{ asset_group.id }}">{{ asset_group.name}}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td colspan="2">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm">{% trans 'Add' %}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</form>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_foot_js %}
|
||||||
|
<script>
|
||||||
|
{# function switch_user_status(obj) {#}
|
||||||
|
{# var status = $(obj).prop('checked');#}
|
||||||
|
{##}
|
||||||
|
{# $.ajax({#}
|
||||||
|
{# url: "{% url 'users:user-active-api' pk=user.id %}",#}
|
||||||
|
{# type: "PUT",#}
|
||||||
|
{# data: {#}
|
||||||
|
{# 'is_active': status#}
|
||||||
|
{# },#}
|
||||||
|
{# success: function (data, status) {#}
|
||||||
|
{# console.log(data)#}
|
||||||
|
{# },#}
|
||||||
|
{# error: function () {#}
|
||||||
|
{# console.log('error')#}
|
||||||
|
{# }#}
|
||||||
|
{# })#}
|
||||||
|
{# }#}
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('.select2').select2();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,95 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
{% block custom_head_css_js %}
|
||||||
|
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||||
|
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper wrapper-content animated fadeInRight">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="ibox-title">
|
||||||
|
<h5>{% trans 'Create system user' %}</h5>
|
||||||
|
<div class="ibox-tools">
|
||||||
|
<a class="collapse-link">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</a>
|
||||||
|
<a class="close-link">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ibox-content">
|
||||||
|
<form enctype="multipart/form-data" method="post" class="form-horizontal" action="" >
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.name|bootstrap_horizontal }}
|
||||||
|
{{ form.username|bootstrap_horizontal }}
|
||||||
|
{{ form.protocol|bootstrap_horizontal }}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.auto_generate_key.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto generate key' %}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{{ form.auto_generate_key}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ form.password|bootstrap_horizontal }}
|
||||||
|
{{ form.private_key_file|bootstrap_horizontal }}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.as_default.id_for_label }}" class="col-sm-2 control-label">{% trans 'As default' %}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{{ form.as_default}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.as_push.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto push' %}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{{ form.auto_push}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.as_update.id_for_label }}" class="col-sm-2 control-label">{% trans 'Auto update' %}</label>
|
||||||
|
<div class="col-sm-8">
|
||||||
|
{{ form.auto_update}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ form.assets|bootstrap_horizontal }}
|
||||||
|
{{ form.asset_groups|bootstrap_horizontal }}
|
||||||
|
{{ form.sudo|bootstrap_horizontal }}
|
||||||
|
{{ form.comment|bootstrap_horizontal }}
|
||||||
|
{{ form.home|bootstrap_horizontal }}
|
||||||
|
{{ form.shell|bootstrap_horizontal }}
|
||||||
|
{{ form.uid|bootstrap_horizontal }}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-4 col-sm-offset-2">
|
||||||
|
<button class="btn btn-white" type="reset">{% trans 'Reset' %}</button>
|
||||||
|
<button id="submit_button" class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_foot_js %}
|
||||||
|
<script>
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('.select2').select2();
|
||||||
|
|
||||||
|
if ($('#'+'{{ form.protocol.id_for_label }}').val() == 'telnet') {
|
||||||
|
$('#'+'{{ form.auto_generate_key.id_for_label }}').closest('.form-group').remove();
|
||||||
|
$('#'+'{{ form.private_key_file.id_for_label }}').closest('.form-group').remove();
|
||||||
|
$('#'+'{{ form.auto_push.id_for_label }}').closest('.form-group').remove();
|
||||||
|
$('#'+'{{ form.auto_update.id_for_label }}').closest('.form-group').remove();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,192 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load common_tags %}
|
||||||
|
{% load users_tags %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block custom_head_css_js %}
|
||||||
|
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
|
||||||
|
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper wrapper-content animated fadeInRight">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="panel-options">
|
||||||
|
<ul class="nav nav-tabs">
|
||||||
|
<li class="active">
|
||||||
|
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}" class="text-center"><i class="fa fa-laptop"></i> {% trans 'Detail' %} </a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'assets:system-user-asset' pk=system_user.id %}" class="text-center">
|
||||||
|
<i class="fa fa-bar-chart-o"></i> {% trans 'Associate assets' %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'assets:system-user-asset-group' pk=system_user.id %}" class="text-center">
|
||||||
|
<i class="fa fa-bar-chart-o"></i> {% trans 'Associate asset groups' %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tab-content">
|
||||||
|
<div class="col-sm-7" style="padding-left: 0;">
|
||||||
|
<div class="ibox float-e-margins">
|
||||||
|
<div class="ibox-title">
|
||||||
|
<span class="label"><b>{{ system_user.name }}</b></span>
|
||||||
|
<div class="ibox-tools">
|
||||||
|
<a class="collapse-link">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-user">
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
<li><a href="#"></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<a class="close-link">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ibox-content">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td>{% trans 'Name' %}:</td>
|
||||||
|
<td><b>{{ system_user.name }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Username' %}:</td>
|
||||||
|
<td><b>{{ system_user.username }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Protocol' %}:</td>
|
||||||
|
<td><b>{{ system_user.protocol }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Auto push' %}:</td>
|
||||||
|
<td><b>{{ system_user.protocol }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Auto update' %}:</td>
|
||||||
|
<td><b>{{ system_user.auto_update }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'As default' %}:</td>
|
||||||
|
<td><b>{{ system_user.protocol }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Sudo' %}:</td>
|
||||||
|
<td><b>{{ system_user.sudo }}</b></td>
|
||||||
|
</tr>
|
||||||
|
{% if system_user.shell %}
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Shell' %}:</td>
|
||||||
|
<td><b>{{ system_user.shell }}</b></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if system_user.home %}
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Home' %}:</td>
|
||||||
|
<td><b>{{ system_user.home }}</b></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if system_user.uid %}
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Uid' %}:</td>
|
||||||
|
<td><b>{{ system_user.uid }}</b></td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Date created' %}:</td>
|
||||||
|
<td><b>{{ system_user.date_created }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Created by' %}:</td>
|
||||||
|
<td><b>{{ asset_group.created_by }}</b></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{% trans 'Comment' %}:</td>
|
||||||
|
<td><b>{{ system_user.comment }}</b></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<table class="table">
|
||||||
|
<tbody>
|
||||||
|
<tr class="no-borders-tr">
|
||||||
|
<td width="50%">{% trans 'Get mannual install script' %}:</td>
|
||||||
|
<td>
|
||||||
|
<span style="float: right">
|
||||||
|
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Get' %}</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%">{% trans 'Retest asset connectivity' %}:</td>
|
||||||
|
<td>
|
||||||
|
<span style="float: right">
|
||||||
|
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Start' %}</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td width="50%">{% trans 'Reset private key' %}:</td>
|
||||||
|
<td>
|
||||||
|
<span style="float: right">
|
||||||
|
<button type="button" class="btn btn-primary btn-xs" style="width: 54px">{% trans 'Reset' %}</button>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_foot_js %}
|
||||||
|
<script>
|
||||||
|
{# function switch_user_status(obj) {#}
|
||||||
|
{# var status = $(obj).prop('checked');#}
|
||||||
|
{##}
|
||||||
|
{# $.ajax({#}
|
||||||
|
{# url: "{% url 'users:user-active-api' pk=user.id %}",#}
|
||||||
|
{# type: "PUT",#}
|
||||||
|
{# data: {#}
|
||||||
|
{# 'is_active': status#}
|
||||||
|
{# },#}
|
||||||
|
{# success: function (data, status) {#}
|
||||||
|
{# console.log(data)#}
|
||||||
|
{# },#}
|
||||||
|
{# error: function () {#}
|
||||||
|
{# console.log('error')#}
|
||||||
|
{# }#}
|
||||||
|
{# })#}
|
||||||
|
{# }#}
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('.select2').select2();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,43 @@
|
||||||
|
{% extends '_list_base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load common_tags %}
|
||||||
|
{% block content_left_head %}
|
||||||
|
<a href="{% url 'assets:system-user-create' %}" class="btn btn-sm btn-primary "> {% trans "Create system user" %} </a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block table_head %}
|
||||||
|
<th class="text-center">{% trans 'ID' %}</th>
|
||||||
|
<th class="text-center"><a href="{% url 'assets:system-user-list' %}?sort=name">{% trans 'Name' %}</a></th>
|
||||||
|
<th class="text-center"><a href="{% url 'assets:system-user-list' %}?sort=username">{% trans 'Username' %}</a></th>
|
||||||
|
<th class="text-center">{% trans 'Asset num' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Asset group num' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Unavailable' %}</th>
|
||||||
|
<th class="text-center">{% trans 'Comment' %}</th>
|
||||||
|
<th class="text-center"></th>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block table_body %}
|
||||||
|
{% for system_user in system_user_list %}
|
||||||
|
<tr class="gradeX">
|
||||||
|
<td class="text-center">{{ system_user.id }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'assets:system-user-detail' pk=system_user.id %}">
|
||||||
|
{{ system_user.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">{{ system_user.username }}</td>
|
||||||
|
<td class="text-center">{{ system_user.assets.count }}</td>
|
||||||
|
<td class="text-center">{{ system_user.asset_groups.count }}</td>
|
||||||
|
<td class="text-center">{{ system_user.assets.count }}</td>
|
||||||
|
<td class="text-center">{{ system_user.comment|truncatewords:4 }}</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<!-- Todo: Click script button will paste a url to clipboard like: curl http://url/system_user_create.sh | bash -->
|
||||||
|
<a href="{% url 'assets:system-user-update' pk=system_user.id %}" class="btn btn-xs btn-primary">{% trans 'Script' %}</a>
|
||||||
|
<!-- Todo: Click refresh button will run a task to test admin user could connect asset or not immediately -->
|
||||||
|
<a href="{% url 'assets:system-user-update' pk=system_user.id %}" class="btn btn-xs btn-warning">{% trans 'Refresh' %}</a>
|
||||||
|
<a href="{% url 'assets:system-user-update' pk=system_user.id %}" class="btn btn-xs btn-info">{% trans 'Update' %}</a>
|
||||||
|
<a href="{% url 'assets:system-user-delete' pk=system_user.id %}" class="btn btn-xs btn-danger del">{% trans 'Delete' %}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
|
@ -12,26 +12,43 @@ import views
|
||||||
app_name = 'assets'
|
app_name = 'assets'
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
# Resource asset url
|
||||||
url(r'^$', views.AssetListView.as_view(), name='asset-index'),
|
url(r'^$', views.AssetListView.as_view(), name='asset-index'),
|
||||||
url(r'^asset$', views.AssetListView.as_view(), name='asset-list'),
|
url(r'^asset$', views.AssetListView.as_view(), name='asset-list'),
|
||||||
url(r'^asset/create$', views.AssetCreateView.as_view(), name='asset-create'),
|
url(r'^asset/create$', views.AssetCreateView.as_view(), name='asset-create'),
|
||||||
url(r'^asset/(?P<pk>[0-9]+)$', views.AssetDetailView.as_view(), name='asset-detail'),
|
url(r'^asset/(?P<pk>[0-9]+)$', views.AssetDetailView.as_view(), name='asset-detail'),
|
||||||
url(r'^asset/(?P<pk>[0-9]+)/update', views.AssetUpdateView.as_view(), name='asset-update'),
|
url(r'^asset/(?P<pk>[0-9]+)/update', views.AssetUpdateView.as_view(), name='asset-update'),
|
||||||
url(r'^asset/(?P<pk>[0-9]+)/delete$', views.AssetDeleteView.as_view(), name='asset-delete'),
|
url(r'^asset/(?P<pk>[0-9]+)/delete$', views.AssetDeleteView.as_view(), name='asset-delete'),
|
||||||
|
|
||||||
|
# Resource asset group url
|
||||||
url(r'^asset-group$', views.AssetGroupListView.as_view(), name='asset-group-list'),
|
url(r'^asset-group$', views.AssetGroupListView.as_view(), name='asset-group-list'),
|
||||||
url(r'^asset-group/create$', views.AssetGroupCreateView.as_view(), name='asset-group-create'),
|
url(r'^asset-group/create$', views.AssetGroupCreateView.as_view(), name='asset-group-create'),
|
||||||
url(r'^asset-group/(?P<pk>[0-9]+)$', views.AssetGroupDetailView.as_view(), name='asset-group-detail'),
|
url(r'^asset-group/(?P<pk>[0-9]+)$', views.AssetGroupDetailView.as_view(), name='asset-group-detail'),
|
||||||
url(r'^asset-group/(?P<pk>[0-9]+)/update$', views.AssetGroupUpdateView.as_view(), name='asset-group-update'),
|
url(r'^asset-group/(?P<pk>[0-9]+)/update$', views.AssetGroupUpdateView.as_view(), name='asset-group-update'),
|
||||||
url(r'^asset-group/(?P<pk>[0-9]+)/delete$', views.AssetGroupDeleteView.as_view(), name='asset-group-delete'),
|
url(r'^asset-group/(?P<pk>[0-9]+)/delete$', views.AssetGroupDeleteView.as_view(), name='asset-group-delete'),
|
||||||
|
|
||||||
|
# Resource idc url
|
||||||
url(r'^idc$', views.IDCListView.as_view(), name='idc-list'),
|
url(r'^idc$', views.IDCListView.as_view(), name='idc-list'),
|
||||||
url(r'^idc/create$', views.IDCCreateView.as_view(), name='idc-create'),
|
url(r'^idc/create$', views.IDCCreateView.as_view(), name='idc-create'),
|
||||||
url(r'^idc/(?P<pk>[0-9]+)$', views.IDCDetailView.as_view(), name='idc-detail'),
|
url(r'^idc/(?P<pk>[0-9]+)$', views.IDCDetailView.as_view(), name='idc-detail'),
|
||||||
url(r'^idc/(?P<pk>[0-9]+)/update', views.IDCUpdateView.as_view(), name='idc-update'),
|
url(r'^idc/(?P<pk>[0-9]+)/update', views.IDCUpdateView.as_view(), name='idc-update'),
|
||||||
url(r'^idc/(?P<pk>[0-9]+)/delete$', views.IDCDeleteView.as_view(), name='idc-delete'),
|
url(r'^idc/(?P<pk>[0-9]+)/delete$', views.IDCDeleteView.as_view(), name='idc-delete'),
|
||||||
|
|
||||||
|
# Resource admin user url
|
||||||
url(r'^admin-user$', views.AdminUserListView.as_view(), name='admin-user-list'),
|
url(r'^admin-user$', views.AdminUserListView.as_view(), name='admin-user-list'),
|
||||||
url(r'^admin-user/create$', views.AdminUserCreateView.as_view(), name='admin-user-create'),
|
url(r'^admin-user/create$', views.AdminUserCreateView.as_view(), name='admin-user-create'),
|
||||||
url(r'^admin-user/(?P<pk>[0-9]+)$', views.AdminUserDetailView.as_view(), name='admin-user-detail'),
|
url(r'^admin-user/(?P<pk>[0-9]+)$', views.AdminUserDetailView.as_view(), name='admin-user-detail'),
|
||||||
url(r'^admin-user/(?P<pk>[0-9]+)/update', views.AdminUserUpdateView.as_view(), name='admin-user-update'),
|
url(r'^admin-user/(?P<pk>[0-9]+)/update', views.AdminUserUpdateView.as_view(), name='admin-user-update'),
|
||||||
url(r'^admin-user/(?P<pk>[0-9]+)/delete$', views.AdminUserDeleteView.as_view(), name='admin-user-delete'),
|
url(r'^admin-user/(?P<pk>[0-9]+)/delete$', views.AdminUserDeleteView.as_view(), name='admin-user-delete'),
|
||||||
|
|
||||||
|
# Resource system user url
|
||||||
|
url(r'^system-user$', views.SystemUserListView.as_view(), name='system-user-list'),
|
||||||
|
url(r'^system-user/create$', views.SystemUserCreateView.as_view(), name='system-user-create'),
|
||||||
|
url(r'^system-user/(?P<pk>[0-9]+)$', views.SystemUserDetailView.as_view(), name='system-user-detail'),
|
||||||
|
url(r'^system-user/(?P<pk>[0-9]+)/update', views.SystemUserUpdateView.as_view(), name='system-user-update'),
|
||||||
|
url(r'^system-user/(?P<pk>[0-9]+)/delete$', views.SystemUserDeleteView.as_view(), name='system-user-delete'),
|
||||||
|
url(r'^system-user/(?P<pk>[0-9]+)/asset$', views.SystemUserAssetView.as_view(), name='system-user-asset'),
|
||||||
|
url(r'^system-user/(?P<pk>[0-9]+)/asset-group$', views.SystemUserAssetGroupView.as_view(),
|
||||||
|
name='system-user-asset-group'),
|
||||||
# url(r'^api/v1.0/', include(router.urls)),
|
# url(r'^api/v1.0/', include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,19 +2,16 @@
|
||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from django.views.generic import TemplateView, ListView
|
|
||||||
from django.urls import reverse_lazy
|
|
||||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.views.generic import TemplateView, ListView
|
from django.views.generic import TemplateView, ListView
|
||||||
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||||
|
|
||||||
from .models import Asset, AssetGroup, IDC, AssetExtend, AdminUser, SystemUser
|
from .models import Asset, AssetGroup, IDC, AssetExtend, AdminUser, SystemUser
|
||||||
from .forms import AssetForm, AssetGroupForm, IDCForm
|
from .forms import AssetForm, AssetGroupForm, IDCForm, AdminUserForm, SystemUserForm
|
||||||
from .hands import AdminUserRequiredMixin
|
from .hands import AdminUserRequiredMixin
|
||||||
|
|
||||||
|
|
||||||
|
@ -216,7 +213,7 @@ class AdminUserListView(AdminUserRequiredMixin, ListView):
|
||||||
return super(AdminUserListView, self).get_context_data(**kwargs)
|
return super(AdminUserListView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# Todo: Default group by lose asset connection num
|
# Todo: Default order by lose asset connection num
|
||||||
self.queryset = super(AdminUserListView, self).get_queryset()
|
self.queryset = super(AdminUserListView, self).get_queryset()
|
||||||
self.keyword = keyword = self.request.GET.get('keyword', '')
|
self.keyword = keyword = self.request.GET.get('keyword', '')
|
||||||
self.sort = sort = self.request.GET.get('sort', '-date_created')
|
self.sort = sort = self.request.GET.get('sort', '-date_created')
|
||||||
|
@ -230,17 +227,208 @@ class AdminUserListView(AdminUserRequiredMixin, ListView):
|
||||||
return self.queryset
|
return self.queryset
|
||||||
|
|
||||||
|
|
||||||
class AdminUserCreateView(AdminUserRequiredMixin, CreateView):
|
class AdminUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
|
||||||
pass
|
model = AdminUser
|
||||||
|
form_class = AdminUserForm
|
||||||
|
template_name = 'assets/admin_user_create_update.html'
|
||||||
|
success_url = reverse_lazy('assets:admin-user-list')
|
||||||
|
success_message = _('Create admin user <a href="%s">%s</a> successfully.')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': 'assets',
|
||||||
|
'action': 'Create admin user'
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(AdminUserCreateView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def get_success_message(self, cleaned_data):
|
||||||
|
return self.success_message % (
|
||||||
|
reverse_lazy('assets:admin-user-detail', kwargs={'pk': self.object.pk}),
|
||||||
|
self.object.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdminUserUpdateView(AdminUserRequiredMixin, UpdateView):
|
class AdminUserUpdateView(AdminUserRequiredMixin, UpdateView):
|
||||||
pass
|
model = AdminUser
|
||||||
|
form_class = AdminUserForm
|
||||||
|
template_name = 'assets/admin_user_create_update.html'
|
||||||
|
success_message = _('Update admin user <a href="%s">%s</a> successfully.')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': 'assets',
|
||||||
|
'action': 'Update admin user'
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(AdminUserUpdateView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
success_url = reverse_lazy('assets:admin-user-detail', pk=self.object.pk)
|
||||||
|
return success_url
|
||||||
|
|
||||||
|
|
||||||
class AdminUserDetailView(AdminUserRequiredMixin, DetailView):
|
class AdminUserDetailView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
|
||||||
pass
|
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||||
|
template_name = 'assets/admin_user_detail.html'
|
||||||
|
context_object_name = 'admin_user'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object(queryset=AdminUser.objects.all())
|
||||||
|
return super(AdminUserDetailView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Todo: queryset default order by connectivity, need ops support
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.object.assets.all()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': 'assets',
|
||||||
|
'action': 'Admin user detail'
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(AdminUserDetailView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
class AdminUserDeleteView(AdminUserRequiredMixin, DeleteView):
|
class AdminUserDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||||
pass
|
model = AdminUser
|
||||||
|
template_name = 'assets/delete_confirm.html'
|
||||||
|
success_url = 'assets:admin-user-list'
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserListView(AdminUserRequiredMixin, ListView):
|
||||||
|
model = SystemUser
|
||||||
|
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||||
|
context_object_name = 'system_user_list'
|
||||||
|
template_name = 'assets/system_user_list.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': _('Assets'),
|
||||||
|
'action': _('System user list'),
|
||||||
|
'keyword': self.request.GET.get('keyword', '')
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(SystemUserListView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# Todo: Default order by lose asset connection num
|
||||||
|
self.queryset = super(SystemUserListView, self).get_queryset()
|
||||||
|
self.keyword = keyword = self.request.GET.get('keyword', '')
|
||||||
|
self.sort = sort = self.request.GET.get('sort', '-date_created')
|
||||||
|
|
||||||
|
if keyword:
|
||||||
|
self.queryset = self.queryset.filter(Q(name__icontains=keyword) |
|
||||||
|
Q(comment__icontains=keyword))
|
||||||
|
|
||||||
|
if sort:
|
||||||
|
self.queryset = self.queryset.order_by(sort)
|
||||||
|
return self.queryset
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
|
||||||
|
model = SystemUser
|
||||||
|
form_class = SystemUserForm
|
||||||
|
template_name = 'assets/system_user_create_update.html'
|
||||||
|
success_url = reverse_lazy('assets:system-user-list')
|
||||||
|
success_message = _('Create system user <a href="%s">%s</a> successfully.')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': _('Assets'),
|
||||||
|
'action': _('Create system user'),
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(SystemUserCreateView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def get_success_message(self, cleaned_data):
|
||||||
|
return self.success_message % (
|
||||||
|
reverse_lazy('assets:system-user-detail', kwargs={'pk': self.object.pk}),
|
||||||
|
self.object.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserUpdateView(AdminUserRequiredMixin, UpdateView):
|
||||||
|
model = SystemUser
|
||||||
|
form_class = SystemUserForm
|
||||||
|
template_name = 'assets/system_user_create_update.html'
|
||||||
|
success_message = _('Update system user <a href="%s">%s</a> successfully.')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': _('Assets'),
|
||||||
|
'action': _('Update system user')
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(SystemUserUpdateView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
success_url = reverse_lazy('assets:system-user-detail', pk=self.object.pk)
|
||||||
|
return success_url
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserDetailView(AdminUserRequiredMixin, DetailView):
|
||||||
|
template_name = 'assets/system_user_detail.html'
|
||||||
|
context_object_name = 'system_user'
|
||||||
|
model = SystemUser
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': _('Assets'),
|
||||||
|
'action': _('System user detail')
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(SystemUserDetailView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserDeleteView(AdminUserRequiredMixin, DeleteView):
|
||||||
|
model = SystemUser
|
||||||
|
template_name = 'assets/delete_confirm.html'
|
||||||
|
success_url = 'assets:system-user-list'
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserAssetView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
|
||||||
|
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||||
|
template_name = 'assets/system_user_asset.html'
|
||||||
|
context_object_name = 'system_user'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object(queryset=SystemUser.objects.all())
|
||||||
|
return super(SystemUserAssetView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Todo: queryset default order by connectivity, need ops support
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.object.assets.all()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': 'assets',
|
||||||
|
'action': 'System user asset',
|
||||||
|
'assets': self.get_queryset(),
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(SystemUserAssetView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SystemUserAssetGroupView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
|
||||||
|
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
|
||||||
|
template_name = 'assets/system_user_asset_group.html'
|
||||||
|
context_object_name = 'system_user'
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
self.object = self.get_object(queryset=SystemUser.objects.all())
|
||||||
|
return super(SystemUserAssetGroupView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Todo: queryset default order by connectivity, need ops support
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.object.asset_groups.all()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {
|
||||||
|
'app': 'assets',
|
||||||
|
'action': 'System user asset group',
|
||||||
|
'asset_groups': self.get_queryset(),
|
||||||
|
}
|
||||||
|
kwargs.update(context)
|
||||||
|
return super(SystemUserAssetGroupView, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -108,6 +108,7 @@ TEMPLATES = [
|
||||||
# WSGI_APPLICATION = 'jumpserver.wsgi.application'
|
# WSGI_APPLICATION = 'jumpserver.wsgi.application'
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = reverse_lazy('index')
|
LOGIN_REDIRECT_URL = reverse_lazy('index')
|
||||||
|
LOGIN_URL = reverse_lazy('users:login')
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
|
||||||
|
@ -227,7 +228,7 @@ USE_L10N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
# I18N translation
|
# I18N translation
|
||||||
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'),]
|
LOCALE_PATHS = [os.path.join(BASE_DIR, 'locale'), ]
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<li id="asset-group"><a href="{% url 'assets:asset-group-list' %}">{% trans 'Asset group' %}</a></li>
|
<li id="asset-group"><a href="{% url 'assets:asset-group-list' %}">{% trans 'Asset group' %}</a></li>
|
||||||
<li id="idc"><a href="{% url 'assets:idc-list' %}">{% trans 'IDC' %}</a></li>
|
<li id="idc"><a href="{% url 'assets:idc-list' %}">{% trans 'IDC' %}</a></li>
|
||||||
<li id="admin-user"><a href="{% url 'assets:admin-user-list' %}">{% trans 'Admin user' %}</a></li>
|
<li id="admin-user"><a href="{% url 'assets:admin-user-list' %}">{% trans 'Admin user' %}</a></li>
|
||||||
<li id="system-user"><a href="">{% trans 'System user' %}</a></li>
|
<li id="system-user"><a href="{% url 'assets:system-user-list' %}">{% trans 'System user' %}</a></li>
|
||||||
<li id=""><a href="">{% trans 'Label' %}</a></li>
|
<li id=""><a href="">{% trans 'Label' %}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -18,6 +18,7 @@ class UserLoginForm(AuthenticationForm):
|
||||||
|
|
||||||
|
|
||||||
class UserCreateForm(forms.ModelForm):
|
class UserCreateForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = User
|
model = User
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -67,3 +68,23 @@ class UserGroupForm(forms.ModelForm):
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'name': '* required'
|
'name': '* required'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserInfoForm(forms.Form):
|
||||||
|
name = forms.CharField(max_length=20, label=_('name'))
|
||||||
|
avatar = forms.ImageField(label=_('avatar'), required=False)
|
||||||
|
wechat = forms.CharField(max_length=30, label=_('wechat'), required=False)
|
||||||
|
phone = forms.CharField(max_length=20, label=_('phone'), required=False)
|
||||||
|
enable_otp = forms.BooleanField(required=False, label=_('enable otp'))
|
||||||
|
|
||||||
|
|
||||||
|
class UserKeyForm(forms.Form):
|
||||||
|
private_key = forms.CharField(max_length=5000, widget=forms.Textarea, label=_('private key'))
|
||||||
|
|
||||||
|
def clean_private_key(self):
|
||||||
|
from users.utils import validate_ssh_pk
|
||||||
|
ssh_pk = self.cleaned_data['private_key']
|
||||||
|
checked, reason = validate_ssh_pk(ssh_pk)
|
||||||
|
if not checked:
|
||||||
|
raise forms.ValidationError(_('Not a valid ssh private key.'))
|
||||||
|
return ssh_pk
|
||||||
|
|
|
@ -2,19 +2,17 @@
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password
|
from django.contrib.auth.hashers import make_password
|
||||||
from django.utils import timezone
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.db import models
|
from django.core import signing
|
||||||
from django.contrib.auth.models import AbstractUser, Permission
|
from django.db import models, IntegrityError
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.db import IntegrityError
|
from django.utils import timezone
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
from django.core import signing
|
|
||||||
|
|
||||||
from common.utils import encrypt, decrypt
|
from common.utils import encrypt, decrypt
|
||||||
|
|
||||||
|
@ -44,16 +42,15 @@ class UserGroup(models.Model):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_fake(cls, count=100):
|
def generate_fake(cls, count=100):
|
||||||
from random import seed, randint, choice
|
from random import seed, choice
|
||||||
import forgery_py
|
import forgery_py
|
||||||
from django.db import IntegrityError
|
|
||||||
|
|
||||||
seed()
|
seed()
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
group = cls(name=forgery_py.name.full_name(),
|
group = cls(name=forgery_py.name.full_name(),
|
||||||
comment=forgery_py.lorem_ipsum.sentence(),
|
comment=forgery_py.lorem_ipsum.sentence(),
|
||||||
created_by=choice(User.objects.all()).username
|
created_by=choice(User.objects.all()).username
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
group.save()
|
group.save()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
|
@ -84,7 +81,7 @@ class User(AbstractUser):
|
||||||
_private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('ssh private key'))
|
_private_key = models.CharField(max_length=5000, blank=True, verbose_name=_('ssh private key'))
|
||||||
_public_key = models.CharField(max_length=1000, blank=True, verbose_name=_('ssh public key'))
|
_public_key = models.CharField(max_length=1000, blank=True, verbose_name=_('ssh public key'))
|
||||||
comment = models.TextField(max_length=200, blank=True, verbose_name=_('Comment'))
|
comment = models.TextField(max_length=200, blank=True, verbose_name=_('Comment'))
|
||||||
is_first_login = models.BooleanField(default=False)
|
is_first_login = models.BooleanField(default=True)
|
||||||
date_expired = models.DateTimeField(default=date_expired_default, blank=True, null=True,
|
date_expired = models.DateTimeField(default=date_expired_default, blank=True, null=True,
|
||||||
verbose_name=_('Date expired'))
|
verbose_name=_('Date expired'))
|
||||||
created_by = models.CharField(max_length=30, default='', verbose_name=_('Created by'))
|
created_by = models.CharField(max_length=30, default='', verbose_name=_('Created by'))
|
||||||
|
@ -235,7 +232,7 @@ class User(AbstractUser):
|
||||||
wechat=forgery_py.internet.user_name(True),
|
wechat=forgery_py.internet.user_name(True),
|
||||||
comment=forgery_py.lorem_ipsum.sentence(),
|
comment=forgery_py.lorem_ipsum.sentence(),
|
||||||
created_by=choice(cls.objects.all()).username,
|
created_by=choice(cls.objects.all()).username,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
user.save()
|
user.save()
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
|
@ -264,4 +261,3 @@ def create_auth_token(sender, instance=None, created=False, **kwargs):
|
||||||
Token.objects.create(user=instance)
|
Token.objects.create(user=instance)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load bootstrap %}
|
||||||
|
|
||||||
|
{% block custom_head_css_js %}
|
||||||
|
{{ wizard.form.media }}
|
||||||
|
<link href="{% static 'css/plugins/steps/jquery.steps.css' %}" rel="stylesheet">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="wrapper wrapper-content animated fadeInRight">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="ibox">
|
||||||
|
<div class="ibox-title">
|
||||||
|
<h5>{% trans 'First Login' %}</h5>
|
||||||
|
<div class="ibox-tools">
|
||||||
|
<a class="collapse-link">
|
||||||
|
<i class="fa fa-chevron-up"></i>
|
||||||
|
</a>
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||||
|
<i class="fa fa-wrench"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ibox-content">
|
||||||
|
<div class="wizard">
|
||||||
|
<div class="steps clearfix">
|
||||||
|
<ul role="tablist">
|
||||||
|
{% for step in wizard.steps.all %}
|
||||||
|
<li role="tab" class="{% ifequal step wizard.steps.first %}first{% endifequal %} {% ifequal step wizard.steps.current %}current{% else %}disabled{% endifequal %} {% ifequal step wizard.steps.last %}last{% endifequal %}"
|
||||||
|
aria-disabled="false" aria-selected="true">
|
||||||
|
<a href="javascript:void(0)"><span class="number">{% trans 'Step' %} {{ step }}</span></a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="content clearfix">
|
||||||
|
<form action="" method="post" class="form col-lg-8 p-m" id="fl_form">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ wizard.management_form }}
|
||||||
|
{% if wizard.form.forms %}
|
||||||
|
{{ wizard.form.management_form }}
|
||||||
|
{% for form in wizard.form.forms %}
|
||||||
|
{{ form|bootstrap }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{{ wizard.form|bootstrap }}
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="actions clearfix">
|
||||||
|
<ul>
|
||||||
|
{% if wizard.steps.prev %}
|
||||||
|
<li><a class="fl_goto" data-goto="{{ wizard.steps.first }}">{% trans "first step" %}</a></li>
|
||||||
|
<li><a class="fl_goto" name="wizard_goto_step" data-goto="{{ wizard.steps.prev }}">{% trans "prev step" %}</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><a id="fl_submit">{% trans "submit" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
{% block custom_foot_js %}
|
||||||
|
<script>
|
||||||
|
$(document).on('click', ".fl_goto", function(){
|
||||||
|
var $form = $('#fl_form');
|
||||||
|
$('<input />', {'name': 'wizard_goto_step', 'value': $(this).data('goto'), 'type': 'hidden'}).appendTo($form);
|
||||||
|
$form.submit();
|
||||||
|
return false;
|
||||||
|
}).on('click', '#fl_submit', function(){
|
||||||
|
$('#fl_form').submit();
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -40,7 +40,7 @@
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="ibox-content">
|
<div class="ibox-content">
|
||||||
<div><img src="{% static 'img/logo.png' %}" width="82" height="82"> <span class="font-bold text-center" style="font-size: 32px; font-family: inherit">{% trans 'Login' %}</span></div>
|
<div><img src="{% static 'img/logo.png' %}" width="82" height="82"> <span class="font-bold text-center" style="font-size: 32px; font-family: inherit">{% trans 'Login' %}</span></div>
|
||||||
<form class="m-t" role="form" method="post" action="{% url 'users:login' %}">
|
<form class="m-t" role="form" method="post" action="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
{% if 'captcha' in form.errors %}
|
{% if 'captcha' in form.errors %}
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
<div class="col-sm-5" style="padding-left: 0;padding-right: 0">
|
||||||
<div class="panel panel-primary">
|
<div class="panel panel-primary">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<i class="fa fa-info-circle"></i> {% trans 'Quick modify' %}
|
<i class="fa fa-info-circle"></i> {% trans 'Quick update' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
|
@ -191,7 +191,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td ><b class="bdg_user_group" data-gid={{ group.id }}>{{ group.name }}</b></td>
|
<td ><b class="bdg_user_group" data-gid={{ group.id }}>{{ group.name }}</b></td>
|
||||||
<td>
|
<td>
|
||||||
<button class="btn btn-danger btn-sm btn_delete_user_group" type="button" style="float: right;"><i class="fa fa-minus"></i></button>
|
<button class="btn btn-danger btn-xs btn_delete_user_group" type="button" style="float: right;"><i class="fa fa-minus"></i></button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -16,6 +16,7 @@ urlpatterns = [
|
||||||
name='reset-password-success'),
|
name='reset-password-success'),
|
||||||
url(r'^user$', views.UserListView.as_view(), name='user-list'),
|
url(r'^user$', views.UserListView.as_view(), name='user-list'),
|
||||||
url(r'^user/(?P<pk>[0-9]+)$', views.UserDetailView.as_view(), name='user-detail'),
|
url(r'^user/(?P<pk>[0-9]+)$', views.UserDetailView.as_view(), name='user-detail'),
|
||||||
|
url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'),
|
||||||
url(r'^user/(?P<pk>[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'),
|
url(r'^user/(?P<pk>[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'),
|
||||||
url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'),
|
url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'),
|
||||||
url(r'^user/(?P<pk>[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'),
|
url(r'^user/(?P<pk>[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'),
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
# ~*~ coding: utf-8 ~*~
|
# ~*~ coding: utf-8 ~*~
|
||||||
#
|
#
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import os
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from paramiko.rsakey import RSAKey
|
|
||||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
from paramiko.rsakey import RSAKey
|
||||||
|
|
||||||
from common.tasks import send_mail_async
|
from common.tasks import send_mail_async
|
||||||
from common.utils import reverse
|
from common.utils import reverse
|
||||||
from users.models import User
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -125,5 +125,56 @@ def send_reset_password_mail(user):
|
||||||
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
send_mail_async.delay(subject, message, recipient_list, html_message=message)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_ssh_pk(text):
|
||||||
|
"""
|
||||||
|
Expects a SSH private key as string.
|
||||||
|
Returns a boolean and a error message.
|
||||||
|
If the text is parsed as private key successfully,
|
||||||
|
(True,'') is returned. Otherwise,
|
||||||
|
(False, <message describing the error>) is returned.
|
||||||
|
|
||||||
|
from https://github.com/githubnemo/SSH-private-key-validator/blob/master/validate.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return False, 'No text given'
|
||||||
|
|
||||||
|
startPattern = re.compile("^-----BEGIN [A-Z]+ PRIVATE KEY-----")
|
||||||
|
optionPattern = re.compile("^.+: .+")
|
||||||
|
contentPattern = re.compile("^([a-zA-Z0-9+/]{64}|[a-zA-Z0-9+/]{1,64}[=]{0,2})$")
|
||||||
|
endPattern = re.compile("^-----END [A-Z]+ PRIVATE KEY-----")
|
||||||
|
|
||||||
|
def contentState(text):
|
||||||
|
for i in range(0, len(text)):
|
||||||
|
line = text[i]
|
||||||
|
|
||||||
|
if endPattern.match(line):
|
||||||
|
if i == len(text) - 1 or len(text[i + 1]) == 0:
|
||||||
|
return True, ''
|
||||||
|
else:
|
||||||
|
return False, 'At end but content coming'
|
||||||
|
|
||||||
|
elif not contentPattern.match(line):
|
||||||
|
return False, 'Wrong string in content section'
|
||||||
|
|
||||||
|
return False, 'No content or missing end line'
|
||||||
|
|
||||||
|
def optionState(text):
|
||||||
|
for i in range(0, len(text)):
|
||||||
|
line = text[i]
|
||||||
|
|
||||||
|
if line[-1:] == '\\':
|
||||||
|
return optionState(text[i + 2:])
|
||||||
|
|
||||||
|
if not optionPattern.match(line):
|
||||||
|
return contentState(text[i + 1:])
|
||||||
|
|
||||||
|
return False, 'Expected option, found nothing'
|
||||||
|
|
||||||
|
def startState(text):
|
||||||
|
if len(text) == 0 or not startPattern.match(text[0]):
|
||||||
|
return False, 'Header is wrong'
|
||||||
|
return optionState(text[1:])
|
||||||
|
|
||||||
|
return startState([n.strip() for n in text.splitlines()])
|
||||||
|
|
|
@ -6,7 +6,9 @@ import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import login as auth_login, logout as auth_logout
|
from django.contrib.auth import login as auth_login, logout as auth_logout
|
||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, reverse, redirect
|
from django.shortcuts import get_object_or_404, reverse, redirect
|
||||||
|
@ -21,10 +23,12 @@ from django.views.generic.list import ListView
|
||||||
from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView
|
from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView
|
||||||
from django.views.generic.detail import DetailView
|
from django.views.generic.detail import DetailView
|
||||||
|
|
||||||
|
from formtools.wizard.views import SessionWizardView
|
||||||
|
|
||||||
from common.utils import get_object_or_none
|
from common.utils import get_object_or_none
|
||||||
|
|
||||||
from .models import User, UserGroup
|
from .models import User, UserGroup
|
||||||
from .forms import UserCreateForm, UserUpdateForm, UserGroupForm, UserLoginForm
|
from .forms import (UserCreateForm, UserUpdateForm, UserGroupForm, UserLoginForm, UserInfoForm, UserKeyForm)
|
||||||
from .utils import AdminUserRequiredMixin, user_add_success_next, send_reset_password_mail
|
from .utils import AdminUserRequiredMixin, user_add_success_next, send_reset_password_mail
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,12 +45,20 @@ class UserLoginView(FormView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
return redirect(request.POST.get(self.redirect_field_name, reverse('index')))
|
return redirect(self.get_success_url())
|
||||||
return self.render_to_response(self.get_context_data(**kwargs))
|
return super(UserLoginView, self).get(request, *args, **kwargs)
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
auth_login(self.request, form.get_user())
|
auth_login(self.request, form.get_user())
|
||||||
return redirect(self.request.POST.get(self.redirect_field_name, reverse('index')))
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
if self.request.user.is_first_login:
|
||||||
|
return reverse('users:user-first-login')
|
||||||
|
|
||||||
|
return self.request.POST.get(
|
||||||
|
self.redirect_field_name,
|
||||||
|
self.request.GET.get(self.redirect_field_name, reverse('index')))
|
||||||
|
|
||||||
|
|
||||||
@method_decorator(never_cache, name='dispatch')
|
@method_decorator(never_cache, name='dispatch')
|
||||||
|
@ -97,7 +109,7 @@ class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView):
|
||||||
form_class = UserCreateForm
|
form_class = UserCreateForm
|
||||||
template_name = 'users/user_create.html'
|
template_name = 'users/user_create.html'
|
||||||
success_url = reverse_lazy('users:user-list')
|
success_url = reverse_lazy('users:user-list')
|
||||||
success_message = _('Create user <a href="%s">%s</a> success.')
|
success_message = _('Create user <a href="%s">%s</a> successfully.')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(UserCreateView, self).get_context_data(**kwargs)
|
context = super(UserCreateView, self).get_context_data(**kwargs)
|
||||||
|
@ -286,3 +298,42 @@ class UserResetPasswordView(TemplateView):
|
||||||
|
|
||||||
user.reset_password(password)
|
user.reset_password(password)
|
||||||
return HttpResponseRedirect(reverse('users:reset-password-success'))
|
return HttpResponseRedirect(reverse('users:reset-password-success'))
|
||||||
|
|
||||||
|
|
||||||
|
class UserFirstLoginView(LoginRequiredMixin, SessionWizardView):
|
||||||
|
template_name = 'users/first_login.html'
|
||||||
|
form_list = [UserInfoForm, UserKeyForm]
|
||||||
|
file_storage = default_storage
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if request.user.is_authenticated() and not request.user.is_first_login:
|
||||||
|
return redirect(reverse('index'))
|
||||||
|
return super(UserFirstLoginView, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def done(self, form_list, form_dict, **kwargs):
|
||||||
|
user = self.request.user
|
||||||
|
for form in form_list:
|
||||||
|
for field in form:
|
||||||
|
if field.value():
|
||||||
|
setattr(user, field.name, field.value())
|
||||||
|
if field.name == 'enable_otp':
|
||||||
|
user.enable_otp = field.value()
|
||||||
|
user.is_first_login = False
|
||||||
|
user.save()
|
||||||
|
return redirect(reverse('index'))
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(UserFirstLoginView, self).get_context_data(**kwargs)
|
||||||
|
context.update({'app': _('Users'), 'action': _('First Login')})
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_form_initial(self, step):
|
||||||
|
user = self.request.user
|
||||||
|
if step == '0':
|
||||||
|
return {
|
||||||
|
'name': user.name or user.username,
|
||||||
|
'enable_otp': user.enable_otp or True,
|
||||||
|
'wechat': user.wechat or '',
|
||||||
|
'phone': user.phone or ''
|
||||||
|
}
|
||||||
|
return super(UserFirstLoginView, self).get_form_initial(step)
|
||||||
|
|
|
@ -17,3 +17,4 @@ paramiko==2.0.2
|
||||||
celery==3.1.23
|
celery==3.1.23
|
||||||
ansible==2.1.1.0
|
ansible==2.1.1.0
|
||||||
django-simple-captcha==0.5.2
|
django-simple-captcha==0.5.2
|
||||||
|
django-formtools==1.0
|
||||||
|
|
Loading…
Reference in New Issue