Commit 4c94d47f by Kálmán Viktor

Merge branch 'master' into feature-voms-occi

Conflicts:
	requirements/base.txt
parents 9875c841 bd2667df
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Level'
db.create_table(u'acl_level', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=50)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
('codename', self.gf('django.db.models.fields.CharField')(max_length=100)),
('weight', self.gf('django.db.models.fields.IntegerField')(null=True)),
))
db.send_create_signal(u'acl', ['Level'])
# Adding unique constraint on 'Level', fields ['content_type', 'codename']
db.create_unique(u'acl_level', ['content_type_id', 'codename'])
# Adding model 'ObjectLevel'
db.create_table(u'acl_objectlevel', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('level', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['acl.Level'])),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
('object_id', self.gf('django.db.models.fields.CharField')(max_length=255)),
))
db.send_create_signal(u'acl', ['ObjectLevel'])
# Adding unique constraint on 'ObjectLevel', fields ['content_type', 'object_id', 'level']
db.create_unique(u'acl_objectlevel', ['content_type_id', 'object_id', 'level_id'])
# Adding M2M table for field users on 'ObjectLevel'
m2m_table_name = db.shorten_name(u'acl_objectlevel_users')
db.create_table(m2m_table_name, (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('objectlevel', models.ForeignKey(orm[u'acl.objectlevel'], null=False)),
('user', models.ForeignKey(orm[u'auth.user'], null=False))
))
db.create_unique(m2m_table_name, ['objectlevel_id', 'user_id'])
# Adding M2M table for field groups on 'ObjectLevel'
m2m_table_name = db.shorten_name(u'acl_objectlevel_groups')
db.create_table(m2m_table_name, (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('objectlevel', models.ForeignKey(orm[u'acl.objectlevel'], null=False)),
('group', models.ForeignKey(orm[u'auth.group'], null=False))
))
db.create_unique(m2m_table_name, ['objectlevel_id', 'group_id'])
def backwards(self, orm):
# Removing unique constraint on 'ObjectLevel', fields ['content_type', 'object_id', 'level']
db.delete_unique(u'acl_objectlevel', ['content_type_id', 'object_id', 'level_id'])
# Removing unique constraint on 'Level', fields ['content_type', 'codename']
db.delete_unique(u'acl_level', ['content_type_id', 'codename'])
# Deleting model 'Level'
db.delete_table(u'acl_level')
# Deleting model 'ObjectLevel'
db.delete_table(u'acl_objectlevel')
# Removing M2M table for field users on 'ObjectLevel'
db.delete_table(db.shorten_name(u'acl_objectlevel_users'))
# Removing M2M table for field groups on 'ObjectLevel'
db.delete_table(db.shorten_name(u'acl_objectlevel_groups'))
models = {
u'acl.level': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Level'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
'weight': ('django.db.models.fields.IntegerField', [], {'null': 'True'})
},
u'acl.objectlevel': {
'Meta': {'unique_together': "(('content_type', 'object_id', 'level'),)", 'object_name': 'ObjectLevel'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'level': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['acl.Level']"}),
'object_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
},
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['acl']
\ No newline at end of file
...@@ -15,29 +15,4 @@ ...@@ -15,29 +15,4 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.db.models import TextField, ForeignKey from .test_acl import TestModel, Test2Model # noqa
from django.contrib.auth.models import User
from ..models import AclBase
class TestModel(AclBase):
normal_field = TextField()
ACL_LEVELS = (
('alfa', 'Alfa'),
('bravo', 'Bravo'),
('charlie', 'Charlie'),
)
class Test2Model(AclBase):
normal2_field = TextField()
owner = ForeignKey(User, null=True)
ACL_LEVELS = (
('one', 'One'),
('two', 'Two'),
('three', 'Three'),
('owner', 'owner'),
)
...@@ -17,9 +17,31 @@ ...@@ -17,9 +17,31 @@
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User, Group, AnonymousUser from django.contrib.auth.models import User, Group, AnonymousUser
from django.db.models import TextField, ForeignKey
from ..models import ObjectLevel from ..models import ObjectLevel, AclBase
from .models import TestModel, Test2Model
class TestModel(AclBase):
normal_field = TextField()
ACL_LEVELS = (
('alfa', 'Alfa'),
('bravo', 'Bravo'),
('charlie', 'Charlie'),
)
class Test2Model(AclBase):
normal2_field = TextField()
owner = ForeignKey(User, null=True)
ACL_LEVELS = (
('one', 'One'),
('two', 'Two'),
('three', 'Three'),
('owner', 'owner'),
)
class AclUserTest(TestCase): class AclUserTest(TestCase):
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
"jquery-knob": "~1.2.9", "jquery-knob": "~1.2.9",
"jquery-simple-slider": "https://github.com/BME-IK/jquery-simple-slider.git", "jquery-simple-slider": "https://github.com/BME-IK/jquery-simple-slider.git",
"bootbox": "~4.3.0", "bootbox": "~4.3.0",
"intro.js": "0.9.0" "intro.js": "0.9.0",
"favico.js": "~0.3.5"
} }
} }
...@@ -50,20 +50,20 @@ def get_env_variable(var_name, default=None): ...@@ -50,20 +50,20 @@ def get_env_variable(var_name, default=None):
########## PATH CONFIGURATION ########## PATH CONFIGURATION
# Absolute filesystem path to the Django project directory: # Absolute filesystem path to the Django project directory:
DJANGO_ROOT = dirname(dirname(abspath(__file__))) BASE_DIR = dirname(dirname(abspath(__file__)))
# Absolute filesystem path to the top-level project folder: # Absolute filesystem path to the top-level project folder:
SITE_ROOT = dirname(DJANGO_ROOT) SITE_ROOT = dirname(BASE_DIR)
# Site name: # Site name:
SITE_NAME = basename(DJANGO_ROOT) SITE_NAME = basename(BASE_DIR)
# Url to site: (e.g. http://localhost:8080/) # Url to site: (e.g. http://localhost:8080/)
DJANGO_URL = get_env_variable('DJANGO_URL', '/') DJANGO_URL = get_env_variable('DJANGO_URL', '/')
# Add our project to our pythonpath, this way we don't need to type our project # Add our project to our pythonpath, this way we don't need to type our project
# name in our dotted import paths: # name in our dotted import paths:
path.append(DJANGO_ROOT) path.append(BASE_DIR)
########## END PATH CONFIGURATION ########## END PATH CONFIGURATION
...@@ -78,14 +78,9 @@ TEMPLATE_DEBUG = DEBUG ...@@ -78,14 +78,9 @@ TEMPLATE_DEBUG = DEBUG
########## MANAGER CONFIGURATION ########## MANAGER CONFIGURATION
# See: https://docs.djangoproject.com/en/dev/ref/settings/#admins # See: https://docs.djangoproject.com/en/dev/ref/settings/#admins
ADMINS = (
('Root', 'root@localhost'),
)
EMAIL_SUBJECT_PREFIX = get_env_variable('DJANGO_SUBJECT_PREFIX', '[CIRCLE] ') EMAIL_SUBJECT_PREFIX = get_env_variable('DJANGO_SUBJECT_PREFIX', '[CIRCLE] ')
# See: https://docs.djangoproject.com/en/dev/ref/settings/#managers
MANAGERS = ADMINS
########## END MANAGER CONFIGURATION ########## END MANAGER CONFIGURATION
...@@ -197,7 +192,9 @@ PIPELINE_JS = { ...@@ -197,7 +192,9 @@ PIPELINE_JS = {
"intro.js/intro.js", "intro.js/intro.js",
"jquery-knob/dist/jquery.knob.min.js", "jquery-knob/dist/jquery.knob.min.js",
"jquery-simple-slider/js/simple-slider.js", "jquery-simple-slider/js/simple-slider.js",
"favico.js/favico.js",
"dashboard/dashboard.js", "dashboard/dashboard.js",
"dashboard/activity.js",
"dashboard/group-details.js", "dashboard/group-details.js",
"dashboard/group-list.js", "dashboard/group-list.js",
"dashboard/js/stupidtable.min.js", # no bower file "dashboard/js/stupidtable.min.js", # no bower file
...@@ -281,12 +278,6 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -281,12 +278,6 @@ TEMPLATE_CONTEXT_PROCESSORS = (
'dashboard.context_processors.extract_settings', 'dashboard.context_processors.extract_settings',
) )
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs # See: https://docs.djangoproject.com/en/dev/ref/settings/#template-dirs
TEMPLATE_DIRS = ( TEMPLATE_DIRS = (
normpath(join(SITE_ROOT, '../../site-circle/templates')), normpath(join(SITE_ROOT, '../../site-circle/templates')),
...@@ -336,7 +327,6 @@ DJANGO_APPS = ( ...@@ -336,7 +327,6 @@ DJANGO_APPS = (
) )
THIRD_PARTY_APPS = ( THIRD_PARTY_APPS = (
'south',
'django_tables2', 'django_tables2',
'crispy_forms', 'crispy_forms',
'djcelery', 'djcelery',
...@@ -348,6 +338,11 @@ THIRD_PARTY_APPS = ( ...@@ -348,6 +338,11 @@ THIRD_PARTY_APPS = (
'pipeline', 'pipeline',
) )
import django
if django.get_version() < '1.7':
THIRD_PARTY_APPS += 'south',
# Apps specific for this project go here. # Apps specific for this project go here.
LOCAL_APPS = ( LOCAL_APPS = (
'common', 'common',
...@@ -541,8 +536,14 @@ LOCALE_PATHS = (join(SITE_ROOT, 'locale'), ) ...@@ -541,8 +536,14 @@ LOCALE_PATHS = (join(SITE_ROOT, 'locale'), )
COMPANY_NAME = "BME IK 2014" COMPANY_NAME = "BME IK 2014"
SOUTH_MIGRATION_MODULES = { SOUTH_MIGRATION_MODULES = {
'taggit': 'taggit.south_migrations', 'taggit': 'taggit.south_migrations',
'vm': 'vm.south_migrations',
'firewall': 'firewall.south_migrations',
'acl': 'acl.south_migrations',
'dashboard': 'dashboard.south_migrations',
'storage': 'storage.south_migrations',
} }
graphite_host = environ.get("GRAPHITE_HOST", None) graphite_host = environ.get("GRAPHITE_HOST", None)
graphite_port = environ.get("GRAPHITE_PORT", None) graphite_port = environ.get("GRAPHITE_PORT", None)
if graphite_host and graphite_port: if graphite_host and graphite_port:
......
...@@ -43,7 +43,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' ...@@ -43,7 +43,7 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# DATABASES = { # DATABASES = {
# 'default': { # 'default': {
# 'ENGINE': 'django.db.backends.sqlite3', # 'ENGINE': 'django.db.backends.sqlite3',
# 'NAME': normpath(join(DJANGO_ROOT, 'default.db')), # 'NAME': normpath(join(BASE_DIR, 'default.db')),
# 'USER': '', # 'USER': '',
# 'PASSWORD': '', # 'PASSWORD': '',
# 'HOST': '', # 'HOST': '',
......
...@@ -370,6 +370,12 @@ class HumanSortField(CharField): ...@@ -370,6 +370,12 @@ class HumanSortField(CharField):
def get_monitored_value(self, instance): def get_monitored_value(self, instance):
return getattr(instance, self.monitor) return getattr(instance, self.monitor)
def deconstruct(self):
name, path, args, kwargs = super(HumanSortField, self).deconstruct()
if self.monitor is not None:
kwargs['monitor'] = self.monitor
return name, path, args, kwargs
@staticmethod @staticmethod
def _partition(s, pred): def _partition(s, pred):
"""Partition a deque of chars to a tuple of a """Partition a deque of chars to a tuple of a
......
...@@ -282,6 +282,8 @@ def register_operation(op_cls, op_id=None, target_cls=None): ...@@ -282,6 +282,8 @@ def register_operation(op_cls, op_id=None, target_cls=None):
"in the 'target_cls' parameter to this " "in the 'target_cls' parameter to this "
"call.") "call.")
assert not hasattr(target_cls, op_id), (
"target class already has an attribute with this id")
if not issubclass(target_cls, OperatedMixin): if not issubclass(target_cls, OperatedMixin):
raise TypeError("%r is not a subclass of %r" % raise TypeError("%r is not a subclass of %r" %
(target_cls.__name__, OperatedMixin.__name__)) (target_cls.__name__, OperatedMixin.__name__))
......
...@@ -41,6 +41,7 @@ from django.forms.widgets import TextInput, HiddenInput ...@@ -41,6 +41,7 @@ from django.forms.widgets import TextInput, HiddenInput
from django.template import Context from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import escape, format_html from django.utils.html import escape, format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
...@@ -95,10 +96,10 @@ class VmSaveForm(OperationForm): ...@@ -95,10 +96,10 @@ class VmSaveForm(OperationForm):
if default: if default:
self.fields['name'].initial = default self.fields['name'].initial = default
if clone: if clone:
self.fields.insert(2, "clone", forms.BooleanField( self.fields["clone"] = forms.BooleanField(
required=False, label=_("Clone template permissions"), required=False, label=_("Clone template permissions"),
help_text=_("Clone the access list of parent template. Useful " help_text=_("Clone the access list of parent template. Useful "
"for updating a template."))) "for updating a template."))
class VmCustomizeForm(forms.Form): class VmCustomizeForm(forms.Form):
...@@ -749,9 +750,9 @@ class VmRenewForm(OperationForm): ...@@ -749,9 +750,9 @@ class VmRenewForm(OperationForm):
default = kwargs.pop('default') default = kwargs.pop('default')
super(VmRenewForm, self).__init__(*args, **kwargs) super(VmRenewForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'lease', forms.ModelChoiceField( self.fields['lease'] = forms.ModelChoiceField(
queryset=choices, initial=default, required=False, queryset=choices, initial=default, required=False,
empty_label=None, label=_('Length'))) empty_label=None, label=_('Length'))
if len(choices) < 2: if len(choices) < 2:
self.fields['lease'].widget = HiddenInput() self.fields['lease'].widget = HiddenInput()
self.fields['save'].widget = HiddenInput() self.fields['save'].widget = HiddenInput()
...@@ -771,9 +772,9 @@ class VmMigrateForm(forms.Form): ...@@ -771,9 +772,9 @@ class VmMigrateForm(forms.Form):
default = kwargs.pop('default') default = kwargs.pop('default')
super(VmMigrateForm, self).__init__(*args, **kwargs) super(VmMigrateForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'to_node', forms.ModelChoiceField( self.fields['to_node'] = forms.ModelChoiceField(
queryset=choices, initial=default, required=False, queryset=choices, initial=default, required=False,
widget=forms.RadioSelect(), label=_("Node"))) widget=forms.RadioSelect(), label=_("Node"))
class VmStateChangeForm(OperationForm): class VmStateChangeForm(OperationForm):
...@@ -834,9 +835,9 @@ class VmDiskResizeForm(OperationForm): ...@@ -834,9 +835,9 @@ class VmDiskResizeForm(OperationForm):
super(VmDiskResizeForm, self).__init__(*args, **kwargs) super(VmDiskResizeForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'disk', forms.ModelChoiceField( self.fields['disk'] = forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True, queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk'))) empty_label=None, label=_('Disk'))
if self.disk: if self.disk:
self.fields['disk'].widget = HiddenInput() self.fields['disk'].widget = HiddenInput()
self.fields['size'].initial += self.disk.size self.fields['size'].initial += self.disk.size
...@@ -870,9 +871,9 @@ class VmDiskRemoveForm(OperationForm): ...@@ -870,9 +871,9 @@ class VmDiskRemoveForm(OperationForm):
super(VmDiskRemoveForm, self).__init__(*args, **kwargs) super(VmDiskRemoveForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'disk', forms.ModelChoiceField( self.fields['disk'] = forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True, queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk'))) empty_label=None, label=_('Disk'))
if self.disk: if self.disk:
self.fields['disk'].widget = HiddenInput() self.fields['disk'].widget = HiddenInput()
...@@ -898,7 +899,7 @@ class VmDownloadDiskForm(OperationForm): ...@@ -898,7 +899,7 @@ class VmDownloadDiskForm(OperationForm):
def clean(self): def clean(self):
cleaned_data = super(VmDownloadDiskForm, self).clean() cleaned_data = super(VmDownloadDiskForm, self).clean()
if not cleaned_data['name']: if not cleaned_data['name']:
if cleaned_data['url']: if cleaned_data.get('url'):
cleaned_data['name'] = urlparse( cleaned_data['name'] = urlparse(
cleaned_data['url']).path.split('/')[-1] cleaned_data['url']).path.split('/')[-1]
if not cleaned_data['name']: if not cleaned_data['name']:
...@@ -908,6 +909,36 @@ class VmDownloadDiskForm(OperationForm): ...@@ -908,6 +909,36 @@ class VmDownloadDiskForm(OperationForm):
return cleaned_data return cleaned_data
class VmRemoveInterfaceForm(OperationForm):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.interface = kwargs.pop('default')
super(VmRemoveInterfaceForm, self).__init__(*args, **kwargs)
self.fields['interface'] = forms.ModelChoiceField(
queryset=choices, initial=self.interface, required=True,
empty_label=None, label=_('Interface'))
if self.interface:
self.fields['interface'].widget = HiddenInput()
@property
def helper(self):
helper = super(VmRemoveInterfaceForm, self).helper
if self.interface:
helper.layout = Layout(
AnyTag(
"div",
HTML(format_html(
_("<label>Vlan:</label> {0}"),
self.interface.vlan)),
css_class="form-group",
),
Field("interface"),
)
return helper
class VmAddInterfaceForm(OperationForm): class VmAddInterfaceForm(OperationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices') choices = kwargs.pop('choices')
...@@ -921,18 +952,45 @@ class VmAddInterfaceForm(OperationForm): ...@@ -921,18 +952,45 @@ class VmAddInterfaceForm(OperationForm):
self.fields['vlan'] = field self.fields['vlan'] = field
class DeployChoiceField(forms.ModelChoiceField):
def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance")
super(DeployChoiceField, self).__init__(*args, **kwargs)
def label_from_instance(self, obj):
traits = set(obj.traits.all())
req_traits = set(self.instance.req_traits.all())
# if the subset is empty the node satisfies the required traits
subset = req_traits - traits
label = "%s %s" % (
"&#xf071" if subset else "&#xf00c;", escape(obj.name),
)
if subset:
missing_traits = ", ".join(map(lambda x: escape(x.name), subset))
label += _(" (missing_traits: %s)") % missing_traits
return mark_safe(label)
class VmDeployForm(OperationForm): class VmDeployForm(OperationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices', None) choices = kwargs.pop('choices', None)
instance = kwargs.pop("instance")
super(VmDeployForm, self).__init__(*args, **kwargs) super(VmDeployForm, self).__init__(*args, **kwargs)
if choices is not None: if choices is not None:
self.fields.insert(0, 'node', forms.ModelChoiceField( self.fields['node'] = DeployChoiceField(
queryset=choices, required=False, label=_('Node'), help_text=_( queryset=choices, required=False, label=_('Node'), help_text=_(
"Deploy virtual machine to this node " "Deploy virtual machine to this node "
"(blank allows scheduling automatically)."))) "(blank allows scheduling automatically)."),
widget=forms.Select(attrs={
'class': "font-awesome-font",
}), instance=instance
)
class VmPortRemoveForm(OperationForm): class VmPortRemoveForm(OperationForm):
...@@ -942,9 +1000,9 @@ class VmPortRemoveForm(OperationForm): ...@@ -942,9 +1000,9 @@ class VmPortRemoveForm(OperationForm):
super(VmPortRemoveForm, self).__init__(*args, **kwargs) super(VmPortRemoveForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'rule', forms.ModelChoiceField( self.fields['rule'] = forms.ModelChoiceField(
queryset=choices, initial=self.rule, required=True, queryset=choices, initial=self.rule, required=True,
empty_label=None, label=_('Port'))) empty_label=None, label=_('Port'))
if self.rule: if self.rule:
self.fields['rule'].widget = HiddenInput() self.fields['rule'].widget = HiddenInput()
...@@ -961,9 +1019,9 @@ class VmPortAddForm(OperationForm): ...@@ -961,9 +1019,9 @@ class VmPortAddForm(OperationForm):
super(VmPortAddForm, self).__init__(*args, **kwargs) super(VmPortAddForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'host', forms.ModelChoiceField( self.fields['host'] = forms.ModelChoiceField(
queryset=choices, initial=self.host, required=True, queryset=choices, initial=self.host, required=True,
empty_label=None, label=_('Host'))) empty_label=None, label=_('Host'))
if self.host: if self.host:
self.fields['host'].widget = HiddenInput() self.fields['host'].widget = HiddenInput()
...@@ -1153,7 +1211,10 @@ class TraitForm(forms.ModelForm): ...@@ -1153,7 +1211,10 @@ class TraitForm(forms.ModelForm):
class MyProfileForm(forms.ModelForm): class MyProfileForm(forms.ModelForm):
preferred_language = forms.ChoiceField(LANGUAGES_WITH_CODE) preferred_language = forms.ChoiceField(
LANGUAGES_WITH_CODE,
label=_("Preferred language"),
)
class Meta: class Meta:
fields = ('preferred_language', 'email_notifications', fields = ('preferred_language', 'email_notifications',
......
...@@ -31,6 +31,7 @@ from django.db.models import ( ...@@ -31,6 +31,7 @@ from django.db.models import (
) )
from django.db.models.signals import post_save, pre_delete, post_delete from django.db.models.signals import post_save, pre_delete, post_delete
from django.templatetags.static import static from django.templatetags.static import static
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
...@@ -53,7 +54,9 @@ from .validators import connect_command_template_validator ...@@ -53,7 +54,9 @@ from .validators import connect_command_template_validator
logger = getLogger(__name__) logger = getLogger(__name__)
pwgen = User.objects.make_random_password
def pwgen():
return User.objects.make_random_password()
class Favourite(Model): class Favourite(Model):
...@@ -87,7 +90,8 @@ class Notification(TimeStampedModel): ...@@ -87,7 +90,8 @@ class Notification(TimeStampedModel):
@property @property
def subject(self): def subject(self):
return HumanReadableObject.from_dict(self.subject_data) return HumanReadableObject.from_dict(
self.escape_dict(self.subject_data))
@subject.setter @subject.setter
def subject(self, value): def subject(self, value):
...@@ -95,7 +99,14 @@ class Notification(TimeStampedModel): ...@@ -95,7 +99,14 @@ class Notification(TimeStampedModel):
@property @property
def message(self): def message(self):
return HumanReadableObject.from_dict(self.message_data) return HumanReadableObject.from_dict(
self.escape_dict(self.message_data))
def escape_dict(self, data):
for k, v in data['params'].items():
if isinstance(v, basestring):
data['params'][k] = escape(v)
return data
@message.setter @message.setter
def message(self, value): def message(self, value):
......
/* for functions in both vm list and vm detail */
$(function() { $(function() {
var in_progress = false;
var activity_hash = 5;
var show_all = false;
var reload_vm_detail = false;
/* do we need to check for new activities */
if(decideActivityRefresh()) {
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
}
/* vm operations */ $('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('fa-spin');
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
});
$("#activity-refresh").on("click", "#show-all-activities", function() {
$(this).find("i").addClass("fa-spinner fa-spin");
show_all = !show_all;
$('a[href="#activity"]').trigger("click");
return false;
});
/* operations */
$('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) { $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface, .operation-wrapper').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
...@@ -23,7 +48,7 @@ $(function() { ...@@ -23,7 +48,7 @@ $(function() {
}); });
/* if the operation fails show the modal again */ /* if the operation fails show the modal again */
$("body").on("click", "#op-form-send", function() { $("body").on("click", "#confirmation-modal #op-form-send", function() {
var url = $(this).closest("form").prop("action"); var url = $(this).closest("form").prop("action");
$.ajax({ $.ajax({
...@@ -77,4 +102,91 @@ $(function() { ...@@ -77,4 +102,91 @@ $(function() {
return false; return false;
}); });
function decideActivityRefresh() {
var check = false;
/* if something is still spinning */
if($('.timeline .activity i').hasClass('fa-spin'))
check = true;
return check;
}
function checkNewActivity(runs) {
$.ajax({
type: 'GET',
url: $('a[href="#activity"]').attr('data-activity-url'),
data: {'show_all': show_all},
success: function(data) {
var new_activity_hash = (data.activities + "").hashCode();
if(new_activity_hash != activity_hash) {
$("#activity-refresh").html(data.activities);
}
activity_hash = new_activity_hash;
$("#ops").html(data.ops);
$("#disk-ops").html(data.disk_ops);
$("[title]").tooltip();
/* changing the status text */
var icon = $("#vm-details-state i");
if(data.is_new_state) {
if(!icon.hasClass("fa-spin"))
icon.prop("class", "fa fa-spinner fa-spin");
} else {
icon.prop("class", "fa " + data.icon);
}
var vm_state = $("#vm-details-state");
if (vm_state.length) {
vm_state.data("status", data['status']); // jshint ignore:line
$("#vm-details-state span").html(data.human_readable_status.toUpperCase());
}
if(data['status'] == "RUNNING") { // jshint ignore:line
if(data.connect_uri) {
$("#dashboard-vm-details-connect-button").removeClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else {
if(data.connect_uri) {
$("#dashboard-vm-details-connect-button").addClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(data.status == "STOPPED" || data.status == "PENDING") {
$(".change-resources-button").prop("disabled", false);
$(".change-resources-help").hide();
} else {
$(".change-resources-button").prop("disabled", true);
$(".change-resources-help").show();
}
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(runs + 1);},
1000 + Math.exp(runs * 0.05)
);
} else {
in_progress = false;
if(reload_vm_detail) location.reload();
}
$('a[href="#activity"] i').removeClass('fa-spin');
},
error: function() {
in_progress = false;
}
});
}
}); });
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length === 0) return hash;
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
...@@ -23,46 +23,23 @@ html { ...@@ -23,46 +23,23 @@ html {
padding-right: 15px; padding-right: 15px;
} }
/* values for 45px tall navbar */ /* --- */
.navbar {
min-height: 45px;
}
.navbar-brand {
height: 45px;
padding: 12.5px 12.5px;
}
.navbar-toggle { #dashboard-menu > li > a {
margin-top: 5.5px; color: white;
margin-bottom: 5.5px; font-size: 10px;
} }
.navbar-form {
margin-top: 5.5px;
margin-bottom: 5.5px;
}
.navbar-btn { #dashboard-menu {
margin-top: 5.5px; margin-right: 15px;
margin-bottom: 5.5px;
} }
.navbar-btn.btn-sm { /* we need this for mobile view */
margin-top: 7.5px; .container > :first-child {
margin-bottom: 7.5px; margin-top: 15px;
}
.navbar-btn.btn-xs {
margin-top: 11.5px;
margin-bottom: 11.5px;
}
.navbar-text {
margin-top: 12.5px;
margin-bottom: 12.5px;
} }
/* --- */
/* Responsive: Portrait tablets and up */ /* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
/* Let the jumbotron breathe */ /* Let the jumbotron breathe */
...@@ -80,13 +57,17 @@ html { ...@@ -80,13 +57,17 @@ html {
} }
} }
.no-margin { .no-margin {
margin: 0!important; margin: 0!important;
} }
.list-group .list-group-footer { .list-group .list-group-footer {
padding-top: 5px; height: 41px;
padding-bottom: 5px; }
.list-group-footer .text-right {
padding-top: 4px;
} }
.big { .big {
...@@ -194,7 +175,7 @@ html { ...@@ -194,7 +175,7 @@ html {
} }
.dashboard-index .panel { .dashboard-index .panel {
height: 300px; height: 294px;
} }
#vm-details-rename, #vm-details-h1-name, #vm-details-rename , #vm-details-rename, #vm-details-h1-name, #vm-details-rename ,
...@@ -207,11 +188,15 @@ html { ...@@ -207,11 +188,15 @@ html {
display: none; display: none;
} }
.vm-details-home-name { #group-details-rename-form {
display: inline-block;
}
.vm-details-home-name, #group-details-rename-form .input-group {
max-width: 401px; max-width: 401px;
} }
#node-details-rename-name, #group-details-rename-name { #node-details-rename-name {
max-width: 160px; max-width: 160px;
} }
...@@ -397,10 +382,6 @@ a.hover-black { ...@@ -397,10 +382,6 @@ a.hover-black {
font-size: 12px; font-size: 12px;
} }
#notification-button {
margin-right: 15px;
}
#vm-migrate-node-list { #vm-migrate-node-list {
list-style: none; list-style: none;
} }
...@@ -516,15 +497,6 @@ footer a, footer a:hover, footer a:visited { ...@@ -516,15 +497,6 @@ footer a, footer a:hover, footer a:visited {
padding: 5px; /* it's nice this way in the tour */ padding: 5px; /* it's nice this way in the tour */
} }
.index-vm-list-name {
display: inline-block;
max-width: 70%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
float: left;
}
#dashboard-vm-list a small { #dashboard-vm-list a small {
padding-left: 10px; padding-left: 10px;
} }
...@@ -590,8 +562,8 @@ footer a, footer a:hover, footer a:visited { ...@@ -590,8 +562,8 @@ footer a, footer a:hover, footer a:visited {
} }
#dashboard-vm-list, #dashboard-node-list, #dashboard-group-list, #dashboard-vm-list, #dashboard-node-list, #dashboard-group-list,
#dashboard-template-list { #dashboard-template-list, #dashboard-files-toplist {
min-height: 204px; min-height: 200px;
} }
#group-detail-user-table td:first-child, #group-detail-user-table th:last-child, #group-detail-user-table td:first-child, #group-detail-user-table th:last-child,
...@@ -754,15 +726,14 @@ textarea[name="new_members"] { ...@@ -754,15 +726,14 @@ textarea[name="new_members"] {
} }
#dashboard-files-toplist { #dashboard-files-toplist {
min-height: 204px; div.list-group-item {
} color: #555;
height: 41px;
#dashboard-files-toplist div.list-group-item {
color: #555;
}
#dashboard-files-toplist div.list-group-item:hover { &:hover {
background: #eee; background: #eee;
}
}
} }
.store-list-item-name { .store-list-item-name {
...@@ -961,6 +932,11 @@ textarea[name="new_members"] { ...@@ -961,6 +932,11 @@ textarea[name="new_members"] {
#vm-list-search, #vm-mass-ops { #vm-list-search, #vm-mass-ops {
margin-top: 8px; margin-top: 8px;
} }
.list-group-item {
border-bottom: 0px !important;
}
.list-group-item-last { .list-group-item-last {
border-bottom: 1px solid #ddd !important; border-bottom: 1px solid #ddd !important;
} }
...@@ -1105,6 +1081,25 @@ textarea[name="new_members"] { ...@@ -1105,6 +1081,25 @@ textarea[name="new_members"] {
text-align: center; text-align: center;
} }
.vm-create-list-name {
display: inline-block;
max-width: 60%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
float: left;
}
.vm-create-list-system {
display: inline-block;
max-width: 40%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
float: right;
}
/* for introjs /* for introjs
* newer version has this fixed * newer version has this fixed
* but it doesn't work w bootstrap 3.2.0 * but it doesn't work w bootstrap 3.2.0
...@@ -1151,3 +1146,78 @@ textarea[name="new_members"] { ...@@ -1151,3 +1146,78 @@ textarea[name="new_members"] {
background-position: 0 0px; background-position: 0 0px;
} }
} }
#dashboard-vm-list {
.list-group-item {
display: flex;
}
.index-vm-list-name, .index-vm-list-host {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.index-vm-list-name {
max-width: 70%;
}
.index-vm-list-host {
padding-top: 3px;
flex: 1;
}
}
.fa-fw-12 {
/* fa-fw is too wide */
width: 12px;
}
.btn-op-form-send {
padding: 6px 12px 6px 8px;
}
@media (max-width: 767px) {
#vm-detail-panel .graph-buttons {
padding-top: 15px;
}
.graph-buttons a {
margin-bottom: 8px;
}
#ops .operation {
margin-bottom: 5px;
}
.vm-details-connection dd {
margin-left: 25px;
}
.vm-details-connection dt {
padding-left: 0px;
}
}
#notifications-upper-pagination {
margin-top: 4px;
}
#notifications-bottom-pagination {
* {
display: inline-block;
}
a {
font-size: 20px;
&:hover {
text-decoration: none;
}
}
.page-numbers {
padding: 25px;
}
}
$(function() {
/* rename */ /* rename */
$("#group-details-h1-name, .group-details-rename-button").click(function() { $("#group-details-h1-name, .group-details-rename-button").click(function() {
$("#group-details-h1-name").hide(); $("#group-details-h1-name span").hide();
$("#group-details-rename").css('display', 'inline'); $("#group-details-rename-form").show().css('display', 'inline-block');
$("#group-details-rename-name").focus(); $("#group-details-rename-name").select();
}); });
/* rename ajax */ /* rename ajax */
$('#group-details-rename-submit').click(function() { $('#group-details-rename-submit').click(function() {
if(!$("#group-details-rename-name")[0].checkValidity()) {
return true;
}
var name = $('#group-details-rename-name').val(); var name = $('#group-details-rename-name').val();
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: location.href, url: location.href,
data: {'new_name': name}, data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
$("#group-details-h1-name").text(data['new_name']).show(); $("#group-details-h1-name span").text(data.new_name).show();
$('#group-details-rename').hide(); $('#group-details-rename-form').hide();
// addMessage(data['message'], "success");
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger"); addMessage("Error during renaming.", "danger");
} }
}); });
return false; return false;
}); });
});
$(".group-details-help-button").click(function() {
$(".group-details-help").stop().slideToggle();
});
/* for Node removes buttons */
$('.delete-from-group').click(function() {
var href = $(this).attr('href');
var tr = $(this).closest('tr');
var group = $(this).data('group_pk');
var member = $(this).data('member_pk');
var dir = window.location.pathname.indexOf('list') == -1;
addModalConfirmation(removeMember,
{ 'url': href,
'data': [],
'tr': tr,
'group_pk': group,
'member_pk': member,
'type': "user",
'redirect': dir});
return false;
});
function removeMember(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
data.tr.fadeOut(function() {
$(this).remove();});
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
$(function() { $(function() {
/* rename */ /* rename */
$("#group-list-rename-button, .group-details-rename-button").click(function() { $("#group-list-rename-button, .group-details-rename-button").click(function() {
$("#group-list-column-name", $(this).closest("tr")).hide(); $(".group-list-column-name", $(this).closest("tr")).hide();
$("#group-list-rename", $(this).closest("tr")).css('display', 'inline'); $("#group-list-rename", $(this).closest("tr")).css('display', 'inline');
$("#group-list-rename").find("input").select(); $("#group-list-rename").find("input").select();
}); });
...@@ -10,7 +10,7 @@ $(function() { ...@@ -10,7 +10,7 @@ $(function() {
$('.group-list-rename-submit').click(function() { $('.group-list-rename-submit').click(function() {
var row = $(this).closest("tr"); var row = $(this).closest("tr");
var name = $('#group-list-rename-name', row).val(); var name = $('#group-list-rename-name', row).val();
var url = '/dashboard/group/' + row.children("td:first-child").text().replace(" ", "") + '/'; var url = row.find(".group-list-column-name a").prop("href");
$.ajax({ $.ajax({
method: 'POST', method: 'POST',
url: url, url: url,
...@@ -18,7 +18,7 @@ $(function() { ...@@ -18,7 +18,7 @@ $(function() {
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
$("#group-list-column-name", row).html( $(".group-list-column-name", row).html(
$("<a/>", { $("<a/>", {
'class': "real-link", 'class': "real-link",
href: "/dashboard/group/" + data.group_pk + "/", href: "/dashboard/group/" + data.group_pk + "/",
......
$(function() {
nodeCreateLoaded();
});
function nodeCreateLoaded() {
/* no js compatibility */
$('.no-js-hidden').show();
$('.js-hidden').hide();
}
...@@ -15,7 +15,7 @@ $(function() { ...@@ -15,7 +15,7 @@ $(function() {
data: {'new_name': name}, data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
$("#node-details-h1-name").text(data['new_name']).show(); $("#node-details-h1-name").text(data.new_name).show();
$('#node-details-rename').hide(); $('#node-details-rename').hide();
// addMessage(data.message, "success"); // addMessage(data.message, "success");
}, },
...@@ -30,20 +30,6 @@ $(function() { ...@@ -30,20 +30,6 @@ $(function() {
$(".node-details-help").stop().slideToggle(); $(".node-details-help").stop().slideToggle();
}); });
/* for Node removes buttons */
$('.node-enable').click(function() {
var node_pk = $(this).data('node-pk');
var dir = window.location.pathname.indexOf('list') == -1;
addModalConfirmation(changeNodeStatus,
{ 'url': '/dashboard/node/status/' + node_pk + '/',
'data': [],
'pk': node_pk,
'type': "node",
'redirect': dir});
return false;
});
// remove trait // remove trait
$('.node-details-remove-trait').click(function() { $('.node-details-remove-trait').click(function() {
var to_remove = $(this).data("trait-pk"); var to_remove = $(this).data("trait-pk");
...@@ -69,22 +55,3 @@ $(function() { ...@@ -69,22 +55,3 @@ $(function() {
}); });
}); });
function changeNodeStatus(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
if(!data.redirect) {
selected = [];
addMessage(re.message, 'success');
} else {
window.location.replace('/dashboard');
}
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
...@@ -9,49 +9,4 @@ $(function() { ...@@ -9,49 +9,4 @@ $(function() {
$('.false').closest("tr").addClass('danger'); $('.false').closest("tr").addClass('danger');
$('.true').closest("tr").removeClass('danger'); $('.true').closest("tr").removeClass('danger');
} }
function statuschangeSuccess(tr){
var tspan=tr.children('.enabled').children();
var buttons=tr.children('.actions').children('.btn-group').children('.dropdown-menu').children('li').children('.node-enable');
buttons.each(function(index){
if ($(this).css("display")=="block"){
$(this).css("display","none");
}
else{
$(this).css("display","block");
}
});
if(tspan.hasClass("false")){
tspan.removeClass("false");
tspan.addClass("true");
tspan.text("✔");
}
else{
tspan.removeClass("true");
tspan.addClass("false");
tspan.text("✘");
}
colortable();
}
$('#table_container').on('click','.node-enable',function() {
var tr= $(this).closest("tr");
var pk =$(this).attr('data-node-pk');
var url = $(this).attr('href');
$.ajax({
method: 'POST',
url: url,
data: {'change_status':''},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
statuschangeSuccess(tr);
},
error: function(xhr, textStatus, error) {
addMessage("Error!", "danger");
}
});
return false;
});
}); });
$(function() { $(function() {
/* for template removes buttons */
$('.template-delete').click(function() {
var template_pk = $(this).data('template-pk');
addModalConfirmationOrDisplayMessage(deleteTemplate,
{ 'url': '/dashboard/template/delete/' + template_pk + '/',
'data': [],
'template_pk': template_pk,
});
return false;
});
/* for lease removes buttons */
$('.lease-delete').click(function() {
var lease_pk = $(this).data('lease-pk');
addModalConfirmationOrDisplayMessage(deleteLease,
{ 'url': '/dashboard/lease/delete/' + lease_pk + '/',
'data': [],
'lease_pk': lease_pk,
});
return false;
});
/* template table sort */ /* template table sort */
var ttable = $(".template-list-table").stupidtable(); var ttable = $(".template-list-table").stupidtable();
...@@ -43,67 +21,3 @@ $(function() { ...@@ -43,67 +21,3 @@ $(function() {
event.preventDefault(); event.preventDefault();
}); });
}); });
// send POST request then delete the row in table
function deleteTemplate(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
addMessage(re.message, 'success');
$('a[data-template-pk="' + data.template_pk + '"]').closest('tr').fadeOut(function() {
$(this).remove();
});
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
// send POST request then delete the row in table
function deleteLease(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
addMessage(re.message, 'success');
$('a[data-lease-pk="' + data.lease_pk + '"]').closest('tr').fadeOut(function() {
$(this).remove();
});
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
function addModalConfirmationOrDisplayMessage(func, data) {
$.ajax({
type: 'GET',
url: data['url'],
data: jQuery.param(data['data']),
success: function(result) {
$('body').append(result);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
$('#confirmation-modal-button').click(function() {
func(data);
$('#confirmation-modal').modal('hide');
});
},
error: function(xhr, textStatus, error) {
if(xhr.status === 403) {
addMessage(gettext("Only the owners can delete the selected object."), "warning");
} else {
addMessage(gettext("An error occurred. (") + xhr.status + ")", 'danger')
}
}
});
}
...@@ -20,15 +20,15 @@ function vmCreateLoaded() { ...@@ -20,15 +20,15 @@ function vmCreateLoaded() {
var template = $(this).data("template-pk"); var template = $(this).data("template-pk");
$.get("/dashboard/vm/create/?template=" + template, function(data) { $.get("/dashboard/vm/create/?template=" + template, function(data) {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
$('body').append(data); $('body').append(data);
vmCreateLoaded(); vmCreateLoaded();
addSliderMiscs(); addSliderMiscs();
$('#create-modal').modal('show'); $('#confirmation-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#confirmation-modal').remove();
}); });
$("#create-modal").on("shown.bs.modal", function() { $("#confirmation-modal").on("shown.bs.modal", function() {
setDefaultSliderValues(); setDefaultSliderValues();
}); });
}); });
...@@ -48,18 +48,18 @@ function vmCreateLoaded() { ...@@ -48,18 +48,18 @@ function vmCreateLoaded() {
window.location.replace(data.redirect + '#activity'); window.location.replace(data.redirect + '#activity');
} }
else { else {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
$('body').append(data); $('body').append(data);
vmCreateLoaded(); vmCreateLoaded();
addSliderMiscs(); addSliderMiscs();
$('#create-modal').modal('show'); $('#confirmation-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#confirmation-modal').remove();
}); });
} }
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
if (xhr.status == 500) { if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger"); addMessage("500 Internal Server Error", "danger");
...@@ -211,7 +211,7 @@ function vmCustomizeLoaded() { ...@@ -211,7 +211,7 @@ function vmCustomizeLoaded() {
}); });
/* start vm button clicks */ /* start vm button clicks */
$('#vm-create-customized-start').click(function() { $('#confirmation-modal #vm-create-customized-start').click(function() {
var error = false; var error = false;
$(".cpu-count-input, .ram-input, #id_name, #id_amount ").each(function() { $(".cpu-count-input, .ram-input, #id_name, #id_amount ").each(function() {
if(!$(this)[0].checkValidity()) { if(!$(this)[0].checkValidity()) {
...@@ -222,8 +222,6 @@ function vmCustomizeLoaded() { ...@@ -222,8 +222,6 @@ function vmCustomizeLoaded() {
$(this).find("i").prop("class", "fa fa-spinner fa-spin"); $(this).find("i").prop("class", "fa fa-spinner fa-spin");
if($("#create-modal")) return true;
$.ajax({ $.ajax({
url: '/dashboard/vm/create/', url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
...@@ -238,18 +236,18 @@ function vmCustomizeLoaded() { ...@@ -238,18 +236,18 @@ function vmCustomizeLoaded() {
window.location.href = data.redirect + '#activity'; window.location.href = data.redirect + '#activity';
} }
else { else {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
$('body').append(data); $('body').append(data);
vmCreateLoaded(); vmCreateLoaded();
addSliderMiscs(); addSliderMiscs();
$('#create-modal').modal('show'); $('#confirmation-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#confirmation-modal').remove();
}); });
} }
}, },
error: function(xhr, textStatus, error) { error: function(xhr, textStatus, error) {
var r = $('#create-modal'); r.next('div').remove(); r.remove(); var r = $('#confirmation-modal'); r.next('div').remove(); r.remove();
if (xhr.status == 500) { if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger"); addMessage("500 Internal Server Error", "danger");
......
var show_all = false;
var in_progress = false;
var activity_hash = 5;
var Websock_native; // not sure var Websock_native; // not sure
var reload_vm_detail = false;
$(function() { $(function() {
/* do we need to check for new activities */
if(decideActivityRefresh()) {
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
}
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('fa-spin');
if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
});
$("#activity-refresh").on("click", "#show-all-activities", function() {
$(this).find("i").addClass("fa-spinner fa-spin");
show_all = !show_all;
$('a[href="#activity"]').trigger("click");
return false;
});
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function(e) { $('#vm-details-resources-save').click(function(e) {
var error = false; var error = false;
...@@ -43,7 +16,7 @@ $(function() { ...@@ -43,7 +16,7 @@ $(function() {
var vm = $(this).data("vm"); var vm = $(this).data("vm");
$.ajax({ $.ajax({
type: 'POST', type: 'POST',
url: "/dashboard/vm/" + vm + "/op/resources_change/", url: $(this).parent("form").prop('action'),
data: $('#vm-details-resources-form').serialize(), data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
if(data.success) { if(data.success) {
...@@ -89,17 +62,6 @@ $(function() { ...@@ -89,17 +62,6 @@ $(function() {
return false; return false;
}); });
/* remove port */
$('.vm-details-remove-port').click(function() {
addModalConfirmation(removePort,
{
'url': $(this).prop("href"),
'data': [],
'rule': $(this).data("rule")
});
return false;
});
/* for js fallback */ /* for js fallback */
$("#vm-details-pw-show").parent("div").children("input").prop("type", "password"); $("#vm-details-pw-show").parent("div").children("input").prop("type", "password");
...@@ -108,8 +70,8 @@ $(function() { ...@@ -108,8 +70,8 @@ $(function() {
var input = $(this).parent("div").children("input"); var input = $(this).parent("div").children("input");
var eye = $(this).children("#vm-details-pw-eye"); var eye = $(this).children("#vm-details-pw-eye");
var span = $(this); var span = $(this);
span.tooltip("destroy") span.tooltip("destroy");
if(eye.hasClass("fa-eye")) { if(eye.hasClass("fa-eye")) {
eye.removeClass("fa-eye").addClass("fa-eye-slash"); eye.removeClass("fa-eye").addClass("fa-eye-slash");
input.prop("type", "text"); input.prop("type", "text");
...@@ -123,80 +85,6 @@ $(function() { ...@@ -123,80 +85,6 @@ $(function() {
span.tooltip(); span.tooltip();
}); });
/* change password confirmation */
$("#vm-details-pw-change").click(function() {
$("#vm-details-pw-confirm").fadeIn();
return false;
});
/* change password */
$(".vm-details-pw-confirm-choice").click(function() {
choice = $(this).data("choice");
if(choice) {
pk = $(this).data("vm");
$.ajax({
type: 'POST',
url: "/dashboard/vm/" + pk + "/",
data: {'change_password': 'true'},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
location.reload();
},
error: function(xhr, textStatus, error) {
if (xhr.status == 500) {
addMessage("Internal Server Error", "danger");
} else {
addMessage(xhr.status + " Unknown Error", "danger");
}
}
});
} else {
$("#vm-details-pw-confirm").fadeOut();
}
return false;
});
/* add network button */
$("#vm-details-network-add").click(function() {
$("#vm-details-network-add-form").toggle();
return false;
});
/* add disk button */
$("#vm-details-disk-add").click(function() {
$("#vm-details-disk-add-for-form").html($("#vm-details-disk-add-form").html());
return false;
});
/* for interface remove buttons */
$('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk');
addModalConfirmation(removeInterface,
{ 'url': '/dashboard/interface/' + interface_pk + '/delete/',
'data': [],
'pk': interface_pk,
'type': "interface",
});
return false;
});
/* removing interface post */
function removeInterface(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
/* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
location.reload();
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger');
}
});
}
/* rename */ /* rename */
$("#vm-details-h1-name, .vm-details-rename-button").click(function() { $("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide(); $("#vm-details-h1-name").hide();
...@@ -244,7 +132,7 @@ $(function() { ...@@ -244,7 +132,7 @@ $(function() {
var tmp = ta.val(); var tmp = ta.val();
ta.val(""); ta.val("");
ta.focus(); ta.focus();
ta.val(tmp) ta.val(tmp);
e.preventDefault(); e.preventDefault();
}); });
...@@ -319,7 +207,7 @@ $(function() { ...@@ -319,7 +207,7 @@ $(function() {
$("#dashboard-tutorial-toggle").click(function() { $("#dashboard-tutorial-toggle").click(function() {
var box = $("#alert-new-template"); var box = $("#alert-new-template");
var list = box.find("ol") var list = box.find("ol");
list.stop().slideToggle(function() { list.stop().slideToggle(function() {
var url = box.find("form").prop("action"); var url = box.find("form").prop("action");
var hidden = list.css("display") === "none"; var hidden = list.css("display") === "none";
...@@ -331,114 +219,8 @@ $(function() { ...@@ -331,114 +219,8 @@ $(function() {
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {} success: function(re, textStatus, xhr) {}
}); });
}); });
return false; return false;
}); });
}); });
function removePort(data) {
$.ajax({
type: 'POST',
url: data.url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
$("a[data-rule=" + data.rule + "]").each(function() {
$(this).closest("tr").fadeOut(500, function() {
$(this).remove();
});
});
addMessage(re.message, "success");
},
error: function(xhr, textStatus, error) {
}
});
}
function decideActivityRefresh() {
var check = false;
/* if something is still spinning */
if($('.timeline .activity i').hasClass('fa-spin'))
check = true;
return check;
}
function checkNewActivity(runs) {
var instance = location.href.split('/'); instance = instance[instance.length - 2];
$.ajax({
type: 'GET',
url: '/dashboard/vm/' + instance + '/activity/',
data: {'show_all': show_all},
success: function(data) {
var new_activity_hash = (data.activities + "").hashCode();
if(new_activity_hash != activity_hash) {
$("#activity-refresh").html(data.activities);
}
activity_hash = new_activity_hash;
$("#ops").html(data.ops);
$("#disk-ops").html(data.disk_ops);
$("[title]").tooltip();
/* changing the status text */
var icon = $("#vm-details-state i");
if(data.is_new_state) {
if(!icon.hasClass("fa-spin"))
icon.prop("class", "fa fa-spinner fa-spin");
} else {
icon.prop("class", "fa " + data.icon);
}
$("#vm-details-state").data("status", data['status']);
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") {
if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").removeClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "pill").attr("href", "#console").parent("li").removeClass("disabled");
} else {
if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").addClass('disabled');
}
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(data.status == "STOPPED" || data.status == "PENDING") {
$(".change-resources-button").prop("disabled", false);
$(".change-resources-help").hide();
} else {
$(".change-resources-button").prop("disabled", true);
$(".change-resources-help").show();
}
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(runs + 1);},
1000 + Math.exp(runs * 0.05)
);
} else {
in_progress = false;
if(reload_vm_detail) location.reload();
}
$('a[href="#activity"] i').removeClass('fa-spin');
},
error: function() {
in_progress = false;
}
});
}
String.prototype.hashCode = function() {
var hash = 0, i, chr, len;
if (this.length === 0) return hash;
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};
...@@ -82,7 +82,7 @@ $(function() { ...@@ -82,7 +82,7 @@ $(function() {
/* mass operations */ /* mass operations */
$("#vm-mass-ops").on('click', '.mass-operation', function(e) { $("#vm-mass-ops").on('click', '.mass-operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
params = "?" + selected.map(function(a){return "vm=" + a.vm}).join("&"); params = "?" + selected.map(function(a){return "vm=" + a.vm;}).join("&");
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
...@@ -212,7 +212,7 @@ function updateStatuses(runs) { ...@@ -212,7 +212,7 @@ function updateStatuses(runs) {
if(checkStatusUpdate()) { if(checkStatusUpdate()) {
setTimeout( setTimeout(
function() {updateStatuses(runs + 1)}, function() {updateStatuses(runs + 1);},
1000 + Math.exp(runs * 0.05) 1000 + Math.exp(runs * 0.05)
); );
} }
......
...@@ -82,7 +82,6 @@ html { ...@@ -82,7 +82,6 @@ html {
z-index: 1; z-index: 1;
} }
.nojs-dropdown-toggle:focus + .nojs-dropdown-menu .nojs-dropdown-toggle:focus + .nojs-dropdown-menu
{ {
display: block; display: block;
...@@ -98,32 +97,6 @@ html { ...@@ -98,32 +97,6 @@ html {
display: block; display: block;
} }
.notification-messages {
padding: 10px 8px;
width: 350px;
}
.notification-message {
margin-bottom: 10px;
padding: 0 0 4px 0;
border-bottom: 1px dotted #D3D3D3;
}
.notification-messages .notification-message:last-child {
margin-bottom: 0px;
padding: 0px;
border-bottom: none;
}
.notification-message-text {
padding: 8px 15px;
display: none;
}
.notification-message .notification-message-subject {
cursor: pointer;
}
/* footer */ /* footer */
footer { footer {
position: absolute; position: absolute;
...@@ -148,10 +121,6 @@ footer a, footer a:hover, footer a:visited { ...@@ -148,10 +121,6 @@ footer a, footer a:hover, footer a:visited {
display: none; display: none;
} }
#notifications-button {
margin: 0;
}
/* 2px border bottom for all bootstrap tables */ /* 2px border bottom for all bootstrap tables */
.table thead>tr>th { .table thead>tr>th {
border-bottom: 1px; border-bottom: 1px;
......
...@@ -19,7 +19,8 @@ from __future__ import absolute_import ...@@ -19,7 +19,8 @@ from __future__ import absolute_import
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_tables2 import Table, A from django_tables2 import Table, A
from django_tables2.columns import TemplateColumn, Column, LinkColumn from django_tables2.columns import (TemplateColumn, Column, LinkColumn,
BooleanColumn)
from vm.models import Node, InstanceTemplate, Lease from vm.models import Node, InstanceTemplate, Lease
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -67,12 +68,18 @@ class NodeListTable(Table): ...@@ -67,12 +68,18 @@ class NodeListTable(Table):
orderable=False, orderable=False,
) )
minion_online = BooleanColumn(
verbose_name=_("Minion online"),
attrs={'th': {'class': 'node-list-table-thin'}},
orderable=False,
)
class Meta: class Meta:
model = Node model = Node
attrs = {'class': ('table table-bordered table-striped table-hover ' attrs = {'class': ('table table-bordered table-striped table-hover '
'node-list-table')} 'node-list-table')}
fields = ('pk', 'name', 'host', 'get_status_display', 'priority', fields = ('pk', 'name', 'host', 'get_status_display', 'priority',
'overcommit', 'number_of_VMs', ) 'minion_online', 'overcommit', 'number_of_VMs', )
class GroupListTable(Table): class GroupListTable(Table):
......
...@@ -64,7 +64,6 @@ ...@@ -64,7 +64,6 @@
<a href="{% url "info.support" %}">{% trans "Support" %}</a> <a href="{% url "info.support" %}">{% trans "Support" %}</a>
<span class="pull-right">{{ COMPANY_NAME }}</span> <span class="pull-right">{{ COMPANY_NAME }}</span>
</footer> </footer>
</body>
<script src="{% static "jquery/dist/jquery.min.js" %}"></script> <script src="{% static "jquery/dist/jquery.min.js" %}"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script> <script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
...@@ -78,4 +77,5 @@ ...@@ -78,4 +77,5 @@
{% block extra_etc %} {% block extra_etc %}
{% endblock %} {% endblock %}
</body>
</html> </html>
<img src="{{ STATIC_URL}}dashboard/img/logo.png" style="height: 25px;"/> <img src="{{ STATIC_URL}}dashboard/img/logo.png" alt="circle logo" style="height: 25px;"/>
<img src="{{ STATIC_URL}}local-logo.png" style="padding-left: 2px; height: 25px;"/> <img src="{{ STATIC_URL}}local-logo.png" alt="local logo" style="padding-left: 2px; height: 25px;"/>
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}" <a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-{{ op.remove_disk.effect}} pull-right operation disk-remove-btn class="btn btn-xs btn-{{ op.remove_disk.effect}} pull-right operation disk-remove-btn
{% if op.remove_disk.disabled %}disabled{% endif %}"> {% if op.remove_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.remove_disk.icon }}"></i> {% trans "Remove" %} <i class="fa fa-{{ op.remove_disk.icon }} fa-fw-12"></i> {% trans "Remove" %}
</a> </a>
</span> </span>
{% endif %} {% endif %}
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}" <a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-{{ op.resize_disk.effect }} pull-right operation disk-resize-btn class="btn btn-xs btn-{{ op.resize_disk.effect }} pull-right operation disk-resize-btn
{% if op.resize_disk.disabled %}disabled{% endif %}"> {% if op.resize_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.resize_disk.icon }}"></i> {% trans "Resize" %} <i class="fa fa-{{ op.resize_disk.icon }} fa-fw-12"></i> {% trans "Resize" %}
</a> </a>
</span> </span>
{% endif %} {% endif %}
......
{% load i18n %} {% load i18n %}
{% load hro %} {% load hro %}
{% for n in notifications %} {% for n in page %}
<li class="notification-message" id="msg-{{n.id}}"> <li class="notification-message" id="msg-{{n.id}}">
<span class="notification-message-subject"> <span class="notification-message-subject">
{% if n.status == "new" %}<i class="fa fa-envelope-o"></i> {% endif %} {% if n.status == "new" %}<i class="fa fa-envelope-o"></i> {% endif %}
......
...@@ -5,8 +5,14 @@ ...@@ -5,8 +5,14 @@
{% for t in templates %} {% for t in templates %}
<div class="vm-create-template"> <div class="vm-create-template">
<div class="vm-create-template-summary"> <div class="vm-create-template-summary">
{{ t.name }} <span class="vm-create-list-name">
<span class="pull-right"><i class="fa fa-{{ t.os_type }}"></i> {{ t.system }}</span> {{ t.name }}
</span>
<span class="vm-create-list-system">
<i class="fa fa-{{ t.os_type }}"></i>
{{ t.system }}
</span>
<div class="clearfix"></div>
</div> </div>
<div class="vm-create-template-details"> <div class="vm-create-template-details">
<ul> <ul>
......
...@@ -23,11 +23,35 @@ Choose a compute node to migrate {{obj}} to. ...@@ -23,11 +23,35 @@ Choose a compute node to migrate {{obj}} to.
<i class="fa {{n.get_status_icon}}"></i> {{n.get_status_display}}</div> <i class="fa {{n.get_status_icon}}"></i> {{n.get_status_display}}</div>
{% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %} {% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %}
{% if recommended == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %} {% if recommended == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
{% if n.pk not in nodes_w_traits %}
<div class="label label-warning">
<i class="fa fa-warning"></i>
{% trans "missing traits" %}</div>
{% endif %}
</label> </label>
<input id="migrate-to-{{n.pk}}" type="radio" name="to_node" value="{{ n.pk }}" style="float: right;" <input id="migrate-to-{{n.pk}}" type="radio" name="to_node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %} {% if current == n.pk %}disabled="disabled"{% endif %}
{% if recommended == n.pk %}checked="checked"{% endif %} {% if recommended == n.pk and n.pk != current %}checked="checked"{% endif %}
/> />
{% if n.pk not in nodes_w_traits %}
<span class="vm-migrate-node-property">
{% trans "Node traits" %}:
{% if n.traits.all %}
{{ n.traits.all|join:", " }}
{% else %}
-
{% endif %}
</span>
<span class="vm-migrate-node-property">
{% trans "Required traits" %}:
{% if object.req_traits.all %}
{{ object.req_traits.all|join:", " }}
{% else %}
-
{% endif %}
</span>
<hr />
{% endif %}
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> <span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property"> <span class="vm-migrate-node-property">
{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> {% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
......
...@@ -18,35 +18,53 @@ ...@@ -18,35 +18,53 @@
{% block navbar %} {% block navbar %}
{% if user.is_authenticated and user.pk and not request.token_user %} {% if user.is_authenticated and user.pk and not request.token_user %}
<ul class="nav navbar-nav pull-right"> <ul class="nav navbar-nav navbar-right" id="dashboard-menu">
<li class="dropdown" id="notification-button"> {% if user.is_superuser %}
<a href="{% url "dashboard.views.notifications" %}" <li>
class="dropdown-toggle" data-toggle="dropdown"> <a href="/admin/"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
</li>
<li>
<a href="/network/"><i class="fa fa-globe"></i> {% trans "Network" %}</a>
</li>
{% endif %}
<li>
<a href="{% url "dashboard.views.profile-preferences" %}">
<i class="fa fa-user"></i>
{% include "dashboard/_display-name.html" with user=user show_org=True %}
</a>
</li>
<li>
<a href="{% url "logout" %}?next={% url "login" %}">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a>
</li>
<li class="visible-xs">
<a href="{% url "dashboard.views.notifications" %}">
{% trans "Notifications" %} {% trans "Notifications" %}
{% if NEW_NOTIFICATIONS_COUNT > 0 %} {% if NEW_NOTIFICATIONS_COUNT > 0 %}
<span class="badge badge-pulse">{{ NEW_NOTIFICATIONS_COUNT }}</span> <span class="badge badge-pulse">{{ NEW_NOTIFICATIONS_COUNT }}</span>
{% endif %} {% endif %}
</a> </a>
</li>
<li class="dropdown hidden-xs" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}"
class="dropdown-toggle" data-toggle="dropdown"
id="notification_count" data-notifications="{{ NEW_NOTIFICATIONS_COUNT }}">
{% trans "Notifications" %}
{% if NEW_NOTIFICATIONS_COUNT > 0 %}
<span class="badge badge-pulse">
{{ NEW_NOTIFICATIONS_COUNT }}
</span>
{% endif %}
</a>
<ul class="dropdown-menu" id="notification-messages"> <ul class="dropdown-menu" id="notification-messages">
<li>{% trans "Loading..." %}</li> <li>{% trans "Loading..." %}</li>
</ul> </ul>
</li> </li>
</ul> </ul>
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">
<i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a>
<a class="navbar-brand pull-right" href="{% url "dashboard.views.profile-preferences" %}" style="color: white; font-size: 10px;">
<i class="fa fa-user"></i>
{% include "dashboard/_display-name.html" with user=user show_org=True %}
</a>
{% if user.is_superuser %}
<a class="navbar-brand pull-right" href="/network/" style="color: white; font-size: 10px;"><i class="fa fa-globe"></i> {% trans "Network" %}</a>
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
{% endif %}
{% else %} {% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={{ request.path }}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a> <a class="navbar-brand pull-right" href="{% url "login" %}?next={{ request.path }}"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
...@@ -3,19 +3,25 @@ ...@@ -3,19 +3,25 @@
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-body"> <div class="modal-body">
{% if text %} {% if member %}
{{ text|safe }} {% blocktrans with group=object member=member %}
Do you really want to remove <strong>{{ member }}</strong> from {{ group }}?
{% endblocktrans %}
{% else %} {% else %}
{%blocktrans with object=object%} {% blocktrans with object=object %}
Are you sure you want to delete <strong>{{ object }}</strong>? Are you sure you want to delete <strong>{{ object }}</strong>?
{%endblocktrans%} {% endblocktrans %}
{% endif %} {% endif %}
<br /> <br />
<div class="pull-right" style="margin-top: 15px;"> <div class="pull-right" style="margin-top: 15px;">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button> <form action="{{ request.path }}" method="POST">
<button id="confirmation-modal-button" type="button" class="btn btn-danger" {% csrf_token %}
{% if disable_submit %}disabled{% endif %} <button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
>{% trans "Delete" %}</button> <input type="hidden" name="next" value="{{ request.GET.next }}"/>
<button class="btn btn-danger"
{% if disable_submit %}disabled{% endif %}
>{% trans "Delete" %}</button>
</form>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
......
{% load i18n %}
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to change <strong>{{ object }}</strong> status?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="{% url "dashboard.views.status-node" pk=object.pk %}" method="POST">
{% csrf_token %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<input type="hidden" name="change_status" value=""/>
<button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button>
</form>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% load i18n %}
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to remove <strong>{{ member }}</strong> from <strong>{{ object }}</strong>?
{%endblocktrans%}
{% endif %}
<br />
<div class="pull-right" style="margin-top: 15px;">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button id="confirmation-modal-button" type="button" class="btn btn-warning">Remove</button>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
...@@ -17,12 +17,18 @@ ...@@ -17,12 +17,18 @@
{% if text %} {% if text %}
{{ text|safe }} {{ text|safe }}
{% else %} {% else %}
{%blocktrans with object=object%} {% if member %}
Are you sure you want to delete <strong>{{ object }}</strong>? {% blocktrans with group=object member=member %}
{%endblocktrans%} Do you really want to remove <strong>{{ member }}</strong> from {{ group }}?
{% endblocktrans %}
{% else %}
{% blocktrans with object=object %}
Are you sure you want to delete <strong>{{ object }}</strong>?
{% endblocktrans %}
{% endif %}
{% endif %} {% endif %}
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="{{ request.path }}" method="POST">
{% csrf_token %} {% csrf_token %}
<a class="btn btn-default">{% trans "Cancel" %}</a> <a class="btn btn-default">{% trans "Cancel" %}</a>
<input type="hidden" name="next" value="{{ request.GET.next }}"/> <input type="hidden" name="next" value="{{ request.GET.next }}"/>
......
{% extends "base.html" %}
{% load i18n %}
{% block title-site %}Dashboard | CIRCLE{% endblock %}
{% block content %}
{% blocktrans with group=object member=member %}
Do you really want to remove {{member}} from {{group}}?
{% endblocktrans %}
<form action="" method="POST">{% csrf_token %}
<input type="submit" value="{% trans "Remove" %}" />
</form>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{%blocktrans with instance=instance.name%}
Renewing <em>{{instance}}</em>
{%endblocktrans%}
</h3>
</div>
<div class="panel-body">
{%blocktrans with object=instance.name%}
Do you want to renew <strong>{{ object }}</strong>?
{%endblocktrans%}
{%blocktrans with suspend=time_of_suspend delete=time_of_delete|default:"n/a" %}
The instance will be suspended at <em>{{suspend}}</em>
and removed at <em>{{delete}}</em> if you renew it now.
{%endblocktrans%}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default"
href="{{instance.get_absolute_path}}">{% trans "Back" %}</a>
<button class="btn btn-danger">{% trans "Renew" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% load i18n %}
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{% trans "Are you sure you want to delete the following objects?" %}<br />
{% for o in objects %}
<strong>{{ o }}</strong>{% if not forloop.last %},{% endif %}
{% endfor %}
<div class="pull-right" style="margin-top: 40px;">
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<button id="confirmation-modal-button" type="button" class="btn btn-danger">{% trans "Delete" %}</button>
</div>
<div class="clearfix"></div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% if title %}
{{ title }}
{% else %}
Flush confirmation
{% endif %}
</h3>
</div>
<div class="panel-body">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to flush <strong>{{ object }}</strong>?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default">{% trans "Back" %}</a>
<input type="hidden" name="flush" value=""/>
<button class="btn btn-warning">{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% if title %}
{{ title }}
{% else %}
{% trans "Status changing confirmation" %}
{% endif %}
</h3>
</div>
<div class="panel-body">
{% if text %}
{{ text }}
{% else %}
{%blocktrans with object=object%}
Are you sure you want to change <strong>{{ object }}</strong> status?
{%endblocktrans%}
{% endif %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default">{% trans "Cancel" %}</a>
<button type="button" class="btn btn-default" data-dismiss="modal"></button>
<input type="hidden" name="change_status" value=""/>
<button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
...@@ -9,43 +9,33 @@ ...@@ -9,43 +9,33 @@
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<div class="pull-right" style="padding-top: 15px;"> <div class="pull-right" style="padding-top: 15px;">
<a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs group-details-rename-button"> <a title="{% trans "Rename" %}" class="btn btn-default btn-xs group-details-rename-button">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
</a> </a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"> <a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}">
<i class="fa fa-trash-o"></i> <i class="fa fa-trash-o"></i>
</a> </a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button">
<i class="fa fa-question"></i>
</a>
</div> </div>
<h1> <h1>
<div id="group-details-rename"> <form action="" method="POST" id="group-details-rename-form" class="js-hidden">
<form action="" method="POST" id="group-details-rename-form"> {% csrf_token %}
{% csrf_token %} <div class="input-group">
<input id="group-details-rename-name" class="form-control" name="new_name" type="text" value="{{ group.name }}"/> <input id="group-details-rename-name" class="form-control" name="new_name"
<button type="submit" id="group-details-rename-submit" class="btn">{% trans "Rename" %}</button> type="text" value="{{ group.name }}" required />
</form> <span class="input-group-btn">
</div> <button type="submit" id="group-details-rename-submit" class="btn">
{% trans "Rename" %}
</button>
</span>
</div>
</form>
<div id="group-details-h1-name"> <div id="group-details-h1-name">
{{ group.name }} <span class="no-js-hidden">{{ group.name }}</span>
{% if group.groupprofile.org_id %} {% if group.groupprofile.org_id %}
<small>{{group.groupprofile.org_id}}</small> <small>{{group.groupprofile.org_id}}</small>
{% endif %} {% endif %}
</div> </div>
</h1> </h1>
<div class="group-details-help js-hidden">
<ul style="list-style: none;">
<li>
<strong>{% trans "Rename" %}:</strong>
{% trans "Change the name of the group." %}
</li>
<li>
<strong>{% trans "Delete" %}:</strong>
{% trans "Delete group." %}
</li>
</ul>
</div>
</div><!-- .page-header --> </div><!-- .page-header -->
<div class="row"> <div class="row">
<div class="col-md-12" id="group-detail-pane"> <div class="col-md-12" id="group-detail-pane">
......
...@@ -16,7 +16,9 @@ ...@@ -16,7 +16,9 @@
<div class="panel-body"> <div class="panel-body">
<div id="table_container"> <div id="table_container">
<div id="rendered_table" class="panel-body"> <div id="rendered_table" class="panel-body">
{% render_table table %} <div class="table-responsive">
{% render_table table %}
</div>
</div> </div>
</div> </div>
</div><!-- .panel-body --> </div><!-- .panel-body -->
......
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
<button type="submit" class="group-list-rename-submit btn btn-sm">{% trans "Rename" %}</button> <button type="submit" class="group-list-rename-submit btn btn-sm">{% trans "Rename" %}</button>
</form> </form>
</div> </div>
<div id="group-list-column-name"> <div class="group-list-column-name">
<a class="real-link" href="{% url "dashboard.views.group-detail" pk=record.pk %}">{{ record.name }}</a> <a class="real-link" href="{% url "dashboard.views.group-detail" pk=record.pk %}">{{ record.name }}</a>
</div> </div>
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
</div> </div>
<h3 class="no-margin"><i class="fa fa-group"></i> {% trans "Groups" %}</h3> <h3 class="no-margin"><i class="fa fa-group"></i> {% trans "Groups" %}</h3>
</div> </div>
<div class="list-group" id="vm-list-view"> <div class="list-group" id="group-list-view">
<div id="dashboard-group-list"> <div id="dashboard-group-list">
{% for i in groups %} {% for i in groups %}
<a href="{% url "dashboard.views.group-detail" pk=i.pk %}" class="list-group-item real-link <a href="{% url "dashboard.views.group-detail" pk=i.pk %}" class="list-group-item real-link
...@@ -15,14 +15,14 @@ ...@@ -15,14 +15,14 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
<div href="#" class="list-group-item list-group-footer text-right"> <div class="list-group-item list-group-footer text-right">
<div class="row"> <div class="row">
<div class="col-xs-6"> <div class="col-xs-6">
<form action="{% url "dashboard.views.group-list" %}" method="GET" id="dashboard-group-search-form"> <form action="{% url "dashboard.views.group-list" %}" method="GET" id="dashboard-group-search-form">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" /> <input id="dashboard-group-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
</div> </div>
</form> </form>
......
...@@ -29,9 +29,43 @@ ...@@ -29,9 +29,43 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
<div class="list-group-item list-group-footer">
<div class="row">
<div class="col-xs-6">
<form action="{% url "dashboard.views.node-list" %}" method="GET"
id="dashboard-node-search-form">
<div class="input-group input-group-sm">
<input id="dashboard-node-search-input" type="text" class="form-control"
placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary" title="{% trans "Search" %}" data-container="body">
<i class="fa fa-search"></i>
</button>
</div>
</div>
</form>
</div>
<div class="col-xs-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.node-list" %}">
<i class="fa fa-chevron-circle-right"></i>
{% if more_nodes > 0 %}
{% blocktrans with count=more_nodes %}<strong>{{count}}</strong> more{% endblocktrans %}
{% else %}
{% trans "list" %}
{% endif %}
</a>
{% if request.user.is_superuser %}
<a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}">
<i class="fa fa-plus-circle"></i> {% trans "new" %}
</a>
{% endif %}
</div>
</div>
</div>
</div><!-- #node-list-view --> </div><!-- #node-list-view -->
<div class="panel-body" id="node-graph-view" style="display: none; min-height: 204px;"> <div class="panel-body" id="node-graph-view" style="display: none">
<p class="pull-right"> <p class="pull-right">
<input class="knob" data-fgColor="chartreuse" <input class="knob" data-fgColor="chartreuse"
data-thickness=".4" data-width="60" data-height="60" data-readOnly="true" data-thickness=".4" data-width="60" data-height="60" data-readOnly="true"
...@@ -46,45 +80,14 @@ ...@@ -46,45 +80,14 @@
</p> </p>
<ul class="list-inline" id="dashboard-node-taglist"> <ul class="list-inline" id="dashboard-node-taglist">
{% for i in nodes %} {% for i in nodes %}
<a href="{{ i.get_absolute_url }}" class="label {{i.get_status_label}}" > <li>
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}</a> <a href="{{ i.get_absolute_url }}" class="label {{i.get_status_label}}" >
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}</a>
</li>
{% endfor %} {% endfor %}
</ul> </ul>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
<div href="#" class="list-group-item list-group-footer">
<div class="row">
<div class="col-xs-6">
<form action="{% url "dashboard.views.node-list" %}" method="GET"
id="dashboard-node-search-form">
<div class="input-group input-group-sm">
<input id="dashboard-node-search-input" type="text" class="form-control"
placeholder="{% trans "Search..." %}" />
<div class="input-group-btn">
<button type="submit" class="btn btn-primary" title="{% trans "Search" %}" data-container="body">
<i class="fa fa-search"></i>
</button>
</div>
</div>
</form>
</div>
<div class="col-xs-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.node-list" %}">
<i class="fa fa-chevron-circle-right"></i>
{% if more_nodes > 0 %}
{% blocktrans with count=more_nodes %}<strong>{{count}}</strong> more{% endblocktrans %}
{% else %}
{% trans "list" %}
{% endif %}
</a>
{% if request.user.is_superuser %}
<a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}">
<i class="fa fa-plus-circle"></i> {% trans "new" %}
</a>
{% endif %}
</div>
</div>
</div>
</div> </div>
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %} <h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}
</h3> </h3>
</div> </div>
<div class="list-group" id="dashboard-template-list"> <div class="list-group" id="template-list-view">
<div id="dashboard-template-list"> <div id="dashboard-template-list">
{% for t in templates %} {% for t in templates %}
<a href="{% url "dashboard.views.template-detail" pk=t.pk %}" class="list-group-item <a href="{% url "dashboard.views.template-detail" pk=t.pk %}" class="list-group-item
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<i class="fa fa-{{ t.os_type }}"></i> {{ t.name }} <i class="fa fa-{{ t.os_type }}"></i> {{ t.name }}
</span> </span>
<small class="text-muted index-template-list-system">{{ t.system }}</small> <small class="text-muted index-template-list-system">{{ t.system }}</small>
<div class="pull-right vm-create" data-template="{{ t.pk }}"> <div href="{% url "dashboard.views.vm-create" %}?template={{ t.pk }}" class="pull-right vm-create">
<i data-container="body" title="{% trans "Start VM instance" %}" <i data-container="body" title="{% trans "Start VM instance" %}"
class="fa fa-play"></i> class="fa fa-play"></i>
</div> </div>
...@@ -32,15 +32,15 @@ ...@@ -32,15 +32,15 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div href="#" class="list-group-item list-group-footer text-right"> <div class="list-group-item list-group-footer">
<p> <div class="text-right">
<a href="{% url "dashboard.views.template-list" %}" class="btn btn-primary btn-xs"> <a href="{% url "dashboard.views.template-list" %}" class="btn btn-primary btn-xs">
<i class="fa fa-chevron-circle-right"></i> {% trans "show all" %} <i class="fa fa-chevron-circle-right"></i> {% trans "show all" %}
</a> </a>
<a href="{% url "dashboard.views.template-choose" %}" class="btn btn-success btn-xs template-choose"> <a href="{% url "dashboard.views.template-choose" %}" class="btn btn-success btn-xs template-choose">
<i class="fa fa-plus-circle"></i> {% trans "new" %} <i class="fa fa-plus-circle"></i> {% trans "new" %}
</a> </a>
</p> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -13,7 +13,14 @@ ...@@ -13,7 +13,14 @@
<span class="btn btn-default btn-xs infobtn" data-container="body" title="{% trans "List of your current virtual machines. Favourited ones are ahead of others." %}"><i class="fa fa-info-circle"></i></span> <span class="btn btn-default btn-xs infobtn" data-container="body" title="{% trans "List of your current virtual machines. Favourited ones are ahead of others." %}"><i class="fa fa-info-circle"></i></span>
</div> </div>
<h3 class="no-margin"> <h3 class="no-margin">
<i class="fa fa-desktop"></i> {% trans "Virtual machines" %} <span class="visible-xs">
<i class="fa fa-desktop"></i>
{% trans "VMs" %}
</span>
<span class="hidden-xs">
<i class="fa fa-desktop"></i>
{% trans "Virtual machines" %}
</span>
</h3> </h3>
</div> </div>
<div class="list-group" id="vm-list-view"> <div class="list-group" id="vm-list-view">
...@@ -25,7 +32,7 @@ ...@@ -25,7 +32,7 @@
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> <i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i>
{{ i.name }} {{ i.name }}
</span> </span>
<small class="text-muted"> <small class="text-muted index-vm-list-host">
{% if i.owner == request.user %}{{ i.short_hostname }} {% if i.owner == request.user %}{{ i.short_hostname }}
{% else %}{{i.owner.profile.get_display_name}}{% endif %} {% else %}{{i.owner.profile.get_display_name}}{% endif %}
</small> </small>
...@@ -44,7 +51,7 @@ ...@@ -44,7 +51,7 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div href="#" class="list-group-item list-group-footer"> <div class="list-group-item list-group-footer">
<div class="row"> <div class="row">
<div class="col-xs-6"> <div class="col-xs-6">
<form action="{% url "dashboard.views.vm-list" %}" method="GET" id="dashboard-vm-search-form"> <form action="{% url "dashboard.views.vm-list" %}" method="GET" id="dashboard-vm-search-form">
...@@ -52,7 +59,7 @@ ...@@ -52,7 +59,7 @@
<input id="dashboard-vm-search-input" type="text" class="form-control" name="s" <input id="dashboard-vm-search-input" type="text" class="form-control" name="s"
placeholder="{% trans "Search..." %}" /> placeholder="{% trans "Search..." %}" />
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-primary"><i class="fa fa-search"></i></button>
</div> </div>
</div> </div>
</form> </form>
...@@ -79,7 +86,7 @@ ...@@ -79,7 +86,7 @@
<p class="pull-right"> <p class="pull-right">
<input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-max="{{ request.user.profile.instance_limit }}" data-width="100" data-height="100" data-readOnly="true" value="{{ instances|length|add:more_instances }}"> <input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-max="{{ request.user.profile.instance_limit }}" data-width="100" data-height="100" data-readOnly="true" value="{{ instances|length|add:more_instances }}">
</p> </p>
<p><span class="bigbig">{% blocktrans with count=running_vm_num %}<big>{{ count }}</big> running{% endblocktrans %}</span> <span class="bigbig">{% blocktrans with count=running_vm_num %}<big>{{ count }}</big> running{% endblocktrans %}</span>
<ul class="list-inline" style="max-height: 95px; overflow: hidden;"> <ul class="list-inline" style="max-height: 95px; overflow: hidden;">
{% for vm in running_vms %} {% for vm in running_vms %}
<li style="display: inline-block; padding: 2px;"> <li style="display: inline-block; padding: 2px;">
...@@ -89,7 +96,6 @@ ...@@ -89,7 +96,6 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
</p>
<div class="clearfix"></div> <div class="clearfix"></div>
<div> <div>
......
<div class="modal fade" id="create-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
{% if box_title and ajax_title %}
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">{{ box_title }}</h4>
</div>
{% endif %}
<div class="modal-body">
{% include template %}
</div>
<!--<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>-->
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
...@@ -73,14 +73,15 @@ ...@@ -73,14 +73,15 @@
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "dashboard.views.vm-list" %}?s=node:{{ node.name }}" <a href="{% url "dashboard.views.vm-list" %}?s=node_exact:{{ node.name }}"
target="blank" class="text-center"> target="blank" class="text-center">
<i class="fa fa-desktop fa-2x"></i><br> <i class="fa fa-desktop fa-2x"></i><br>
{% trans "Virtual Machines" %} {% trans "Virtual Machines" %}
</a> </a>
</li> </li>
<li> <li>
<a href="#activity" data-toggle="pill" class="text-center"> <a href="#activity" data-toggle="pill" class="text-center"
data-activity-url="{% url "dashboard.views.node-activity-list" node.pk %}">
<i class="fa fa-clock-o fa-2x"></i><br> <i class="fa fa-clock-o fa-2x"></i><br>
{% trans "Activity" %} {% trans "Activity" %}
</a> </a>
......
{% load i18n %} {% load i18n %}
{% load hro %} {% load hro %}
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% for a in activities %} {% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}"> <div class="activity" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}"> <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-plus{% endif %}"></i> <i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-plus{% endif %}"></i>
</span> </span>
<strong title="{{ a.result.get_admin_text }}"> <strong title="{{ a.result.get_admin_text }}">
{{ a.readable_name.get_admin_text|capfirst }} {{ a.readable_name.get_admin_text|capfirst }}
</strong> </strong>
{{ a.started|date:"Y-m-d H:i" }}, {{ a.user }} {{ a.started|date:"Y-m-d H:i" }}{% if a.user %}, {{ a.user }}{% endif %}
{% if a.children.count > 0 %} {% if a.children.count > 0 %}
<div class="sub-timeline"> <div class="sub-timeline">
{% for s in a.children.all %} {% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" <div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}">
> <span title="{{ s.result.get_admin_text }}">
{{ s.readable_name|get_text:user }} {{ s.readable_name|get_text:user }}
&ndash; </span>
{% if s.finished %} &ndash;
{{ s.finished|time:"H:i:s" }} {% if s.finished %}
{% else %} {{ s.finished|time:"H:i:s" }}
<i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i> {% else %}
{% endif %} <i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i>
{% if s.has_failed %} {% endif %}
<div title="{{ s.result.get_admin_text }}" class="label label-danger">{% trans "failed" %}</div> {% if s.has_failed %}
{% endif %} <div class="label label-danger">{% trans "failed" %}</div>
</div> {% endif %}
{% endfor %} </div>
</div> {% endfor %}
{% endif %} </div>
{% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
<h3>{% trans "Activity" %}</h3> <h3>{% trans "Activity" %}</h3>
<div id="activity-timeline-wrapper"> <div id="activity-refresh">
{% include "dashboard/node-detail/_activity-timeline.html" %} {% include "dashboard/node-detail/_activity-timeline.html" %}
</div> </div>
...@@ -7,8 +7,9 @@ ...@@ -7,8 +7,9 @@
<dt>{% trans "RAM size" %}:</dt> <dd>{% widthratio node.info.ram_size 1048576 1 %} MiB</dd> <dt>{% trans "RAM size" %}:</dt> <dd>{% widthratio node.info.ram_size 1048576 1 %} MiB</dd>
<dt>{% trans "Architecture" %}:</dt><dd>{{ node.info.architecture }}</dd> <dt>{% trans "Architecture" %}:</dt><dd>{{ node.info.architecture }}</dd>
<dt>{% trans "Host IP" %}:</dt><dd>{{ node.host.ipv4 }}</dd> <dt>{% trans "Host IP" %}:</dt><dd>{{ node.host.ipv4 }}</dd>
<dt>{% trans "Enabled" %}:</dt><dd>{{ node.enabled }}</dd> <dt>{% trans "Enabled" %}:</dt><dd>{{ node.enabled|yesno }}</dd>
<dt>{% trans "Host online" %}:</dt><dd> {{ node.online }}</dd> <dt>{% trans "Host online" %}:</dt><dd> {{ node.online|yesno }}</dd>
<dt>{% trans "Minion online" %}:</dt><dd> {{ node.minion_online|yesno }}</dd>
<dt>{% trans "Priority" %}:</dt><dd>{{ node.priority }}</dd> <dt>{% trans "Priority" %}:</dt><dd>{{ node.priority }}</dd>
<dt>{% trans "Driver Version:" %}</dt> <dt>{% trans "Driver Version:" %}</dt>
<dd> <dd>
......
...@@ -15,7 +15,9 @@ ...@@ -15,7 +15,9 @@
</div> </div>
<div id="table_container"> <div id="table_container">
<div id="rendered_table" class="panel-body"> <div id="rendered_table" class="panel-body">
{% render_table table %} <div class="table-responsive">
{% render_table table %}
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -6,6 +6,18 @@ ...@@ -6,6 +6,18 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div id="notifications-upper-pagination" class="pull-right">
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}">
<i class="fa fa-chevron-left"></i></a>
</a>
{% endif %}
{{ page.number }} / {{ paginator.num_pages }}
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}"><i class="fa fa-chevron-right"></i></a>
{% endif %}
</div>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Notifications" %}</h3> <h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Notifications" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
...@@ -13,6 +25,29 @@ ...@@ -13,6 +25,29 @@
{% include "dashboard/_notifications-timeline.html" %} {% include "dashboard/_notifications-timeline.html" %}
</ul> </ul>
</div> </div>
<div class="panel-body text-center" id="notifications-bottom-pagination">
{% if page.has_previous %}
<a href="?page=1">
<i class="fa fa-angle-double-left"></i>
</a>
<a href="{% if page.has_previous %}?page={{ page.previous_page_number}}{% else %}#{% endif %}">
<i class="fa fa-angle-left"></i>
</a>
{% endif %}
<div class="page-numbers">
{{ page.number }} / {{ paginator.num_pages }}
</div>
{% if page.has_next %}
<a href="{% if page.has_next %}?page={{ page.next_page_number}}{% else %}#{% endif %}">
<i class="fa fa-angle-right"></i>
</a>
<a href="?page={{ paginator.num_pages }}">
<i class="fa fa-angle-double-right"></i>
</a>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -19,8 +19,8 @@ Do you want to perform the following operation on ...@@ -19,8 +19,8 @@ Do you want to perform the following operation on
<div class="pull-right"> <div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}" <a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a> data-dismiss="modal">{% trans "Cancel" %}</a>
<button class="btn btn-{{ opview.effect }}" type="submit" id="op-form-send"> <button class="btn btn-{{ opview.effect }} btn-op-form-send" type="submit" id="op-form-send">
{% if opview.icon %}<i class="fa fa-{{opview.icon}}"></i> {% endif %}{{ op|capfirst }} {% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ op.name|capfirst }}
</button> </button>
</div> </div>
</form> </form>
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
class="list-group-item class="list-group-item
{% if forloop.last and files.toplist|length < 5 %}list-group-item-last{% endif %}"> {% if forloop.last and files.toplist|length < 5 %}list-group-item-last{% endif %}">
<i class="fa fa-{{ t.icon }} dashboard-toplist-icon"></i> <i class="fa fa-{{ t.icon }} dashboard-toplist-icon"></i>
<div class="store-list-item-name"> <div class="store-list-item-name">
{{ t.NAME }} {{ t.NAME }}
</div> </div>
<div style="clear: both;"></div> <div style="clear: both;"></div>
...@@ -53,20 +53,22 @@ ...@@ -53,20 +53,22 @@
{% trans "Your toplist is empty, upload something." %} {% trans "Your toplist is empty, upload something." %}
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="list-group-item text-right no-hover"> <div class="list-group-item list-group-footer">
<form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}"> <div class="text-right">
{% csrf_token %} <form class="pull-left" method="POST" action="{% url "dashboard.views.store-refresh-toplist" %}">
<button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}"/> {% csrf_token %}
<i class="fa fa-refresh"></i> <button class="btn btn-success btn-xs" type="submit" title="{% trans "Refresh" %}"/>
</button> <i class="fa fa-refresh"></i>
</form> </button>
<a href="{% url "dashboard.views.store-list" %}" class="btn btn-primary btn-xs"> </form>
<i class="fa fa-chevron-circle-right"></i> {% trans "show my files" %} <a href="{% url "dashboard.views.store-list" %}" class="btn btn-primary btn-xs">
</a> <i class="fa fa-chevron-circle-right"></i> {% trans "show my files" %}
<a href="{% url "dashboard.views.store-upload" %}" class="btn btn-success btn-xs"> </a>
<i class="fa fa-cloud-upload"></i> {% trans "upload" %} <a href="{% url "dashboard.views.store-upload" %}" class="btn btn-success btn-xs">
</a> <i class="fa fa-cloud-upload"></i> {% trans "upload" %}
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a href="{% url "dashboard.views.template-delete" pk=object.pk %}" <a href="{% url "dashboard.views.template-delete" pk=object.pk %}"
class="btn btn-xs btn-danger pull-right"> class="btn btn-xs btn-danger pull-right template-delete">
{% trans "Delete" %} {% trans "Delete" %}
</a> </a>
<h4 class="no-margin"><i class="fa fa-times"></i> {% trans "Delete template" %}</h4> <h4 class="no-margin"><i class="fa fa-times"></i> {% trans "Delete template" %}</h4>
......
...@@ -34,7 +34,9 @@ ...@@ -34,7 +34,9 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% render_table table %} <div class="table-responsive">
{% render_table table %}
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -207,19 +207,25 @@ ...@@ -207,19 +207,25 @@
{% trans "Network" %}</a> {% trans "Network" %}</a>
</li> </li>
<li> <li>
<a href="#activity" data-toggle="pill" data-target="#_activity" class="text-center"> <a href="#activity" data-toggle="pill" data-target="#_activity" class="text-center"
data-activity-url="{% url "dashboard.views.vm-activity-list" instance.pk %}">
<i class="fa fa-clock-o fa-2x"></i><br> <i class="fa fa-clock-o fa-2x"></i><br>
{% trans "Activity" %}</a> {% trans "Activity" %}</a>
</li> </li>
</ul> </ul>
<div class="tab-content panel-body"> <div class="tab-content panel-body">
<div class="tab-pane active" id="_home">{% include "dashboard/vm-detail/home.html" %}</div> <div class="not-tab-pane active" id="_home">{% include "dashboard/vm-detail/home.html" %}</div>
<div class="tab-pane" id="_resources">{% include "dashboard/vm-detail/resources.html" %}</div> <hr class="js-hidden"/>
<div class="not-tab-pane" id="_resources">{% include "dashboard/vm-detail/resources.html" %}</div>
<div class="tab-pane" id="_console">{% include "dashboard/vm-detail/console.html" %}</div> <div class="tab-pane" id="_console">{% include "dashboard/vm-detail/console.html" %}</div>
<div class="tab-pane" id="_access">{% include "dashboard/vm-detail/access.html" %} </div> <hr class="js-hidden"/>
<div class="tab-pane" id="_network">{% include "dashboard/vm-detail/network.html" %}</div> <div class="not-tab-pane" id="_access">{% include "dashboard/vm-detail/access.html" %} </div>
<div class="tab-pane" id="_activity">{% include "dashboard/vm-detail/activity.html" %}</div> <hr class="js-hidden"/>
<div class="not-tab-pane" id="_network">{% include "dashboard/vm-detail/network.html" %}</div>
<hr class="js-hidden"/>
<div class="not-tab-pane" id="_activity">{% include "dashboard/vm-detail/activity.html" %}</div>
<hr class="js-hidden"/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
{% if op.is_disk_operation %} {% if op.is_disk_operation %}
<a href="{{op.get_url}}" class="btn btn-success btn-xs <a href="{{op.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.op}}"> operation operation-{{op.op}}">
<i class="fa fa-{{op.icon}}"></i> <i class="fa fa-{{op.icon}} fa-fw-12"></i>
{{op.name}} </a> {{op.name}} </a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
...@@ -13,7 +13,10 @@ ...@@ -13,7 +13,10 @@
<select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select> <select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select>
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm <button type="submit" class="btn btn-success btn-sm
{% if not is_operator %}disabled{% endif %}">{% trans "Add" %}</button> {% if not is_operator %}disabled{% endif %}">
<span class="hidden-xs">{% trans "Add" %}</span>
<span class="visible-xs"><i class="fa fa-plus-circle"></i></span>
</button>
</div> </div>
</div> </div>
</form> </form>
......
...@@ -21,13 +21,14 @@ ...@@ -21,13 +21,14 @@
<a href="{{ i.host.get_absolute_url }}" <a href="{{ i.host.get_absolute_url }}"
class="btn btn-default btn-xs">{% trans "edit" %}</a> class="btn btn-default btn-xs">{% trans "edit" %}</a>
{% endif %} {% endif %}
{% if is_owner %} {% with op=op.remove_interface %}{% if op %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" <span class="operation-wrapper">
class="btn btn-danger btn-xs interface-remove" <a href="{{op.get_url}}?interface={{ i.pk }}"
data-interface-pk="{{ i.pk }}"> class="btn btn-{{op.effect}} btn-xs operation interface-remove"
{% trans "remove" %} {% if op.disabled %}disabled{% endif %}>{% trans "remove" %}
</a> </a>
{% endif %} </span>
{% endif %}{% endwith %}
</h3> </h3>
{% if i.host %} {% if i.host %}
<div class="row"> <div class="row">
...@@ -78,7 +79,13 @@ ...@@ -78,7 +79,13 @@
{{ l.private }}/{{ l.proto }} {{ l.private }}/{{ l.proto }}
</td> </td>
<td> <td>
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a> <span class="operation-wrapper">
<a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}"
class="btn btn-link btn-xs operation"
title="{% trans "Remove" %}">
<i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i>
</a>
</span>
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
</div> </div>
</div> </div>
</h3> </h3>
<div class="clearfix"></div>
{% if not instance.disks.all %} {% if not instance.disks.all %}
{% trans "No disks are added." %} {% trans "No disks are added." %}
......
...@@ -50,6 +50,7 @@ ...@@ -50,6 +50,7 @@
</div><!-- .row --> </div><!-- .row -->
</div><!-- .panel-body --> </div><!-- .panel-body -->
<div class="panel-body"> <div class="panel-body">
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover vm-list-table" <table class="table table-bordered table-striped table-hover vm-list-table"
id="vm-list-table"> id="vm-list-table">
<thead><tr> <thead><tr>
...@@ -140,6 +141,7 @@ ...@@ -140,6 +141,7 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div><!-- .table-responsive -->
</div> </div>
</div> </div>
</div> </div>
......
...@@ -389,6 +389,7 @@ class RenewViewTest(unittest.TestCase): ...@@ -389,6 +389,7 @@ class RenewViewTest(unittest.TestCase):
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.name = 'foo' inst.name = 'foo'
inst.lease = MagicMock(pk=99)
inst.renew = Instance._ops['renew'](inst) inst.renew = Instance._ops['renew'](inst)
inst.has_level.return_value = True inst.has_level.return_value = True
go.return_value = inst go.return_value = inst
...@@ -403,6 +404,7 @@ class RenewViewTest(unittest.TestCase): ...@@ -403,6 +404,7 @@ class RenewViewTest(unittest.TestCase):
patch('dashboard.views.util.messages') as msg: patch('dashboard.views.util.messages') as msg:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.lease = MagicMock(pk=99)
inst.renew = Instance._ops['renew'](inst) inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock() inst.renew.async = MagicMock()
inst.has_level.return_value = True inst.has_level.return_value = True
...@@ -421,6 +423,7 @@ class RenewViewTest(unittest.TestCase): ...@@ -421,6 +423,7 @@ class RenewViewTest(unittest.TestCase):
patch('dashboard.views.util.messages') as msg: patch('dashboard.views.util.messages') as msg:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.lease = MagicMock(pk=99)
inst.renew = Instance._ops['renew'](inst) inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock() inst.renew.async = MagicMock()
inst.has_level.return_value = True inst.has_level.return_value = True
...@@ -463,6 +466,7 @@ class RenewViewTest(unittest.TestCase): ...@@ -463,6 +466,7 @@ class RenewViewTest(unittest.TestCase):
with patch.object(view, 'get_object') as go: with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance, pk=11) inst = MagicMock(spec=Instance, pk=11)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.lease = MagicMock(pk=99)
inst.renew = Instance._ops['renew'](inst) inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock() inst.renew.async = MagicMock()
inst.has_level.return_value = False inst.has_level.return_value = False
......
...@@ -27,7 +27,7 @@ from django.contrib.auth import authenticate ...@@ -27,7 +27,7 @@ from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import (WakeUpOperation, AddInterfaceOperation, from vm.operations import (WakeUpOperation, AddInterfaceOperation,
AddPortOperation) AddPortOperation, RemoveInterfaceOperation)
from ..models import Profile from ..models import Profile
from firewall.models import Vlan, Host, VlanGroup from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch from mock import Mock, patch
...@@ -169,26 +169,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -169,26 +169,12 @@ class VmDetailTest(LoginMixin, TestCase):
inst.save() inst.save()
iface_count = inst.interface_set.count() iface_count = inst.interface_set.count()
c.post("/dashboard/interface/1/delete/") with patch.object(RemoveInterfaceOperation, 'async') as mock_method:
self.assertEqual(inst.interface_set.count(), iface_count - 1) mock_method.side_effect = inst.remove_interface
response = c.post("/dashboard/vm/1/op/remove_interface/",
def test_permitted_network_delete_w_ajax(self): {'interface': 1})
c = Client() self.assertEqual(response.status_code, 302)
self.login(c, "user1") assert mock_method.called
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
vlan = Vlan.objects.get(pk=1)
inst.add_interface(vlan=vlan, user=self.us)
inst.status = 'RUNNING'
inst.save()
iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/",
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
removed_network = json.loads(response.content)['removed_network']
self.assertEqual(removed_network['vlan'], vlan.name)
self.assertEqual(removed_network['vlan_pk'], vlan.pk)
self.assertEqual(removed_network['managed'], vlan.managed)
self.assertEqual(inst.interface_set.count(), iface_count - 1) self.assertEqual(inst.interface_set.count(), iface_count - 1)
def test_unpermitted_network_delete(self): def test_unpermitted_network_delete(self):
...@@ -199,7 +185,10 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -199,7 +185,10 @@ class VmDetailTest(LoginMixin, TestCase):
inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us) inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us)
iface_count = inst.interface_set.count() iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/") with patch.object(RemoveInterfaceOperation, 'async') as mock_method:
mock_method.side_effect = inst.remove_interface
response = c.post("/dashboard/vm/1/op/remove_interface/",
{'interface': 1})
self.assertEqual(iface_count, inst.interface_set.count()) self.assertEqual(iface_count, inst.interface_set.count())
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
...@@ -766,42 +755,6 @@ class NodeDetailTest(LoginMixin, TestCase): ...@@ -766,42 +755,6 @@ class NodeDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(len(Node.objects.get(pk=1).traits.all()), trait_count) self.assertEqual(len(Node.objects.get(pk=1).traits.all()), trait_count)
def test_anon_change_node_status(self):
c = Client()
node = Node.objects.get(pk=1)
node_enabled = node.enabled
response = c.post("/dashboard/node/1/", {'change_status': ''})
self.assertEqual(response.status_code, 302)
self.assertEqual(node_enabled, Node.objects.get(pk=1).enabled)
def test_unpermitted_change_node_status(self):
c = Client()
self.login(c, "user2")
node = Node.objects.get(pk=1)
node_enabled = node.enabled
response = c.post("/dashboard/node/status/1/", {'change_status': ''})
self.assertEqual(response.status_code, 302)
self.assertEqual(node_enabled, Node.objects.get(pk=1).enabled)
def test_permitted_change_node_status(self):
c = Client()
self.login(c, "superuser")
node = Node.objects.get(pk=1)
node_enabled = node.enabled
response = c.post("/dashboard/node/status/1/", {'change_status': ''})
self.assertEqual(response.status_code, 302)
self.assertEqual(node_enabled, not Node.objects.get(pk=1).enabled)
def test_permitted_change_node_status_w_ajax(self):
c = Client()
self.login(c, "superuser")
node = Node.objects.get(pk=1)
node_enabled = node.enabled
response = c.post("/dashboard/node/status/1/", {'change_status': ''},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertEqual(node_enabled, not Node.objects.get(pk=1).enabled)
class GroupCreateTest(LoginMixin, TestCase): class GroupCreateTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json'] fixtures = ['test-vm-fixture.json', 'node.json']
...@@ -949,21 +902,26 @@ class GroupDeleteTest(LoginMixin, TestCase): ...@@ -949,21 +902,26 @@ class GroupDeleteTest(LoginMixin, TestCase):
def test_permitted_group_page(self): def test_permitted_group_page(self):
c = Client() c = Client()
self.login(c, 'user0') self.login(c, 'user0')
response = c.get('/dashboard/group/delete/' + str(self.g1.pk) + '/') with patch('dashboard.views.util.messages') as msg:
response = c.get('/dashboard/group/delete/%d/' % self.g1.pk)
assert not msg.error.called and not msg.warning.called
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_unpermitted_group_page(self): def test_unpermitted_group_page(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
response = c.get('/dashboard/group/delete/' + str(self.g1.pk) + '/') with patch('dashboard.views.util.messages') as msg:
self.assertEqual(response.status_code, 403) response = c.get('/dashboard/group/delete/%d/' % self.g1.pk)
assert msg.error.called or msg.warning.called
self.assertEqual(response.status_code, 302)
def test_anon_group_delete(self): def test_anon_group_delete(self):
c = Client() c = Client()
groupnum = Group.objects.count() response = c.get('/dashboard/group/delete/%d/' % self.g1.pk)
response = c.post('/dashboard/group/delete/' + str(self.g1.pk) + '/') self.assertRedirects(
response, '/accounts/login/?next=/dashboard/group/delete/5/',
status_code=302)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(Group.objects.count(), groupnum)
def test_unpermitted_group_delete(self): def test_unpermitted_group_delete(self):
c = Client() c = Client()
...@@ -1484,38 +1442,6 @@ class TransferOwnershipViewTest(LoginMixin, TestCase): ...@@ -1484,38 +1442,6 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'}) response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
self.assertEqual(self.u2.notification_set.count(), c2 + 1) self.assertEqual(self.u2.notification_set.count(), c2 + 1)
def test_transfer(self):
self.skipTest("How did this ever pass?")
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
def test_transfer_token_used_by_others(self):
self.skipTest("How did this ever pass?")
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
response = c.post(url) # token is for user2
assert response.status_code == 403
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u1.pk)
def test_transfer_by_superuser(self):
self.skipTest("How did this ever pass?")
c = Client()
self.login(c, 'superuser')
response = c.post('/dashboard/vm/1/tx/', {'name': 'user2'})
url = response.context['token']
c = Client()
self.login(c, 'user2')
response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
class IndexViewTest(LoginMixin, TestCase): class IndexViewTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json'] fixtures = ['test-vm-fixture.json', 'node.json']
......
...@@ -25,12 +25,12 @@ from .views import ( ...@@ -25,12 +25,12 @@ from .views import (
GroupDetailView, GroupList, IndexView, GroupDetailView, GroupList, IndexView,
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail, InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeStatus, NodeDetailView, NodeList,
NotificationView, TemplateAclUpdateView, TemplateCreate, NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TemplateDelete, TemplateDetail, TemplateList,
vm_activity, VmCreate, VmDetailView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmList, VmDetailVncTokenView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView, DiskRemoveView, get_disk_download_status,
GroupRemoveUserView, GroupRemoveUserView,
GroupRemoveFutureUserView, GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate, GroupCreate, GroupProfileUpdate,
...@@ -51,6 +51,7 @@ from .views import ( ...@@ -51,6 +51,7 @@ from .views import (
TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView, TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView,
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView, TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
OpenSearchDescriptionView, OpenSearchDescriptionView,
NodeActivityView,
) )
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
...@@ -94,7 +95,8 @@ urlpatterns = patterns( ...@@ -94,7 +95,8 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'), url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(), url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'), name='dashboard.views.vm-create'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity), url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity,
name='dashboard.views.vm-activity-list'),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(), url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'), name='dashboard.views.vm-activity'),
url(r'^vm/(?P<pk>\d+)/screenshot/$', get_vm_screenshot, url(r'^vm/(?P<pk>\d+)/screenshot/$', get_vm_screenshot,
...@@ -119,8 +121,8 @@ urlpatterns = patterns( ...@@ -119,8 +121,8 @@ urlpatterns = patterns(
name='dashboard.views.template-transfer-ownership-confirm'), name='dashboard.views.template-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(), url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(), url(r'^node/(?P<pk>\d+)/activity/$', NodeActivityView.as_view(),
name="dashboard.views.status-node"), name='dashboard.views.node-activity-list'),
url(r'^node/create/$', NodeCreate.as_view(), url(r'^node/create/$', NodeCreate.as_view(),
name='dashboard.views.node-create'), name='dashboard.views.node-create'),
...@@ -156,9 +158,6 @@ urlpatterns = patterns( ...@@ -156,9 +158,6 @@ urlpatterns = patterns(
url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status, url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status,
name="dashboard.views.disk-status"), name="dashboard.views.disk-status"),
url(r'^interface/(?P<pk>\d+)/delete/$', InterfaceDeleteView.as_view(),
name="dashboard.views.interface-delete"),
url(r'^profile/$', MyPreferencesView.as_view(), url(r'^profile/$', MyPreferencesView.as_view(),
name="dashboard.views.profile-preferences"), name="dashboard.views.profile-preferences"),
url(r'^subscribe/(?P<token>.*)/$', UnsubscribeFormView.as_view(), url(r'^subscribe/(?P<token>.*)/$', UnsubscribeFormView.as_view(),
......
...@@ -62,7 +62,7 @@ class GraphViewBase(LoginRequiredMixin, View): ...@@ -62,7 +62,7 @@ class GraphViewBase(LoginRequiredMixin, View):
metric = self.create_class(metric)(instance) metric = self.create_class(metric)(instance)
return HttpResponse(metric.get_graph(graphite_url, time), return HttpResponse(metric.get_graph(graphite_url, time),
mimetype="image/png") content_type="image/png")
def get_object(self, request, pk): def get_object(self, request, pk):
instance = self.model.objects.get(id=pk) instance = self.model.objects.get(id=pk)
......
...@@ -29,7 +29,7 @@ from django.core.urlresolvers import reverse, reverse_lazy ...@@ -29,7 +29,7 @@ from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404
from django.shortcuts import redirect from django.shortcuts import redirect
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import UpdateView, DeleteView, TemplateView from django.views.generic import UpdateView, TemplateView
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
...@@ -41,7 +41,8 @@ from ..forms import ( ...@@ -41,7 +41,8 @@ from ..forms import (
from ..models import FutureMember, GroupProfile from ..models import FutureMember, GroupProfile
from vm.models import Instance, InstanceTemplate from vm.models import Instance, InstanceTemplate
from ..tables import GroupListTable from ..tables import GroupListTable
from .util import CheckedDetailView, AclUpdateView, search_user, saml_available from .util import (CheckedDetailView, AclUpdateView, search_user,
saml_available, DeleteViewBase)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -224,15 +225,18 @@ class GroupList(LoginRequiredMixin, SingleTableView): ...@@ -224,15 +225,18 @@ class GroupList(LoginRequiredMixin, SingleTableView):
return groups return groups
class GroupRemoveUserView(CheckedDetailView, DeleteView): class GroupRemoveUserView(DeleteViewBase):
model = Group model = Group
slug_field = 'pk' slug_field = 'pk'
slug_url_kwarg = 'group_pk' slug_url_kwarg = 'group_pk'
read_level = 'operator' level = 'operator'
member_key = 'member_pk' member_key = 'member_pk'
success_message = _("Member successfully removed from group.")
def get_has_level(self): def check_auth(self):
return self.object.profile.has_level if not self.get_object().profile.has_level(
self.request.user, self.level):
raise PermissionDenied()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(GroupRemoveUserView, self).get_context_data(**kwargs) context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
...@@ -243,50 +247,24 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView): ...@@ -243,50 +247,24 @@ class GroupRemoveUserView(CheckedDetailView, DeleteView):
return context return context
def get_success_url(self): def get_success_url(self):
next = self.request.POST.get('next') return reverse_lazy("dashboard.views.group-detail",
if next: kwargs={'pk': self.get_object().pk})
return next
else:
return reverse_lazy("dashboard.views.group-detail",
kwargs={'pk': self.get_object().pk})
def get(self, request, member_pk, *args, **kwargs): def get(self, request, member_pk, *args, **kwargs):
self.member_pk = member_pk self.member_pk = member_pk
return super(GroupRemoveUserView, self).get(request, *args, **kwargs) return super(GroupRemoveUserView, self).get(request, *args, **kwargs)
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-remove.html']
else:
return ['dashboard/confirm/base-remove.html']
def remove_member(self, pk): def remove_member(self, pk):
container = self.get_object() container = self.get_object()
container.user_set.remove(User.objects.get(pk=pk)) container.user_set.remove(User.objects.get(pk=pk))
def get_success_message(self): def delete_obj(self, request, *args, **kwargs):
return _("Member successfully removed from group.")
def delete(self, request, *args, **kwargs):
object = self.get_object()
if not object.profile.has_level(request.user, 'operator'):
raise PermissionDenied()
self.remove_member(kwargs[self.member_key]) self.remove_member(kwargs[self.member_key])
success_url = self.get_success_url()
success_message = self.get_success_message()
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return redirect(success_url)
class GroupRemoveFutureUserView(GroupRemoveUserView): class GroupRemoveFutureUserView(GroupRemoveUserView):
member_key = 'member_org_id' member_key = 'member_org_id'
success_message = _("Future user successfully removed from group.")
def get(self, request, member_org_id, *args, **kwargs): def get(self, request, member_org_id, *args, **kwargs):
self.member_org_id = member_org_id self.member_org_id = member_org_id
...@@ -305,53 +283,17 @@ class GroupRemoveFutureUserView(GroupRemoveUserView): ...@@ -305,53 +283,17 @@ class GroupRemoveFutureUserView(GroupRemoveUserView):
FutureMember.objects.filter(org_id=org_id, FutureMember.objects.filter(org_id=org_id,
group=self.get_object()).delete() group=self.get_object()).delete()
def get_success_message(self):
return _("Future user successfully removed from group.")
class GroupDelete(DeleteViewBase):
class GroupDelete(CheckedDetailView, DeleteView):
"""This stuff deletes the group.
"""
model = Group model = Group
template_name = "dashboard/confirm/base-delete.html" success_message = _("Group successfully deleted.")
read_level = 'operator'
def get_has_level(self):
return self.object.profile.has_level
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
# github.com/django/django/blob/master/django/views/generic/edit.py#L245 def check_auth(self):
def delete(self, request, *args, **kwargs): if not self.get_object().profile.has_level(self.request.user, 'owner'):
object = self.get_object()
if not object.profile.has_level(request.user, 'owner'):
raise PermissionDenied() raise PermissionDenied()
object.delete()
success_url = self.get_success_url()
success_message = _("Group successfully deleted.")
if request.is_ajax():
if request.POST.get('redirect').lower() == "true":
messages.success(request, success_message)
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return redirect(success_url)
def get_success_url(self): def get_success_url(self):
next = self.request.POST.get('next') return reverse_lazy('dashboard.views.group-list')
if next:
return next
else:
return reverse_lazy('dashboard.index')
class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView): class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
...@@ -360,7 +302,7 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView): ...@@ -360,7 +302,7 @@ class GroupCreate(GroupCodeMixin, LoginRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/modal-wrapper.html'] return ['dashboard/_modal.html']
else: else:
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
......
...@@ -27,8 +27,10 @@ from django.db.models import Count ...@@ -27,8 +27,10 @@ from django.db.models import Count
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import RequestContext
from django.template.loader import render_to_string
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.generic import DetailView, TemplateView, DeleteView from django.views.generic import DetailView, TemplateView, View
from braces.views import LoginRequiredMixin, SuperuserRequiredMixin from braces.views import LoginRequiredMixin, SuperuserRequiredMixin
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
...@@ -38,7 +40,7 @@ from vm.models import Node, NodeActivity, Trait ...@@ -38,7 +40,7 @@ from vm.models import Node, NodeActivity, Trait
from ..forms import TraitForm, HostForm, NodeForm from ..forms import TraitForm, HostForm, NodeForm
from ..tables import NodeListTable from ..tables import NodeListTable
from .util import AjaxOperationMixin, OperationView, GraphMixin from .util import AjaxOperationMixin, OperationView, GraphMixin, DeleteViewBase
def get_operations(instance, user): def get_operations(instance, user):
...@@ -59,6 +61,8 @@ class NodeOperationView(AjaxOperationMixin, OperationView): ...@@ -59,6 +61,8 @@ class NodeOperationView(AjaxOperationMixin, OperationView):
model = Node model = Node
context_object_name = 'node' # much simpler to mock object context_object_name = 'node' # much simpler to mock object
with_reload = True
wait_for_result = 1
node_ops = OrderedDict([ node_ops = OrderedDict([
...@@ -68,6 +72,8 @@ node_ops = OrderedDict([ ...@@ -68,6 +72,8 @@ node_ops = OrderedDict([
op='passivate', icon='play-circle-o', effect='info')), op='passivate', icon='play-circle-o', effect='info')),
('disable', NodeOperationView.factory( ('disable', NodeOperationView.factory(
op='disable', icon='times-circle-o', effect='danger')), op='disable', icon='times-circle-o', effect='danger')),
('update_node', NodeOperationView.factory(
op='update_node', icon='refresh', effect='warning')),
('reset', NodeOperationView.factory( ('reset', NodeOperationView.factory(
op='reset', icon='stethoscope', effect='danger')), op='reset', icon='stethoscope', effect='danger')),
('flush', NodeOperationView.factory( ('flush', NodeOperationView.factory(
...@@ -137,8 +143,13 @@ class NodeDetailView(LoginRequiredMixin, ...@@ -137,8 +143,13 @@ class NodeDetailView(LoginRequiredMixin,
def __remove_trait(self, request): def __remove_trait(self, request):
try: try:
to_remove = request.POST.get('to_remove') to_remove = request.POST.get('to_remove')
self.object = self.get_object() trait = Trait.objects.get(pk=to_remove)
self.object.traits.remove(to_remove) node = self.get_object()
node.traits.remove(to_remove)
if not trait.in_use:
trait.delete()
message = u"Success" message = u"Success"
except: # note this won't really happen except: # note this won't really happen
message = u"Not success" message = u"Not success"
...@@ -149,7 +160,7 @@ class NodeDetailView(LoginRequiredMixin, ...@@ -149,7 +160,7 @@ class NodeDetailView(LoginRequiredMixin,
content_type="application/json" content_type="application/json"
) )
else: else:
return redirect(self.object.get_absolute_url()) return redirect(node.get_absolute_url())
class NodeList(LoginRequiredMixin, GraphMixin, SingleTableView): class NodeList(LoginRequiredMixin, GraphMixin, SingleTableView):
...@@ -191,7 +202,7 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): ...@@ -191,7 +202,7 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/modal-wrapper.html'] return ['dashboard/_modal.html']
else: else:
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
...@@ -203,7 +214,7 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): ...@@ -203,7 +214,7 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
context = self.get_context_data(**kwargs) context = self.get_context_data(**kwargs)
context.update({ context.update({
'template': 'dashboard/node-create.html', 'template': 'dashboard/node-create.html',
'box_title': 'Create a Node', 'box_title': _('Create a node'),
'hostform': hostform, 'hostform': hostform,
'formset': formset, 'formset': formset,
...@@ -236,44 +247,16 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView): ...@@ -236,44 +247,16 @@ class NodeCreate(LoginRequiredMixin, SuperuserRequiredMixin, TemplateView):
return redirect(path) return redirect(path)
class NodeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): class NodeDelete(SuperuserRequiredMixin, DeleteViewBase):
"""This stuff deletes the node.
"""
model = Node model = Node
template_name = "dashboard/confirm/base-delete.html" success_message = _("Node successfully deleted.")
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
# github.com/django/django/blob/master/django/views/generic/edit.py#L245 def check_auth(self):
def delete(self, request, *args, **kwargs): # SuperuserRequiredMixin
object = self.get_object() pass
object.delete()
success_url = self.get_success_url()
success_message = _("Node successfully deleted.")
if request.is_ajax():
if request.POST.get('redirect').lower() == "true":
messages.success(request, success_message)
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return redirect(success_url)
def get_success_url(self): def get_success_url(self):
next = self.request.POST.get('next') return reverse_lazy('dashboard.views.node-list')
if next:
return next
else:
return reverse_lazy('dashboard.index')
class NodeAddTraitView(SuperuserRequiredMixin, DetailView): class NodeAddTraitView(SuperuserRequiredMixin, DetailView):
...@@ -309,55 +292,20 @@ class NodeAddTraitView(SuperuserRequiredMixin, DetailView): ...@@ -309,55 +292,20 @@ class NodeAddTraitView(SuperuserRequiredMixin, DetailView):
return self.get(self, request, pk, *args, **kwargs) return self.get(self, request, pk, *args, **kwargs)
class NodeStatus(LoginRequiredMixin, SuperuserRequiredMixin, DetailView): class NodeActivityView(LoginRequiredMixin, SuperuserRequiredMixin, View):
template_name = "dashboard/confirm/node-status.html" def get(self, request, pk):
model = Node node = Node.objects.get(pk=pk)
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-node-status.html']
else:
return ['dashboard/confirm/node-status.html']
def get_success_url(self):
next = self.request.GET.get('next')
if next:
return next
else:
return reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.object.pk})
def get_context_data(self, **kwargs):
context = super(NodeStatus, self).get_context_data(**kwargs)
if self.object.enabled:
context['status'] = "disable"
else:
context['status'] = "enable"
return context
def post(self, request, *args, **kwargs): activities = NodeActivity.objects.filter(
if request.POST.get('change_status') is not None: node=node, parent=None).order_by('-started').select_related()
return self.__set_status(request)
return redirect(reverse_lazy("dashboard.views.node-detail",
kwargs={'pk': self.get_object().pk}))
def __set_status(self, request): response = {
self.object = self.get_object() 'activities': render_to_string(
if not self.object.enabled: "dashboard/node-detail/_activity-timeline.html",
self.object.enable(user=request.user) RequestContext(request, {'activities': activities}))
else: }
self.object.disable(user=request.user)
success_message = _("Node successfully changed status.")
if request.is_ajax(): return HttpResponse(
response = { json.dumps(response),
'message': success_message, content_type="application/json"
'node_pk': self.object.pk )
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.get_success_url())
...@@ -28,7 +28,7 @@ from django.http import HttpResponse, HttpResponseRedirect ...@@ -28,7 +28,7 @@ from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _, ugettext_noop from django.utils.translation import ugettext as _, ugettext_noop
from django.views.generic import ( from django.views.generic import (
TemplateView, CreateView, DeleteView, UpdateView, TemplateView, CreateView, UpdateView,
) )
from braces.views import ( from braces.views import (
...@@ -47,6 +47,7 @@ from ..tables import TemplateListTable, LeaseListTable ...@@ -47,6 +47,7 @@ from ..tables import TemplateListTable, LeaseListTable
from .util import ( from .util import (
AclUpdateView, FilterMixin, AclUpdateView, FilterMixin,
TransferOwnershipConfirmView, TransferOwnershipView, TransferOwnershipConfirmView, TransferOwnershipView,
DeleteViewBase
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -56,7 +57,7 @@ class TemplateChoose(LoginRequiredMixin, TemplateView): ...@@ -56,7 +57,7 @@ class TemplateChoose(LoginRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/modal-wrapper.html'] return ['dashboard/_modal.html']
else: else:
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
...@@ -231,46 +232,17 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView): ...@@ -231,46 +232,17 @@ class TemplateList(LoginRequiredMixin, FilterMixin, SingleTableView):
return qs.select_related("lease", "owner", "owner__profile") return qs.select_related("lease", "owner", "owner__profile")
class TemplateDelete(LoginRequiredMixin, DeleteView): class TemplateDelete(DeleteViewBase):
model = InstanceTemplate model = InstanceTemplate
success_message = _("Template successfully deleted.")
def get_success_url(self): def get_success_url(self):
return reverse("dashboard.views.template-list") return reverse("dashboard.views.template-list")
def get_template_names(self): def delete_obj(self, request, *args, **kwargs):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
message = _("Only the owners can delete the selected template.")
if request.is_ajax():
raise PermissionDenied()
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(TemplateDelete, self).get(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
object = self.get_object() object = self.get_object()
if not object.has_level(request.user, 'owner'):
raise PermissionDenied()
object.destroy_disks() object.destroy_disks()
object.delete() object.delete()
success_url = self.get_success_url()
success_message = _("Template successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
...@@ -333,25 +305,24 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -333,25 +305,24 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return kwargs return kwargs
class DiskRemoveView(DeleteView): class DiskRemoveView(DeleteViewBase):
model = Disk model = Disk
success_message = _("Disk successfully removed.")
def get_queryset(self): def get_queryset(self):
qs = super(DiskRemoveView, self).get_queryset() qs = super(DiskRemoveView, self).get_queryset()
return qs.exclude(template_set=None) return qs.exclude(template_set=None)
def get_template_names(self): def check_auth(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(DiskRemoveView, self).get_context_data(**kwargs)
disk = self.get_object() disk = self.get_object()
template = disk.template_set.get() template = disk.template_set.get()
if not template.has_level(self.request.user, 'owner'): if not template.has_level(self.request.user, 'owner'):
raise PermissionDenied() raise PermissionDenied()
def get_context_data(self, **kwargs):
disk = self.get_object()
template = disk.template_set.get()
context = super(DiskRemoveView, self).get_context_data(**kwargs)
context['title'] = _("Disk remove confirmation") context['title'] = _("Disk remove confirmation")
context['text'] = _("Are you sure you want to remove " context['text'] = _("Are you sure you want to remove "
"<strong>%(disk)s</strong> from " "<strong>%(disk)s</strong> from "
...@@ -360,28 +331,14 @@ class DiskRemoveView(DeleteView): ...@@ -360,28 +331,14 @@ class DiskRemoveView(DeleteView):
) )
return context return context
def delete(self, request, *args, **kwargs): def delete_obj(self, request, *args, **kwargs):
disk = self.get_object() disk = self.get_object()
template = disk.template_set.get() template = disk.template_set.get()
template.remove_disk(disk)
if not template.has_level(request.user, 'owner'):
raise PermissionDenied()
template.remove_disk(disk=disk, user=request.user)
disk.destroy() disk.destroy()
next_url = request.POST.get("next") def get_success_url(self):
success_url = next_url if next_url else template.get_absolute_url() return self.request.POST.get("next") or "/"
success_message = _("Disk successfully removed.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#resources" % success_url)
class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin, class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
...@@ -435,18 +392,13 @@ class LeaseDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -435,18 +392,13 @@ class LeaseDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return super(LeaseDetail, self).post(request, *args, **kwargs) return super(LeaseDetail, self).post(request, *args, **kwargs)
class LeaseDelete(LoginRequiredMixin, DeleteView): class LeaseDelete(DeleteViewBase):
model = Lease model = Lease
success_message = _("Lease successfully deleted.")
def get_success_url(self): def get_success_url(self):
return reverse("dashboard.views.template-list") return reverse("dashboard.views.template-list")
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
c = super(LeaseDelete, self).get_context_data(*args, **kwargs) c = super(LeaseDelete, self).get_context_data(*args, **kwargs)
lease = self.get_object() lease = self.get_object()
...@@ -461,36 +413,11 @@ class LeaseDelete(LoginRequiredMixin, DeleteView): ...@@ -461,36 +413,11 @@ class LeaseDelete(LoginRequiredMixin, DeleteView):
c['disable_submit'] = True c['disable_submit'] = True
return c return c
def get(self, request, *args, **kwargs): def delete_obj(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
message = _("Only the owners can delete the selected lease.")
if request.is_ajax():
raise PermissionDenied()
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(LeaseDelete, self).get(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
object = self.get_object() object = self.get_object()
if not object.has_level(request.user, "owner"):
raise PermissionDenied()
if object.instancetemplate_set.count() > 0: if object.instancetemplate_set.count() > 0:
raise SuspiciousOperation() raise SuspiciousOperation()
object.delete() object.delete()
success_url = self.get_success_url()
success_message = _("Lease successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView): class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView):
......
...@@ -30,12 +30,13 @@ from django.core.exceptions import ( ...@@ -30,12 +30,13 @@ from django.core.exceptions import (
PermissionDenied, SuspiciousOperation, PermissionDenied, SuspiciousOperation,
) )
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.core.paginator import Paginator, InvalidPage
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.views.generic import ( from django.views.generic import (
TemplateView, DetailView, View, DeleteView, UpdateView, CreateView, TemplateView, DetailView, View, UpdateView, CreateView,
) )
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
...@@ -50,7 +51,7 @@ from ..forms import ( ...@@ -50,7 +51,7 @@ from ..forms import (
from ..models import Profile, GroupProfile, ConnectCommand, create_profile from ..models import Profile, GroupProfile, ConnectCommand, create_profile
from ..tables import UserKeyListTable, ConnectCommandListTable from ..tables import UserKeyListTable, ConnectCommandListTable
from .util import saml_available from .util import saml_available, DeleteViewBase
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -67,9 +68,18 @@ class NotificationView(LoginRequiredMixin, TemplateView): ...@@ -67,9 +68,18 @@ class NotificationView(LoginRequiredMixin, TemplateView):
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super(NotificationView, self).get_context_data( context = super(NotificationView, self).get_context_data(
*args, **kwargs) *args, **kwargs)
n = 10 if self.request.is_ajax() else 1000 paginate_by = 10 if self.request.is_ajax() else 25
context['notifications'] = list( page = self.request.GET.get("page", 1)
self.request.user.notification_set.all()[:n])
notifications = self.request.user.notification_set.all()
paginator = Paginator(notifications, paginate_by)
try:
current_page = paginator.page(page)
except InvalidPage:
current_page = paginator.page(1)
context['page'] = current_page
context['paginator'] = paginator
return context return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
...@@ -257,6 +267,9 @@ class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin, ...@@ -257,6 +267,9 @@ class UserCreationView(LoginRequiredMixin, PermissionRequiredMixin,
template_name = 'dashboard/user-create.html' template_name = 'dashboard/user-create.html'
permission_required = "auth.add_user" permission_required = "auth.add_user"
def get_success_url(self):
reverse('dashboard.views.group-detail', args=[self.group.pk])
def get_group(self, group_pk): def get_group(self, group_pk):
self.group = get_object_or_404(Group, pk=group_pk) self.group = get_object_or_404(Group, pk=group_pk)
if not self.group.profile.has_level(self.request.user, 'owner'): if not self.group.profile.has_level(self.request.user, 'owner'):
...@@ -299,7 +312,7 @@ class ProfileView(LoginRequiredMixin, DetailView): ...@@ -299,7 +312,7 @@ class ProfileView(LoginRequiredMixin, DetailView):
# if the intersection of the 2 lists is empty the logged in user # if the intersection of the 2 lists is empty the logged in user
# has no permission to check the target's profile # has no permission to check the target's profile
# (except if the user want to see his own profile) # (except if the user want to see his own profile)
if len(intersection) < 1 and target != user: if not intersection and target != user and not user.is_superuser:
raise PermissionDenied raise PermissionDenied
return super(ProfileView, self).get(*args, **kwargs) return super(ProfileView, self).get(*args, **kwargs)
...@@ -385,36 +398,17 @@ class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -385,36 +398,17 @@ class UserKeyDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return super(UserKeyDetail, self).post(self, request, args, kwargs) return super(UserKeyDetail, self).post(self, request, args, kwargs)
class UserKeyDelete(LoginRequiredMixin, DeleteView): class UserKeyDelete(DeleteViewBase):
model = UserKey model = UserKey
success_message = _("SSH key successfully deleted.")
def get_success_url(self): def get_success_url(self):
return reverse("dashboard.views.profile-preferences") return reverse("dashboard.views.profile-preferences")
def get_template_names(self): def check_auth(self):
if self.request.is_ajax(): if self.get_object().user != self.request.user:
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def delete(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied() raise PermissionDenied()
object.delete()
success_url = self.get_success_url()
success_message = _("SSH key successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView): class UserKeyCreate(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = UserKey model = UserKey
...@@ -460,36 +454,17 @@ class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin, ...@@ -460,36 +454,17 @@ class ConnectCommandDetail(LoginRequiredMixin, SuccessMessageMixin,
return kwargs return kwargs
class ConnectCommandDelete(LoginRequiredMixin, DeleteView): class ConnectCommandDelete(DeleteViewBase):
model = ConnectCommand model = ConnectCommand
success_message = _("Command template successfully deleted.")
def get_success_url(self): def get_success_url(self):
return reverse("dashboard.views.profile-preferences") return reverse("dashboard.views.profile-preferences")
def get_template_names(self): def check_auth(self):
if self.request.is_ajax(): if self.get_object().user != self.request.user:
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def delete(self, request, *args, **kwargs):
object = self.get_object()
if object.user != request.user:
raise PermissionDenied() raise PermissionDenied()
object.delete()
success_url = self.get_success_url()
success_message = _("Command template successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin, class ConnectCommandCreate(LoginRequiredMixin, SuccessMessageMixin,
CreateView): CreateView):
......
...@@ -33,7 +33,7 @@ from django.db.models import Q ...@@ -33,7 +33,7 @@ from django.db.models import Q
from django.http import HttpResponse, Http404, HttpResponseRedirect from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.utils.translation import ugettext_lazy as _, ugettext_noop from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.views.generic import DetailView, View from django.views.generic import DetailView, View, DeleteView
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from braces.views import LoginRequiredMixin from braces.views import LoginRequiredMixin
...@@ -694,3 +694,45 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View): ...@@ -694,3 +694,45 @@ class TransferOwnershipConfirmView(LoginRequiredMixin, View):
unicode(user), user.pk, new_owner, key) unicode(user), user.pk, new_owner, key)
raise PermissionDenied() raise PermissionDenied()
return (instance, new_owner) return (instance, new_owner)
class DeleteViewBase(LoginRequiredMixin, DeleteView):
level = 'owner'
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def check_auth(self):
if not self.get_object().has_level(self.request.user, self.level):
raise PermissionDenied()
def get(self, request, *args, **kwargs):
try:
self.check_auth()
except PermissionDenied:
message = _("Only the owners can delete the selected object.")
if request.is_ajax():
raise PermissionDenied()
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(DeleteViewBase, self).get(request, *args, **kwargs)
def delete_obj(self, request, *args, **kwargs):
self.get_object().delete()
def delete(self, request, *args, **kwargs):
self.check_auth()
self.delete_obj(request, *args, **kwargs)
if request.is_ajax():
return HttpResponse(
json.dumps({'message': self.success_message}),
content_type="application/json",
)
else:
messages.success(request, self.success_message)
return HttpResponseRedirect(self.get_success_url())
...@@ -37,7 +37,7 @@ from django.utils.translation import ( ...@@ -37,7 +37,7 @@ from django.utils.translation import (
) )
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django.views.generic import ( from django.views.generic import (
UpdateView, ListView, TemplateView, DeleteView UpdateView, ListView, TemplateView
) )
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
...@@ -64,8 +64,10 @@ from ..forms import ( ...@@ -64,8 +64,10 @@ from ..forms import (
VmDiskResizeForm, RedeployForm, VmDiskRemoveForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmMigrateForm, VmDeployForm, VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm, VmPortRemoveForm, VmPortAddForm,
VmRemoveInterfaceForm,
) )
from ..models import Favourite from ..models import Favourite
from manager.scheduler import has_traits
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -324,6 +326,32 @@ def get_operations(instance, user): ...@@ -324,6 +326,32 @@ def get_operations(instance, user):
return ops return ops
class VmRemoveInterfaceView(FormOperationMixin, VmOperationView):
op = 'remove_interface'
form_class = VmRemoveInterfaceForm
show_in_toolbar = False
wait_for_result = 0.5
icon = 'times'
effect = "danger"
with_reload = True
def get_form_kwargs(self):
instance = self.get_op().instance
choices = instance.interface_set.all()
interface_pk = self.request.GET.get('interface')
if interface_pk:
try:
default = choices.get(pk=interface_pk)
except (ValueError, Interface.DoesNotExist):
raise Http404()
else:
default = None
val = super(VmRemoveInterfaceView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
class VmAddInterfaceView(FormOperationMixin, VmOperationView): class VmAddInterfaceView(FormOperationMixin, VmOperationView):
op = 'add_interface' op = 'add_interface'
...@@ -417,6 +445,20 @@ class VmMigrateView(FormOperationMixin, VmOperationView): ...@@ -417,6 +445,20 @@ class VmMigrateView(FormOperationMixin, VmOperationView):
val.update({'choices': choices, 'default': default}) val.update({'choices': choices, 'default': default})
return val return val
def get_context_data(self, *args, **kwargs):
ctx = super(VmMigrateView, self).get_context_data(*args, **kwargs)
inst = self.get_object()
if isinstance(inst, Instance):
nodes_w_traits = [
n.pk for n in Node.objects.filter(enabled=True)
if n.online and
has_traits(inst.req_traits.all(), n)
]
ctx['nodes_w_traits'] = nodes_w_traits
return ctx
class VmPortRemoveView(FormOperationMixin, VmOperationView): class VmPortRemoveView(FormOperationMixin, VmOperationView):
...@@ -671,6 +713,7 @@ class VmDeployView(FormOperationMixin, VmOperationView): ...@@ -671,6 +713,7 @@ class VmDeployView(FormOperationMixin, VmOperationView):
online = (n.pk for n in online = (n.pk for n in
Node.objects.filter(enabled=True) if n.online) Node.objects.filter(enabled=True) if n.online)
kwargs['choices'] = Node.objects.filter(pk__in=online) kwargs['choices'] = Node.objects.filter(pk__in=online)
kwargs['instance'] = self.get_object()
return kwargs return kwargs
...@@ -707,6 +750,7 @@ vm_ops = OrderedDict([ ...@@ -707,6 +750,7 @@ vm_ops = OrderedDict([
op='remove_disk', form_class=VmDiskRemoveForm, op='remove_disk', form_class=VmDiskRemoveForm,
icon='times', effect="danger")), icon='times', effect="danger")),
('add_interface', VmAddInterfaceView), ('add_interface', VmAddInterfaceView),
('remove_interface', VmRemoveInterfaceView),
('remove_port', VmPortRemoveView), ('remove_port', VmPortRemoveView),
('add_port', VmPortAddView), ('add_port', VmPortAddView),
('renew', VmRenewView), ('renew', VmRenewView),
...@@ -951,10 +995,21 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -951,10 +995,21 @@ class VmCreate(LoginRequiredMixin, TemplateView):
def get_template_names(self): def get_template_names(self):
if self.request.is_ajax(): if self.request.is_ajax():
return ['dashboard/modal-wrapper.html'] return ['dashboard/_modal.html']
else: else:
return ['dashboard/nojs-wrapper.html'] return ['dashboard/nojs-wrapper.html']
def get_template(self, request, pk):
try:
template = InstanceTemplate.objects.get(
pk=int(pk))
except (ValueError, InstanceTemplate.DoesNotExist):
raise Http404()
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
return template
def get(self, request, form=None, *args, **kwargs): def get(self, request, form=None, *args, **kwargs):
if not request.user.has_perm('vm.create_vm'): if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied() raise PermissionDenied()
...@@ -965,9 +1020,7 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -965,9 +1020,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
template_pk = form.template.pk template_pk = form.template.pk
if template_pk: if template_pk:
template = get_object_or_404(InstanceTemplate, pk=template_pk) template = self.get_template(request, template_pk)
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
if form is None: if form is None:
form = self.form_class(user=request.user, template=template) form = self.form_class(user=request.user, template=template)
else: else:
...@@ -992,33 +1045,21 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -992,33 +1045,21 @@ class VmCreate(LoginRequiredMixin, TemplateView):
}) })
return self.render_to_response(context) return self.render_to_response(context)
def __create_normal(self, request, *args, **kwargs): def __create_normal(self, request, template, *args, **kwargs):
user = request.user instances = [Instance.create_from_template(
template = InstanceTemplate.objects.get( template=template,
pk=request.POST.get("template")) owner=request.user)]
# permission check
if not template.has_level(request.user, 'user'):
raise PermissionDenied()
args = {"template": template, "owner": user}
instances = [Instance.create_from_template(**args)]
return self.__deploy(request, instances) return self.__deploy(request, instances)
def __create_customized(self, request, *args, **kwargs): def __create_customized(self, request, template, *args, **kwargs):
user = request.user user = request.user
# no form yet, using POST directly: # no form yet, using POST directly:
template = get_object_or_404(InstanceTemplate,
pk=request.POST.get("template"))
form = self.form_class( form = self.form_class(
request.POST, user=request.user, template=template) request.POST, user=request.user, template=template)
if not form.is_valid(): if not form.is_valid():
return self.get(request, form, *args, **kwargs) return self.get(request, form, *args, **kwargs)
post = form.cleaned_data post = form.cleaned_data
if not template.has_level(user, 'user'):
raise PermissionDenied()
ikwargs = { ikwargs = {
'name': post['name'], 'name': post['name'],
'template': template, 'template': template,
...@@ -1071,6 +1112,8 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1071,6 +1112,8 @@ class VmCreate(LoginRequiredMixin, TemplateView):
if not request.user.has_perm('vm.create_vm'): if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied() raise PermissionDenied()
template = self.get_template(request, request.POST.get("template"))
# limit chekcs # limit chekcs
try: try:
limit = user.profile.instance_limit limit = user.profile.instance_limit
...@@ -1096,7 +1139,7 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1096,7 +1139,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
request.POST.get("customized") is None else request.POST.get("customized") is None else
self.__create_customized) self.__create_customized)
return create_func(request, *args, **kwargs) return create_func(request, template, *args, **kwargs)
@require_GET @require_GET
...@@ -1108,57 +1151,7 @@ def get_vm_screenshot(request, pk): ...@@ -1108,57 +1151,7 @@ def get_vm_screenshot(request, pk):
# TODO handle this better # TODO handle this better
raise Http404() raise Http404()
return HttpResponse(image, mimetype="image/png") return HttpResponse(image, content_type="image/png")
class InterfaceDeleteView(DeleteView):
model = Interface
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(InterfaceDeleteView, self).get_context_data(**kwargs)
interface = self.get_object()
context['text'] = _("Are you sure you want to remove this interface "
"from <strong>%(vm)s</strong>?" %
{'vm': interface.instance.name})
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
instance = self.object.instance
if not instance.has_level(request.user, "owner"):
raise PermissionDenied()
instance.remove_interface(interface=self.object, user=request.user)
success_url = self.get_success_url()
success_message = _("Interface successfully deleted.")
if request.is_ajax():
return HttpResponse(
json.dumps(
{'message': success_message,
'removed_network': {
'vlan': self.object.vlan.name,
'vlan_pk': self.object.vlan.pk,
'managed': self.object.host is not None,
}}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#network" % success_url)
def get_success_url(self):
redirect = self.request.POST.get("next")
if redirect:
return redirect
self.object.instance.get_absolute_url()
class InstanceActivityDetail(CheckedDetailView): class InstanceActivityDetail(CheckedDetailView):
......
...@@ -71,10 +71,17 @@ def compile_messages(): ...@@ -71,10 +71,17 @@ def compile_messages():
run("./manage.py compilemessages") run("./manage.py compilemessages")
def compile_less():
"Compile LESS files"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py compileless")
@roles('portal') @roles('portal')
def compile_things(): def compile_things():
"Compile translation and collect static files" "Compile translation and collect static files"
compile_js() compile_js()
compile_less()
collectstatic() collectstatic()
compile_messages() compile_messages()
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import firewall.fields
class Migration(migrations.Migration):
dependencies = [
('firewall', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='vlan',
name='ipv6_template',
field=models.TextField(help_text='Template for translating IPv4 addresses to IPv6. Automatically generated hosts in dual-stack networks will get this address. The template can contain four tokens: "%(a)d", "%(b)d", "%(c)d", and "%(d)d", representing the four bytes of the IPv4 address, respectively, in decimal notation. Moreover you can use any standard printf format specification like %(a)02x to get the first byte as two hexadecimal digits. Usual choices for mapping 198.51.100.0/24 to 2001:0DB8:1:1::/64 would be "2001:db8:1:1:%(d)d::" and "2001:db8:1:1:%(d)02x00::".', blank=True, verbose_name='ipv6 template', validators=[firewall.fields.val_ipv6_template]),
preserve_default=True,
),
]
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment