diff --git a/.gitignore b/.gitignore index 1a2a4cd..0238b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,11 @@ coverage.xml *.mo # saml -circle/attribute-maps/ +circle/attribute-maps circle/remote_metadata.xml -circle/samlcert.key -circle/samlcert.pem +circle/*.key +circle/*.pem + +# collected static files: +circle/static +circle/static_collected diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index 41dcd67..de25d05 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -5,6 +5,7 @@ from os.path import abspath, basename, dirname, join, normpath, isfile from sys import path from django.core.exceptions import ImproperlyConfigured +from django.utils.translation import ugettext_lazy as _ from json import loads @@ -93,7 +94,13 @@ except: TIME_ZONE = get_env_variable('DJANGO_TIME_ZONE', default=systz) # See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = get_env_variable("DJANGO_LANGUAGE_CODE", "en") + +# https://docs.djangoproject.com/en/dev/ref/settings/#languages +LANGUAGES = ( + ('en', _('English')), + ('hu', _('Hungarian')), +) # See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id SITE_ID = 1 @@ -125,11 +132,6 @@ STATIC_ROOT = normpath(join(SITE_ROOT, 'static_collected')) # See: https://docs.djangoproject.com/en/dev/ref/settings/#static-url STATIC_URL = get_env_variable('DJANGO_STATIC_URL', default='/static/') -# See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS -STATICFILES_DIRS = ( - normpath(join(SITE_ROOT, 'static')), -) - # See: https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', diff --git a/circle/common/operations.py b/circle/common/operations.py index 7a11e15..ed01532 100644 --- a/circle/common/operations.py +++ b/circle/common/operations.py @@ -13,6 +13,7 @@ class Operation(object): """ async_queue = 'localhost.man' required_perms = () + do_not_call_in_templates = True def __call__(self, **kwargs): return self.call(**kwargs) @@ -28,11 +29,11 @@ class Operation(object): def __prelude(self, kwargs): """This method contains the shared prelude of call and async. """ - skip_checks = kwargs.setdefault('system', False) + skip_auth_check = kwargs.setdefault('system', False) user = kwargs.setdefault('user', None) parent_activity = kwargs.pop('parent_activity', None) - if not skip_checks: + if not skip_auth_check: self.check_auth(user) self.check_precond() return self.create_activity(parent=parent_activity, user=user) @@ -42,8 +43,7 @@ class Operation(object): """ with activity_context(activity, on_abort=self.on_abort, on_commit=self.on_commit): - return self._operation(activity=activity, user=user, - **kwargs) + return self._operation(activity=activity, user=user, **kwargs) def _operation(self, activity, user, system, **kwargs): """This method is the operation's particular implementation. @@ -128,15 +128,46 @@ class OperatedMixin(object): raise AttributeError("%r object has no attribute %r" % (self.__class__.__name__, name)) - -def register_operation(target_cls, op_cls, op_id=None): + def get_available_operations(self, user): + """Yield Operations that match permissions of user and preconditions. + """ + for name in getattr(self, operation_registry_name, {}): + try: + op = getattr(self, name) + op.check_auth(user) + op.check_precond() + except: + pass # unavailable + else: + yield op + + +def register_operation(op_cls, op_id=None, target_cls=None): """Register the specified operation with the target class. You can optionally specify an ID to be used for the registration; otherwise, the operation class' 'id' attribute will be used. """ if op_id is None: - op_id = op_cls.id + try: + op_id = op_cls.id + except AttributeError: + raise NotImplementedError("Operations should specify an 'id' " + "attribute designating the name the " + "operation can be called by on its " + "host. Alternatively, provide the name " + "in the 'op_id' parameter to this call.") + + if target_cls is None: + try: + target_cls = op_cls.host_cls + except AttributeError: + raise NotImplementedError("Operations should specify a 'host_cls' " + "attribute designating the host class " + "the operation should be registered to. " + "Alternatively, provide the host class " + "in the 'target_cls' parameter to this " + "call.") if not issubclass(target_cls, OperatedMixin): raise TypeError("%r is not a subclass of %r" % diff --git a/circle/dashboard/admin.py b/circle/dashboard/admin.py new file mode 100644 index 0000000..65eeeee --- /dev/null +++ b/circle/dashboard/admin.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +from django import contrib +from django.contrib.auth.admin import UserAdmin +from django.contrib.auth.models import User + +from dashboard.models import Profile + + +class ProfileInline(contrib.admin.TabularInline): + model = Profile + + +UserAdmin.inlines = (ProfileInline, ) + +contrib.admin.site.unregister(User) +contrib.admin.site.register(User, UserAdmin) diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index 4141946..dcc0359 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -1,8 +1,11 @@ +from __future__ import absolute_import + from datetime import timedelta from django.contrib.auth.models import User from django.contrib.auth.forms import ( AuthenticationForm, PasswordResetForm, SetPasswordForm, + PasswordChangeForm, ) from crispy_forms.helper import FormHelper @@ -1075,9 +1078,20 @@ class MyProfileForm(forms.ModelForm): def helper(self): helper = FormHelper() helper.layout = Layout('preferred_language', ) - helper.add_input(Submit("submit", _("Save"))) + helper.add_input(Submit("submit", _("Change language"))) return helper def save(self, *args, **kwargs): value = super(MyProfileForm, self).save(*args, **kwargs) return value + + +class CirclePasswordChangeForm(PasswordChangeForm): + + @property + def helper(self): + helper = FormHelper() + helper.add_input(Submit("submit", _("Change password"), + css_class="btn btn-primary", + css_id="submit-password-button")) + return helper diff --git a/circle/dashboard/models.py b/circle/dashboard/models.py index 680b524..18c8533 100644 --- a/circle/dashboard/models.py +++ b/circle/dashboard/models.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from itertools import chain from logging import getLogger diff --git a/circle/dashboard/static/dashboard/vm-common.js b/circle/dashboard/static/dashboard/vm-common.js index 55599f3..c26738a 100644 --- a/circle/dashboard/static/dashboard/vm-common.js +++ b/circle/dashboard/static/dashboard/vm-common.js @@ -2,22 +2,21 @@ $(function() { - /* vm migrate */ - $('.vm-migrate').click(function(e) { - var icon = $(this).children("i"); - var vm = $(this).data("vm-pk"); - icon.removeClass("icon-truck").addClass("icon-spinner icon-spin"); + /* vm operations */ + $('#ops').on('click', '.operation.btn', function(e) { + var icon = $(this).children("i").addClass('icon-spinner icon-spin'); $.ajax({ type: 'GET', - url: '/dashboard/vm/' + vm + '/migrate/', + url: $(this).attr('href'), success: function(data) { - icon.addClass("icon-truck").removeClass("icon-spinner icon-spin"); + icon.removeClass("icon-spinner icon-spin"); $('body').append(data); - $('#create-modal').modal('show'); - $('#create-modal').on('hidden.bs.modal', function() { - $('#create-modal').remove(); + $('#confirmation-modal').modal('show'); + $('#confirmation-modal').on('hidden.bs.modal', function() { + $('#confirmation-modal').remove(); }); + $('#vm-migrate-node-list li').click(function(e) { var li = $(this).closest('li'); if (li.find('input').attr('disabled')) diff --git a/circle/dashboard/static/dashboard/vm-details.js b/circle/dashboard/static/dashboard/vm-details.js index 916f75b..dce3b55 100644 --- a/circle/dashboard/static/dashboard/vm-details.js +++ b/circle/dashboard/static/dashboard/vm-details.js @@ -211,6 +211,7 @@ function checkNewActivity(only_status, runs) { success: function(data) { if(!only_status) { $("#activity-timeline").html(data['activities']); + $("#ops").html(data['ops']); $("[title]").tooltip(); } diff --git a/circle/dashboard/tables.py b/circle/dashboard/tables.py index 301eb3b..14b508c 100644 --- a/circle/dashboard/tables.py +++ b/circle/dashboard/tables.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from django.contrib.auth.models import Group, User from django_tables2 import Table, A from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, diff --git a/circle/dashboard/templates/dashboard/_base.html b/circle/dashboard/templates/dashboard/_base.html new file mode 100644 index 0000000..572e73f --- /dev/null +++ b/circle/dashboard/templates/dashboard/_base.html @@ -0,0 +1,20 @@ +{% extends "dashboard/base.html" %} +{% load i18n %} + +{% block content %} +