diff --git a/TODO.md b/TODO.md index bfad8a93..92b2d7ce 100644 --- a/TODO.md +++ b/TODO.md @@ -455,8 +455,130 @@ mkhomedir_helper or create ssh homes with bash.rc and such * setuppostgres use porject_name for db name and user instead of orchestra -* show all available choices (plugins) on admin settings value i.e. payment methods + # POSTFIX web traffic monitor '": uid=" from=<%(user)s>' + + +### Quick start + 0. Install orchestra following any of these methods: + 1. [PIP-only, Fast deployment setup (demo)](README.md#fast-deployment-setup) + 2. [Docker container (development)](INSTALLDEV.md) + 3. [Install on current system (production)](INSTALL.md) + 1. Add the servers that you want to manage on `/admin/orchestration/servers` and copy orchestra's SSH pubkey to them + `orchestra@panel:~ ssh-copy-id root@server.address` + 2. Now configure service by service (domains, databases, webapps, websites...): + 1. Add the route through `/admin/orchestration/route/` + 2. Check and configure related settings on `/admin/settings/setting/` + 3. Configure related resources if needed `/resources/resource/`, like Account Disc limit and traffic. + 3. Test that everything works by creating and deleting services + 4. Do the same for the other services + 3. Configure billing by adding services `/admin/services/service/add/` and plans `/admin/plans/plan/` + 1. Once a service is created hit the *Update orders* button + + +### Architecture + Orchestration + Orders + + +### Creating new services + 1. Think about if the service can fit into one of the existing models like: SaaS or WebApps, refere to the related documentation if that is the case. + 2. Create a new django app using startapp management command. For ilustrational purposes we will create a crontab services that will allow orchestra to manage user-based crontabs. + `python3 manage.py startapp crontabs` + 3. Add the new *crontabs* app to the `INSTALLED_APPS` in your project's `settings.py` + 3. Create a `models.py` file with the data your service needs to keep in order to be managed by orchestra + ```python +from django.db import models + +class CrontabSchedule(models.Model): + account = models.ForeignKey('accounts.Account', verbose_name=_("account")) + minute = models.CharField(_("minute"), max_length=64, default='*') + hour = models.CharField(_("hour"), max_length=64, default='*') + day_of_week = models.CharField(_("day of week"), max_length=64, default='*') + day_of_month = models.CharField(_("day of month"), max_length=64, default='*') + month_of_year = models.CharField(_("month of year"), max_length=64, default='*') + + class Meta: + ordering = ('month_of_year', 'day_of_month', 'day_of_week', 'hour', 'minute') + + def __str__(self): + rfield = lambda f: f and str(f).replace(' ', '') or '*' + return "{0} {1} {2} {3} {4} (m/h/d/dM/MY)".format( + rfield(self.minute), rfield(self.hour), rfield(self.day_of_week), + rfield(self.day_of_month), rfield(self.month_of_year), + ) + +class Crontab(models.Model): + account = models.ForeignKey('accounts.Account', verbose_name=_("account")) + schedule = models.ForeignKey(CrontabSchedule, verbose_name=_("schedule")) + description = models.CharField(_("description"), max_length=256, blank=True) + command = models.TextField(_("content")) + + def __str__(self): + return (self.description or self.command)[:32] +``` + + 4. Create a `admin.py` to enable the admin interface + ```python + from django.contrib import admin + from .models import CrontabSchedule, Crontab + + class CrontabScheduleAdmin(admin.ModelAdmin): + pass + + class CrontabAdmin(admin.ModelAdmin): + pass + + admin.site.register(CrontabSchedule, CrontabScheduleAdmin) + admin.site.register(Crontab, CrontabAdmin) + + + 5. Create a `api.py` to enable the REST API. + 6. Create a `backends.py` fiel with the needed backends for service orchestration and monitoring + ```python + import os + import textwrap + from django.utils.translation import ugettext_lazy as _ + from orchestra.contrib.orchestration import ServiceController, replace + from orchestra.contrib.resources import ServiceMonitor + + class UNIXCronBackend(ServiceController): + """ + Basic UNIX cron support. + """ + verbose_name = _("UNIX cron") + model = 'crons.CronTab' + + def prepare(self): + super(UNIXCronBackend, self).prepare() + self.accounts = set() + + def save(self, crontab): + self.accounts.add(crontab.account) + + def delete(self, crontab): + self.accounts.add(crontab.account) + + def commit(self): + for account in self.accounts: + crontab = None + self.append("echo '' > %(crontab_path)s" % context) + chown + for crontab in account.crontabs.all(): + self.append(" + # if crontab is None: + # self.append("rm -f %(crontab_path)s" % context) + ``` + 7. Configure the routing + + + + + + + + + diff --git a/orchestra/contrib/mailboxes/models.py b/orchestra/contrib/mailboxes/models.py index a6326d54..5c9694fb 100644 --- a/orchestra/contrib/mailboxes/models.py +++ b/orchestra/contrib/mailboxes/models.py @@ -15,7 +15,8 @@ class Mailbox(models.Model): CUSTOM = 'CUSTOM' name = models.CharField(_("name"), max_length=64, unique=True, - help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."), + help_text=_("Required. %s characters or fewer. Letters, digits and ./-/_ only.") % + settings.MAILBOXES_NAME_MAX_LENGTH, validators=[ RegexValidator(r'^[\w.-]+$', _("Enter a valid mailbox name.")), ]) diff --git a/orchestra/contrib/mailboxes/settings.py b/orchestra/contrib/mailboxes/settings.py index fc74e56e..f8dccc50 100644 --- a/orchestra/contrib/mailboxes/settings.py +++ b/orchestra/contrib/mailboxes/settings.py @@ -21,8 +21,8 @@ MAILBOXES_DOMAIN_MODEL = Setting('MAILBOXES_DOMAIN_MODEL', 'domains.Domain', MAILBOXES_NAME_MAX_LENGTH = Setting('MAILBOXES_NAME_MAX_LENGTH', - 64, - help_text=_("Limit for system user based mailbox on Linux should be 32.") + 32, + help_text=_("Limit for system user based mailbox on Linux is 32.") ) diff --git a/orchestra/contrib/miscellaneous/admin.py b/orchestra/contrib/miscellaneous/admin.py index 75a50e10..95934626 100644 --- a/orchestra/contrib/miscellaneous/admin.py +++ b/orchestra/contrib/miscellaneous/admin.py @@ -37,7 +37,7 @@ class MiscServiceAdmin(ExtendedModelAdmin): """ return num slivers as a link to slivers changelist view """ num = misc.instances__count url = reverse('admin:miscellaneous_miscellaneous_changelist') - url += '?service={}'.format(misc.pk) + url += '?service__name={}'.format(misc.name) return mark_safe('{1}'.format(url, num)) num_instances.short_description = _("Instances") num_instances.admin_order_field = 'instances__count' diff --git a/orchestra/contrib/payments/methods/options.py b/orchestra/contrib/payments/methods/options.py index cf74a635..4e661459 100644 --- a/orchestra/contrib/payments/methods/options.py +++ b/orchestra/contrib/payments/methods/options.py @@ -1,4 +1,6 @@ +import importlib import logging +import os from dateutil import relativedelta from functools import lru_cache @@ -8,7 +10,7 @@ from orchestra.utils.python import import_class from .. import settings -class PaymentMethod(plugins.Plugin): +class PaymentMethod(plugins.Plugin, metaclass=plugins.PluginMount): label_field = 'label' number_field = 'number' process_credit = False @@ -18,10 +20,16 @@ class PaymentMethod(plugins.Plugin): @classmethod @lru_cache() - def get_plugins(cls): - plugins = [] - for cls in settings.PAYMENTS_ENABLED_METHODS: - plugins.append(import_class(cls)) + def get_plugins(cls, all=False): + if all: + for module in os.listdir(os.path.dirname(__file__)): + if module not in ('options.py', '__init__.py') and module[-3:] == '.py': + importlib.import_module('.'+module[:-3], __package__) + plugins = super().get_plugins() + else: + plugins = [] + for cls in settings.PAYMENTS_ENABLED_METHODS: + plugins.append(import_class(cls)) return plugins def get_label(self): diff --git a/orchestra/contrib/payments/methods/sepadirectdebit.py b/orchestra/contrib/payments/methods/sepadirectdebit.py index 53a1e788..26c51b07 100644 --- a/orchestra/contrib/payments/methods/sepadirectdebit.py +++ b/orchestra/contrib/payments/methods/sepadirectdebit.py @@ -20,7 +20,7 @@ logger = logging.getLogger(__name__) try: import lxml except ImportError: - logger.error('Error loading lxml, module not install') + logger.error('Error loading lxml, module not installed.') class SEPADirectDebitForm(PluginDataForm): diff --git a/orchestra/contrib/payments/settings.py b/orchestra/contrib/payments/settings.py index 4a37ed9d..68d27511 100644 --- a/orchestra/contrib/payments/settings.py +++ b/orchestra/contrib/payments/settings.py @@ -41,6 +41,6 @@ PAYMENTS_ENABLED_METHODS = Setting('PAYMENTS_ENABLED_METHODS', 'orchestra.contrib.payments.methods.creditcard.CreditCard', ), # lazy loading - choices=lambda : ((m.get_class_path(), m.get_class_path()) for m in payments.methods.PaymentMethod.get_plugins()), + choices=lambda : ((m.get_class_path(), m.get_class_path()) for m in payments.methods.PaymentMethod.get_plugins(all=True)), multiple=True, ) diff --git a/orchestra/contrib/plans/admin.py b/orchestra/contrib/plans/admin.py index 58febe68..224865f2 100644 --- a/orchestra/contrib/plans/admin.py +++ b/orchestra/contrib/plans/admin.py @@ -1,7 +1,10 @@ from django.contrib import admin +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin -from orchestra.admin.utils import insertattr +from orchestra.admin.utils import insertattr, admin_link from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import AccountAdminMixin from orchestra.contrib.services.models import Service @@ -15,7 +18,9 @@ class RateInline(admin.TabularInline): class PlanAdmin(ExtendedModelAdmin): - list_display = ('name', 'is_default', 'is_combinable', 'allow_multiple', 'is_active') + list_display = ( + 'name', 'is_default', 'is_combinable', 'allow_multiple', 'is_active', 'num_contracts', + ) list_filter = ('is_default', 'is_combinable', 'allow_multiple', 'is_active') fields = ('verbose_name', 'name', 'is_default', 'is_combinable', 'allow_multiple') prepopulated_fields = { @@ -23,14 +28,29 @@ class PlanAdmin(ExtendedModelAdmin): } change_readonly_fields = ('name',) inlines = [RateInline] + + def num_contracts(self, plan): + num = plan.contracts__count + url = reverse('admin:plans_contractedplan_changelist') + url += '?plan__name={}'.format(plan.name) + return '{1}'.format(url, num) + num_contracts.short_description = _("Contracts") + num_contracts.admin_order_field = 'contracts__count' + num_contracts.allow_tags = True + + def get_queryset(self, request): + qs = super(PlanAdmin, self).get_queryset(request) + return qs.annotate(models.Count('contracts', distinct=True)) class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin): - list_display = ('plan', 'account_link') + list_display = ('id', 'plan_link', 'account_link') list_filter = ('plan__name',) list_select_related = ('plan', 'account') search_fields = ('account__username', 'plan__name', 'id') actions = (list_accounts,) + + plan_link = admin_link('plan') admin.site.register(Plan, PlanAdmin) diff --git a/orchestra/contrib/saas/services/options.py b/orchestra/contrib/saas/services/options.py index ee34eb91..0819f75b 100644 --- a/orchestra/contrib/saas/services/options.py +++ b/orchestra/contrib/saas/services/options.py @@ -1,3 +1,5 @@ +import importlib +import os from functools import lru_cache from urllib.parse import urlparse @@ -17,7 +19,7 @@ from .. import settings from ..forms import SaaSPasswordForm -class SoftwareService(plugins.Plugin): +class SoftwareService(plugins.Plugin, metaclass=plugins.PluginMount): PROTOCOL_MAP = { 'http': (Website.HTTP, (Website.HTTP, Website.HTTP_AND_HTTPS)), 'https': (Website.HTTPS_ONLY, (Website.HTTPS, Website.HTTP_AND_HTTPS, Website.HTTPS_ONLY)), @@ -35,10 +37,16 @@ class SoftwareService(plugins.Plugin): @classmethod @lru_cache() - def get_plugins(cls): - plugins = [] - for cls in settings.SAAS_ENABLED_SERVICES: - plugins.append(import_class(cls)) + def get_plugins(cls, all=False): + if all: + for module in os.listdir(os.path.dirname(__file__)): + if module not in ('options.py', '__init__.py') and module[-3:] == '.py': + importlib.import_module('.'+module[:-3], __package__) + plugins = super().get_plugins() + else: + plugins = [] + for cls in settings.SAAS_ENABLED_SERVICES: + plugins.append(import_class(cls)) return plugins def get_change_readonly_fileds(cls): @@ -143,6 +151,7 @@ class SoftwareService(plugins.Plugin): class DBSoftwareService(SoftwareService): db_name = None db_user = None + abstract = True def get_db_name(self): context = { diff --git a/orchestra/contrib/saas/settings.py b/orchestra/contrib/saas/settings.py index 34e9f1a0..9e2f4641 100644 --- a/orchestra/contrib/saas/settings.py +++ b/orchestra/contrib/saas/settings.py @@ -21,7 +21,7 @@ SAAS_ENABLED_SERVICES = Setting('SAAS_ENABLED_SERVICES', # 'orchestra.contrib.saas.services.seafile.SeaFileService', ), # lazy loading - choices=lambda: ((s.get_class_path(), s.get_class_path()) for s in saas.services.SoftwareService.get_plugins()), + choices=lambda: ((s.get_class_path(), s.get_class_path()) for s in saas.services.SoftwareService.get_plugins(all=True)), multiple=True, ) diff --git a/orchestra/contrib/services/admin.py b/orchestra/contrib/services/admin.py index 7afb0263..5f1b2d49 100644 --- a/orchestra/contrib/services/admin.py +++ b/orchestra/contrib/services/admin.py @@ -66,7 +66,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): def num_orders(self, service): num = service.orders__count url = reverse('admin:orders_order_changelist') - url += '?service=%i&is_active=True' % service.pk + url += '?service__id__exact=%i&is_active=True' % service.pk return '%d' % (url, num) num_orders.short_description = _("Orders") num_orders.admin_order_field = 'orders__count' diff --git a/orchestra/contrib/systemusers/actions.py b/orchestra/contrib/systemusers/actions.py index 102d73ad..8cefff51 100644 --- a/orchestra/contrib/systemusers/actions.py +++ b/orchestra/contrib/systemusers/actions.py @@ -7,7 +7,7 @@ from django.utils.translation import ungettext, ugettext_lazy as _ from orchestra.contrib.orchestration import Operation, helpers -from .forms import PermissionForm +from .forms import PermissionForm, LinkForm def get_verbose_choice(choices, value): @@ -21,11 +21,12 @@ def set_permission(modeladmin, request, queryset): for user in queryset: account_id = account_id or user.account_id if user.account_id != account_id: - messages.error("Users from the same account should be selected.") + messages.error(request, "Users from the same account should be selected.") + return user = queryset[0] form = PermissionForm(user) action_value = 'set_permission' - if request.POST.get('action') == action_value: + if request.POST.get('post') == 'generic_confirmation': form = PermissionForm(user, request.POST) if form.is_valid(): cleaned_data = form.cleaned_data @@ -35,7 +36,10 @@ def set_permission(modeladmin, request, queryset): extension = cleaned_data['home_extension'] action = cleaned_data['set_action'] perms = cleaned_data['permissions'] - user.set_permission(base_home, extension, action=action, perms=perms) + user.set_perm_action = action + user.set_perm_base_home = base_home + user.set_perm_home_extension = extension + user.set_perm_perms = perms operations.extend(Operation.create_for_action(user, 'set_permission')) verbose_action = get_verbose_choice(form.fields['set_action'].choices, user.set_perm_action) @@ -48,8 +52,11 @@ def set_permission(modeladmin, request, queryset): } msg = _("%(action)s %(perms)s permission to %(to)s") % context modeladmin.log_change(request, user, msg) - logs = Operation.execute(operations) - helpers.message_user(request, logs) + if not operations: + messages.error(request, "No backend operation has been executed.") + else: + logs = Operation.execute(operations) + helpers.message_user(request, logs) return opts = modeladmin.model._meta app_label = opts.app_label @@ -70,6 +77,60 @@ set_permission.url_name = 'set-permission' set_permission.tool_description = _("Set permission") +def create_link(modeladmin, request, queryset): + account_id = None + for user in queryset: + account_id = account_id or user.account_id + if user.account_id != account_id: + messages.error(request, "Users from the same account should be selected.") + return + user = queryset[0] + form = LinkForm(user) + action_value = 'create_link' + if request.POST.get('post') == 'generic_confirmation': + form = LinkForm(user, request.POST, queryset=queryset) + if form.is_valid(): + cleaned_data = form.cleaned_data + operations = [] + for user in queryset: + base_home = cleaned_data['base_home'] + extension = cleaned_data['home_extension'] + target = os.path.join(base_home, extension) + link_name = cleaned_data['link_name'] or os.path.join(user.home, os.path.basename(target)) + user.create_link_target = target + user.create_link_name = link_name + operations.extend(Operation.create_for_action(user, 'create_link')) + context = { + 'target': target, + 'link_name': link_name, + } + msg = _("Created link from %(target)s to %(link_name)s") % context + modeladmin.log_change(request, request.user, msg) + logs = Operation.execute(operations) + if logs: + helpers.message_user(request, logs) + else: + messages.error(request, "No backend operation has been executed.") + return + opts = modeladmin.model._meta + app_label = opts.app_label + context = { + 'title': _("Create link"), + 'action_name': _("Create link"), + 'action_value': action_value, + 'queryset': queryset, + 'opts': opts, + 'obj': user, + 'app_label': app_label, + 'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME, + 'form': form, + } + return TemplateResponse(request, 'admin/systemusers/systemuser/create_link.html', + context, current_app=modeladmin.admin_site.name) +create_link.url_name = 'create-link' +create_link.tool_description = _("Create link") + + def delete_selected(modeladmin, request, queryset): """ wrapper arround admin.actions.delete_selected to prevent main system users deletion """ opts = modeladmin.model._meta diff --git a/orchestra/contrib/systemusers/admin.py b/orchestra/contrib/systemusers/admin.py index db7c5ca7..4c4293ad 100644 --- a/orchestra/contrib/systemusers/admin.py +++ b/orchestra/contrib/systemusers/admin.py @@ -6,7 +6,7 @@ from orchestra.contrib.accounts.actions import list_accounts from orchestra.contrib.accounts.admin import SelectAccountAdminMixin from orchestra.contrib.accounts.filters import IsActiveListFilter -from .actions import set_permission, delete_selected +from .actions import set_permission, create_link, delete_selected from .filters import IsMainListFilter from .forms import SystemUserCreationForm, SystemUserChangeForm from .models import SystemUser @@ -41,7 +41,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende add_form = SystemUserCreationForm form = SystemUserChangeForm ordering = ('-id',) - change_view_actions = (set_permission,) + change_view_actions = (set_permission, create_link) actions = (delete_selected, list_accounts) + change_view_actions def display_main(self, user): diff --git a/orchestra/contrib/systemusers/backends.py b/orchestra/contrib/systemusers/backends.py index 6415646c..734f278c 100644 --- a/orchestra/contrib/systemusers/backends.py +++ b/orchestra/contrib/systemusers/backends.py @@ -17,7 +17,7 @@ class UNIXUserBackend(ServiceController): """ verbose_name = _("UNIX user") model = 'systemusers.SystemUser' - actions = ('save', 'delete', 'set_permission', 'validate_path_exists') + actions = ('save', 'delete', 'set_permission', 'validate_path_exists', 'create_link') doc_settings = (settings, ( 'SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', 'SYSTEMUSERS_MOVE_ON_DELETE_PATH', @@ -189,6 +189,24 @@ class UNIXUserBackend(ServiceController): else: raise NotImplementedError() + def create_link(self, user): + context = self.get_context(user) + context.update({ + 'link_target': user.create_link_target, + 'link_name': user.create_link_name, + }) + self.append(textwrap.dedent("""\ + # Create link + su %(user)s --shell /bin/bash << 'EOF' || exit_code=1 + if [[ ! -e %(link_name)s ]]; then + ln -s %(link_target)s %(link_name)s + else + echo "%(link_name)s already exists, doing nothing." >&2 + exit 1 + fi + EOF""") % context + ) + def validate_path_exists(self, user): context = { 'path': user.path_to_validate, diff --git a/orchestra/contrib/systemusers/forms.py b/orchestra/contrib/systemusers/forms.py index b706159f..c2bced5c 100644 --- a/orchestra/contrib/systemusers/forms.py +++ b/orchestra/contrib/systemusers/forms.py @@ -9,7 +9,7 @@ from orchestra.forms import UserCreationForm, UserChangeForm from . import settings from .models import SystemUser -from .validators import validate_home, validate_path_exists +from .validators import validate_home, validate_paths_exist class SystemUserFormMixin(object): @@ -89,7 +89,56 @@ class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm): pass -class PermissionForm(forms.Form): +class LinkForm(forms.Form): + base_home = forms.ChoiceField(label=_("Target path"), choices=(), + help_text=_("Target link will be under this directory.")) + home_extension = forms.CharField(label=_("Home extension"), required=False, initial='', + widget=forms.TextInput(attrs={'size':'70'}), help_text=_("Relative to chosen home.")) + link_name = forms.CharField(label=_("Link name"), required=False, initial='', + widget=forms.TextInput(attrs={'size':'70'}), + help_text=_("If left blank or relative path: link will be created in each user home.")) + + def __init__(self, *args, **kwargs): + self.instance = args[0] + self.queryset = kwargs.pop('queryset', []) + super_args = [] + if len(args) > 1: + super_args.append(args[1]) + super(LinkForm, self).__init__(*super_args, **kwargs) + related_users = type(self.instance).objects.filter(account=self.instance.account_id) + self.fields['base_home'].choices = ( + (user.get_base_home(), user.get_base_home()) for user in related_users + ) + + def clean_home_extension(self): + home_extension = self.cleaned_data['home_extension'] + return home_extension.lstrip('/') + + def clean_link_name(self): + link_name = self.cleaned_data['link_name'] + if link_name: + if link_name.startswith('/'): + if len(self.queryset) > 1: + raise ValidationError(_("Link name can not be a full path when multiple users.")) + link_names = [os.path.dirname(link_name)] + else: + link_names = [os.path.join(user.home, os.path.dirname(link_names)) for user in self.queryset] + validate_paths_exist(self.instance, link_names) + return link_name + + def clean(self): + cleaned_data = super(LinkForm, self).clean() + path = os.path.join(cleaned_data['base_home'], cleaned_data['home_extension']) + try: + validate_paths_exist(self.instance, [path]) + except ValidationError as err: + raise ValidationError({ + 'home_extension': err, + }) + return cleaned_data + + +class PermissionForm(LinkForm): set_action = forms.ChoiceField(label=_("Action"), initial='grant', choices=( ('grant', _("Grant")), @@ -105,29 +154,3 @@ class PermissionForm(forms.Form): ('r', _("Read only")), ('w', _("Write only")) )) - - def __init__(self, *args, **kwargs): - self.instance = args[0] - super_args = [] - if len(args) > 1: - super_args.append(args[1]) - super(PermissionForm, self).__init__(*super_args, **kwargs) - related_users = type(self.instance).objects.filter(account=self.instance.account_id) - self.fields['base_home'].choices = ( - (user.get_base_home(), user.get_base_home()) for user in related_users - ) - - def clean_home_extension(self): - home_extension = self.cleaned_data['home_extension'] - return home_extension.lstrip('/') - - def clean(self): - cleaned_data = super(PermissionForm, self).clean() - path = os.path.join(cleaned_data['base_home'], cleaned_data['home_extension']) - try: - validate_path_exists(self.instance, path) - except ValidationError as err: - raise ValidationError({ - 'home_extension': err, - }) - return cleaned_data diff --git a/orchestra/contrib/systemusers/models.py b/orchestra/contrib/systemusers/models.py index 06cb775d..cbf26728 100644 --- a/orchestra/contrib/systemusers/models.py +++ b/orchestra/contrib/systemusers/models.py @@ -123,12 +123,6 @@ class SystemUser(models.Model): def set_password(self, raw_password): self.password = make_password(raw_password) - def set_permission(self, base_home, extension, perms='rw', action='grant'): - self.set_perm_action = action - self.set_perm_base_home = base_home - self.set_perm_home_extension = extension - self.set_perm_perms = perms - def get_base_home(self): context = { 'user': self.username, diff --git a/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/create_link.html b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/create_link.html new file mode 100644 index 00000000..70816c4f --- /dev/null +++ b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/create_link.html @@ -0,0 +1,74 @@ +{% extends "admin/base_site.html" %} +{% load i18n l10n %} +{% load url from future %} +{% load admin_urls static utils %} + +{% block extrastyle %} +{{ block.super }} + + +{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +
+
+ {% block introduction %} + Create link for {% for user in queryset %}{{ user.username }}{% if not forloop.last %}, {% endif %}{% endfor %}. + {% endblock %} + +
{% csrf_token %} +
+ {{ form.non_field_errors }} + {% block prefields %} + {% endblock %} +
+
+ {{ form.base_home.errors }} + + {{ form.base_home }}{% for x in ""|ljust:"50" %} {% endfor %} +

{{ form.base_home.help_text|safe }}

+
+
+ {{ form.home_extension.errors }} + + {{ form.home_extension }} +

{{ form.home_extension.help_text|safe }}

+
+
+ {% block postfields %} +
+ {{ form.link_name.errors }} + + {{ form.link_name }} +

{{ form.link_name.help_text|safe }}

+
+ {% endblock %} +
+
+ {% for obj in queryset %} + + {% endfor %} + + + +
+
+{% endblock %} + diff --git a/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/set_permission.html b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/set_permission.html index 8570b74f..c9ef2848 100644 --- a/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/set_permission.html +++ b/orchestra/contrib/systemusers/templates/admin/systemusers/systemuser/set_permission.html @@ -1,74 +1,27 @@ -{% extends "admin/base_site.html" %} +{% extends "admin/systemusers/systemuser/create_link.html" %} {% load i18n l10n %} {% load url from future %} {% load admin_urls static utils %} -{% block extrastyle %} -{{ block.super }} - - +{% block introduction %} +Set permissions for {% for user in queryset %}{{ user.username }}{% if not forloop.last %}, {% endif %}{% endfor %} system user(s). {% endblock %} -{% block breadcrumbs %} -