From 9534e6e57166922f964e991fb574d4461f6e15cd Mon Sep 17 00:00:00 2001 From: Marc Date: Thu, 23 Oct 2014 15:38:46 +0000 Subject: [PATCH] Random improvements --- TODO.md | 20 +++++++++++-- orchestra/apps/accounts/admin.py | 13 ++------- orchestra/apps/accounts/api.py | 9 +++++- orchestra/apps/accounts/forms.py | 17 ++++++++--- orchestra/apps/accounts/models.py | 28 +++++++++++++------ orchestra/apps/accounts/settings.py | 13 +++------ orchestra/apps/bills/admin.py | 2 +- orchestra/apps/contacts/admin.py | 11 ++++++-- .../databases/tests/functional_tests/tests.py | 27 +++++++++++++----- orchestra/apps/domains/admin.py | 14 ++++++---- orchestra/apps/domains/backends.py | 4 +-- orchestra/apps/domains/models.py | 3 ++ orchestra/apps/issues/admin.py | 6 +--- orchestra/apps/lists/models.py | 6 ++++ orchestra/apps/lists/settings.py | 3 +- orchestra/apps/mailboxes/admin.py | 6 +++- .../mailboxes/tests/functional_tests/tests.py | 7 ++--- orchestra/apps/miscellaneous/admin.py | 2 +- orchestra/apps/miscellaneous/models.py | 15 ++++++++-- orchestra/apps/orders/models.py | 2 +- orchestra/apps/payments/actions.py | 12 +++++++- orchestra/apps/payments/admin.py | 5 +--- .../payments/transaction/get_processes.html | 3 +- orchestra/apps/resources/admin.py | 15 ++++++---- orchestra/apps/resources/models.py | 19 ++++++++----- orchestra/apps/resources/validators.py | 8 ++++++ orchestra/apps/services/actions.py | 20 ++++++++++++- orchestra/apps/services/admin.py | 8 +++--- orchestra/apps/services/handlers.py | 10 +++++++ orchestra/apps/services/models.py | 15 ++++++++-- .../tests/functional_tests/test_job.py | 3 +- .../tests/functional_tests/test_traffic.py | 2 +- orchestra/apps/systemusers/admin.py | 25 ++++++++++++++--- orchestra/apps/systemusers/api.py | 9 +++++- orchestra/apps/systemusers/filters.py | 23 +++++++++++++++ orchestra/apps/systemusers/models.py | 11 ++++++-- .../tests/functional_tests/tests.py | 15 +++++----- orchestra/apps/webapps/backends/__init__.py | 4 +-- orchestra/apps/webapps/models.py | 6 ++++ orchestra/apps/websites/admin.py | 8 +++--- orchestra/apps/websites/backends/apache.py | 4 +-- orchestra/apps/websites/models.py | 16 +++++++++++ orchestra/utils/system.py | 2 +- orchestra/utils/tests.py | 3 +- 44 files changed, 332 insertions(+), 122 deletions(-) create mode 100644 orchestra/apps/resources/validators.py create mode 100644 orchestra/apps/systemusers/filters.py diff --git a/TODO.md b/TODO.md index 8b75515b..1468bca8 100644 --- a/TODO.md +++ b/TODO.md @@ -12,7 +12,6 @@ TODO * enforce an emergency email contact and account to contact contacts about problems when mailserver is down * add `BackendLog` retry action -* move invoice contact to invoices app? * PHPbBckendMiixin with get_php_ini * Apache: `IncludeOptional /etc/apache2/extra-vhos[t]/account-site-custom.con[f]` * webmail identities and addresses @@ -143,7 +142,7 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * based on a merge set of save(update_fields) -textwrap.dedent( \\) +* textwrap.dedent( \\) * accounts * short name / long name, account name really needed? address? only minimal info.. @@ -159,7 +158,22 @@ textwrap.dedent( \\) * better modeling of the interdependency between webapps and websites (settings) * webapp options cfig agnostic -* Disable menu on tests, fucking overlapping * service.name / verbose_name instead of .description ? +* miscellaneous.name / verbose_name +* service.invoice_name * Bills can have sublines? + +* proforma without billing contact? + +* remove contact addresss, and use invoice contact for it (maybe move to contacts app again) + +* env ORCHESTRA_MASTER_SERVER='test1.orchestra.lan' ORCHESTRA_SECOND_SERVER='test2.orchestra.lan' ORCHESTRA_SLAVE_SERVER='test3.orchestra.lan' python manage.py test orchestra.apps.domains.tests.functional_tests.tests:AdminBind9BackendDomainTest + +* Pangea modifications: domain registered/non-registered list_display and field with register link: inconsistent, what happen to related objects with a domain that is converted to register-only? + +* ForeignKey.swappable +* Field.editable +* ManyToManyField.symmetrical = False (user group) + +* REST PERMISSIONS diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index a3f261bb..3ea1bdc8 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -63,6 +63,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) change_form_template = 'admin/accounts/account/change_form.html' actions = [disable] change_view_actions = actions + list_select_related = ('billcontact',) def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ @@ -99,12 +100,6 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) fieldsets.insert(1, (_("Related services"), {'fields': fields})) return fieldsets - def get_queryset(self, request): - """ Select related for performance """ - qs = super(AccountAdmin, self).get_queryset(request) - related = ('invoicecontact',) - return qs.select_related(*related) - def save_model(self, request, obj, form, change): super(AccountAdmin, self).save_model(request, obj, form, change) if not change: @@ -152,6 +147,7 @@ class AccountAdminMixin(object): change_list_template = 'admin/accounts/account/change_list.html' change_form_template = 'admin/accounts/account/change_form.html' account = None + list_select_related = ('account',) def account_link(self, instance): account = instance.account if instance.pk else self.account @@ -167,11 +163,6 @@ class AccountAdminMixin(object): self.account = obj.account return super(AccountAdminMixin, self).get_readonly_fields(request, obj=obj) - def get_queryset(self, request): - """ Select related for performance """ - qs = super(AccountAdminMixin, self).get_queryset(request) - return qs.select_related('account') - def formfield_for_dbfield(self, db_field, **kwargs): """ Filter by account """ formfield = super(AccountAdminMixin, self).formfield_for_dbfield(db_field, **kwargs) diff --git a/orchestra/apps/accounts/api.py b/orchestra/apps/accounts/api.py index 6ee24660..24b82db7 100644 --- a/orchestra/apps/accounts/api.py +++ b/orchestra/apps/accounts/api.py @@ -1,4 +1,5 @@ -from rest_framework import viewsets +from django.utils.translation import ugettext_lazy as _ +from rest_framework import viewsets, exceptions from orchestra.api import router, SetPasswordApiMixin @@ -20,6 +21,12 @@ class AccountViewSet(SetPasswordApiMixin, viewsets.ModelViewSet): def get_queryset(self): qs = super(AccountViewSet, self).get_queryset() return qs.filter(id=self.request.user.pk) + + def destroy(self, request, pk=None): + # TODO reimplement in permissions + if not request.user.is_superuser: + raise exceptions.PermissionDenied(_("Accounts can not be deleted.")) + super(AccountViewSet, self).destroy(request, pk=pk) router.register(r'accounts', AccountViewSet) diff --git a/orchestra/apps/accounts/forms.py b/orchestra/apps/accounts/forms.py index c9d19a8f..79ddfde1 100644 --- a/orchestra/apps/accounts/forms.py +++ b/orchestra/apps/accounts/forms.py @@ -9,7 +9,12 @@ from .models import Account def create_account_creation_form(): - fields = {} + fields = { + 'create_systemuser': forms.BooleanField(initial=True, required=False, + label=_("Create systemuser"), widget=forms.CheckboxInput(attrs={'disabled': True}), + help_text=_("Designates whether to creates a related system user with the same " + "username and password or not.")) + } for model, key, kwargs, help_text in settings.ACCOUNTS_CREATE_RELATED: model = get_model(model) field_name = 'create_%s' % model._meta.model_name @@ -28,6 +33,9 @@ def create_account_creation_form(): except KeyError: # Previous validation error return + systemuser_model = Account.main_systemuser.field.rel.to + if systemuser_model.objects.filter(username=account.username).exists(): + raise forms.ValidationError(_("A system user with this name already exists")) for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: model = get_model(model) kwargs = { @@ -44,9 +52,10 @@ def create_account_creation_form(): model = get_model(model) field_name = 'create_%s' % model._meta.model_name if self.cleaned_data[field_name]: - for key, value in related_kwargs.iteritems(): - related_kwargs[key] = eval(value, {'account': account}) - model.objects.create(account=account, **related_kwargs) + kwargs = { + key: eval(value, {'account': account}) for key, value in related_kwargs.iteritems() + } + model.objects.create(account=account, **kwargs) fields.update({ 'create_related_fields': fields.keys(), diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py index 1104857d..28d404f0 100644 --- a/orchestra/apps/accounts/models.py +++ b/orchestra/apps/accounts/models.py @@ -17,6 +17,8 @@ class Account(auth.AbstractBaseUser): help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."), validators=[validators.RegexValidator(r'^[\w.-]+$', _("Enter a valid username."), 'invalid')]) + main_systemuser = models.ForeignKey(settings.ACCOUNTS_SYSTEMUSER_MODEL, null=True, + related_name='accounts_main') first_name = models.CharField(_("first name"), max_length=30, blank=True) last_name = models.CharField(_("last name"), max_length=30, blank=True) email = models.EmailField(_('email address'), help_text=_("Used for password recovery")) @@ -50,14 +52,22 @@ class Account(auth.AbstractBaseUser): def is_staff(self): return self.is_superuser - @property - def main_systemuser(self): - return self.systemusers.get(is_main=True) +# @property +# def main_systemuser(self): +# return self.systemusers.get(is_main=True) @classmethod def get_main(cls): return cls.objects.get(pk=settings.ACCOUNTS_MAIN_PK) + def save(self, *args, **kwargs): + created = not self.pk + super(Account, self).save(*args, **kwargs) + if created: + self.main_systemuser = self.systemusers.create(account=self, username=self.username, + password=self.password) + self.save(update_fields=['main_systemuser']) + def clean(self): self.first_name = self.first_name.strip() self.last_name = self.last_name.strip() @@ -124,15 +134,17 @@ class Account(auth.AbstractBaseUser): if self.is_active and self.is_superuser: return True return auth._user_has_module_perms(self, app_label) - + def get_related_passwords(self): - related = [] - for model, key, kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: - if 'password' not in kwargs: + related = [ + self.main_systemuser, + ] + for model, key, related_kwargs, __ in settings.ACCOUNTS_CREATE_RELATED: + if 'password' not in related_kwargs: continue model = get_model(model) kwargs = { - key: eval(kwargs[key], {'account': self}) + key: eval(related_kwargs[key], {'account': self}) } try: rel = model.objects.get(account=self, **kwargs) diff --git a/orchestra/apps/accounts/settings.py b/orchestra/apps/accounts/settings.py index c4dd4730..efdac9db 100644 --- a/orchestra/apps/accounts/settings.py +++ b/orchestra/apps/accounts/settings.py @@ -18,6 +18,10 @@ ACCOUNTS_LANGUAGES = getattr(settings, 'ACCOUNTS_LANGUAGES', ( )) +ACCOUNTS_SYSTEMUSER_MODEL = getattr(settings, 'ACCOUNTS_SYSTEMUSER_MODEL', + 'systemusers.SystemUser') + + ACCOUNTS_DEFAULT_LANGUAGE = getattr(settings, 'ACCOUNTS_DEFAULT_LANGUAGE', 'en') @@ -26,15 +30,6 @@ ACCOUNTS_MAIN_PK = getattr(settings, 'ACCOUNTS_MAIN_PK', 1) ACCOUNTS_CREATE_RELATED = getattr(settings, 'ACCOUNTS_CREATE_RELATED', ( # , , , - ('systemusers.SystemUser', - 'username', - { - 'username': 'account.username', - 'password': 'account.password', - 'is_main': 'True', - }, - _("Designates whether to creates a related system user with the same username and password or not."), - ), ('mailboxes.Mailbox', 'name', { diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py index cbdcddb9..049baf63 100644 --- a/orchestra/apps/bills/admin.py +++ b/orchestra/apps/bills/admin.py @@ -36,7 +36,7 @@ class BillLineInline(admin.TabularInline): if sublines: content = '\n'.join(['%s: %s' % (sub.description, sub.total) for sub in sublines]) img = static('admin/img/icon_alert.gif') - return '%s' % (content, str(total), img) + return '%s ' % (content, str(total), img) return total display_total.short_description = _("Total") display_total.allow_tags = True diff --git a/orchestra/apps/contacts/admin.py b/orchestra/apps/contacts/admin.py index 8ddb85b8..0c8feeb9 100644 --- a/orchestra/apps/contacts/admin.py +++ b/orchestra/apps/contacts/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin from django.utils.translation import ugettext, ugettext_lazy as _ from orchestra.admin import AtLeastOneRequiredInlineFormSet -from orchestra.admin.utils import insertattr +from orchestra.admin.utils import insertattr, admin_link, change_url from orchestra.apps.accounts.admin import AccountAdmin, AccountAdminMixin from orchestra.forms.widgets import paddingCheckboxSelectMultiple @@ -74,15 +74,20 @@ class ContactInline(admin.StackedInline): formset = AtLeastOneRequiredInlineFormSet extra = 0 fields = ( - 'short_name', 'full_name', 'email', 'email_usage', ('phone', 'phone2'), - 'address', ('city', 'zipcode'), 'country', + ('short_name', 'full_name'), 'email', 'email_usage', ('phone', 'phone2'), ) def get_extra(self, request, obj=None, **kwargs): return 0 if obj and obj.contacts.exists() else 1 + def get_view_on_site_url(self, obj=None): + if obj: + return change_url(obj) + def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ + if db_field.name == 'short_name': + kwargs['widget'] = forms.TextInput(attrs={'size':'15'}) if db_field.name == 'address': kwargs['widget'] = forms.Textarea(attrs={'cols': 70, 'rows': 2}) if db_field.name == 'email_usage': diff --git a/orchestra/apps/databases/tests/functional_tests/tests.py b/orchestra/apps/databases/tests/functional_tests/tests.py index 72b54479..4af3b0a8 100644 --- a/orchestra/apps/databases/tests/functional_tests/tests.py +++ b/orchestra/apps/databases/tests/functional_tests/tests.py @@ -296,10 +296,13 @@ class AdminDatabaseMixin(DatabaseTestMixin): self.selenium.get(url) user = DatabaseUser.objects.get(username=username, type=self.db_type) - users_input = self.selenium.find_element_by_id('id_users') - users_select = Select(users_input) + users_from = self.selenium.find_element_by_id('id_users_from') + users_select = Select(users_from) users_select.select_by_value(str(user.pk)) + add_user = self.selenium.find_element_by_id('id_users_add_link') + add_user.click() + save = self.selenium.find_element_by_name('_save') save.submit() self.assertNotEqual(url, self.selenium.current_url) @@ -310,13 +313,23 @@ class AdminDatabaseMixin(DatabaseTestMixin): url = self.live_server_url + change_url(database) self.selenium.get(url) + # remove user "username" user = DatabaseUser.objects.get(username=username, type=self.db_type) - users_input = self.selenium.find_element_by_id('id_users') - users_select = Select(users_input) - users_select.deselect_by_value(str(user.pk)) - - user = DatabaseUser.objects.get(username=username2, type=self.db_type) + users_to = self.selenium.find_element_by_id('id_users_to') + users_select = Select(users_to) users_select.select_by_value(str(user.pk)) + remove_user = self.selenium.find_element_by_id('id_users_remove_link') + remove_user.click() + time.sleep(0.2) + + # add user "username2" + user = DatabaseUser.objects.get(username=username2, type=self.db_type) + users_from = self.selenium.find_element_by_id('id_users_from') + users_select = Select(users_from) + users_select.select_by_value(str(user.pk)) + add_user = self.selenium.find_element_by_id('id_users_add_link') + add_user.click() + time.sleep(0.2) save = self.selenium.find_element_by_name('_save') save.submit() diff --git a/orchestra/apps/domains/admin.py b/orchestra/apps/domains/admin.py index 9dc6fb39..9ee0ccac 100644 --- a/orchestra/apps/domains/admin.py +++ b/orchestra/apps/domains/admin.py @@ -20,19 +20,22 @@ class RecordInline(admin.TabularInline): formset = RecordInlineFormSet verbose_name_plural = _("Extra records") - class Media: - css = { - 'all': ('orchestra/css/hide-inline-id.css',) - } - +# class Media: +# css = { +# 'all': ('orchestra/css/hide-inline-id.css',) +# } +# def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ if db_field.name == 'value': kwargs['widget'] = forms.TextInput(attrs={'size':'100'}) + if db_field.name == 'ttl': + kwargs['widget'] = forms.TextInput(attrs={'size':'10'}) return super(RecordInline, self).formfield_for_dbfield(db_field, **kwargs) class DomainInline(admin.TabularInline): + # TODO account, and record sumary fields model = Domain fields = ('domain_link',) readonly_fields = ('domain_link',) @@ -47,6 +50,7 @@ class DomainInline(admin.TabularInline): class DomainAdmin(ChangeListDefaultFilter, AccountAdminMixin, ExtendedModelAdmin): + # TODO name link fields = ('name', 'account') list_display = ( 'structured_name', 'display_is_top', 'websites', 'account_link' diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index cc263a3d..156fe05b 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -91,7 +91,7 @@ class Bind9MasterDomainBackend(ServiceController): 'zone_path': settings.DOMAINS_ZONE_PATH % {'name': domain.name}, 'subdomains': domain.subdomains.all(), 'banner': self.get_banner(), - 'slaves': '; '.join(self.get_slaves(domain)) or '"none"', + 'slaves': '; '.join(self.get_slaves(domain)) or '', } context.update({ 'conf_path': settings.DOMAINS_MASTERS_PATH, @@ -133,7 +133,7 @@ class Bind9SlaveDomainBackend(Bind9MasterDomainBackend): 'name': domain.name, 'banner': self.get_banner(), 'subdomains': domain.subdomains.all(), - 'masters': '; '.join(self.get_masters(domain)) or '"none"', + 'masters': '; '.join(self.get_masters(domain)) or '', } context.update({ 'conf_path': settings.DOMAINS_SLAVES_PATH, diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 1b40eb99..fb76471f 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -42,6 +42,9 @@ class Domain(models.Model): # don't cache, don't replace by top_id return not bool(self.top) + def get_absolute_url(self): + return 'http://%s' % self.name + def get_records(self): """ proxy method, needed for input validation, see helpers.domain_for_validation """ return self.records.all() diff --git a/orchestra/apps/issues/admin.py b/orchestra/apps/issues/admin.py index 80096cca..7f34fb98 100644 --- a/orchestra/apps/issues/admin.py +++ b/orchestra/apps/issues/admin.py @@ -180,6 +180,7 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): 'description') }), ) + list_select_related = ('queue', 'owner', 'creator') class Media: css = { @@ -285,11 +286,6 @@ class TicketAdmin(ChangeListDefaultFilter, ExtendedModelAdmin): data = request.POST.get("data") data_formated = markdown(strip_tags(data)) return HttpResponse(data_formated) - - def get_queryset(self, request): - """ Order by structured name and imporve performance """ - qs = super(TicketAdmin, self).get_queryset(request) - return qs.select_related('queue', 'owner', 'creator') class QueueAdmin(admin.ModelAdmin): diff --git a/orchestra/apps/lists/models.py b/orchestra/apps/lists/models.py index 1c0b69e4..0706f6e2 100644 --- a/orchestra/apps/lists/models.py +++ b/orchestra/apps/lists/models.py @@ -43,6 +43,12 @@ class List(models.Model): def set_password(self, password): self.password = password + + def get_absolute_url(self): + context = { + 'name': self.name + } + return settings.LISTS_LIST_URL % context services.register(List) diff --git a/orchestra/apps/lists/settings.py b/orchestra/apps/lists/settings.py index a34c0180..794466c6 100644 --- a/orchestra/apps/lists/settings.py +++ b/orchestra/apps/lists/settings.py @@ -7,10 +7,11 @@ LISTS_DOMAIN_MODEL = getattr(settings, 'LISTS_DOMAIN_MODEL', 'domains.Domain') LISTS_DEFAULT_DOMAIN = getattr(settings, 'LIST_DEFAULT_DOMAIN', 'lists.orchestra.lan') +LISTS_LIST_URL = getattr(settings, 'LISTS_LIST_URL', 'https://lists.orchestra.lan/mailman/listinfo/%(name)s') + LISTS_MAILMAN_POST_LOG_PATH = getattr(settings, 'LISTS_MAILMAN_POST_LOG_PATH', '/var/log/mailman/post') - LISTS_MAILMAN_ROOT_PATH = getattr(settings, 'LISTS_MAILMAN_ROOT_PATH', '/var/lib/mailman/') diff --git a/orchestra/apps/mailboxes/admin.py b/orchestra/apps/mailboxes/admin.py index 209110b6..4b10fbba 100644 --- a/orchestra/apps/mailboxes/admin.py +++ b/orchestra/apps/mailboxes/admin.py @@ -70,11 +70,15 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo display_addresses.allow_tags = True def get_fieldsets(self, request, obj=None): - """ not collapsed filtering when exists """ fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj) if obj and obj.filtering == obj.CUSTOM: + # not collapsed filtering when exists fieldsets = copy.deepcopy(fieldsets) fieldsets[1][1]['classes'] = fieldsets[0][1]['fields'] + ('open',) + elif '_to_field' in parse_qs(request.META['QUERY_STRING']): + # remove address from popup + fieldsets = list(copy.deepcopy(fieldsets)) + fieldsets.pop(-1) return fieldsets def get_form(self, *args, **kwargs): diff --git a/orchestra/apps/mailboxes/tests/functional_tests/tests.py b/orchestra/apps/mailboxes/tests/functional_tests/tests.py index 136113c6..6ece4a56 100644 --- a/orchestra/apps/mailboxes/tests/functional_tests/tests.py +++ b/orchestra/apps/mailboxes/tests/functional_tests/tests.py @@ -14,7 +14,6 @@ from django.core.management.base import CommandError from django.core.urlresolvers import reverse from selenium.webdriver.support.select import Select -from orchestra.apps.accounts.models import Account from orchestra.apps.orchestration.models import Server, Route from orchestra.apps.resources.models import Resource from orchestra.utils.system import run, sshrun @@ -303,9 +302,9 @@ class AdminMailboxMixin(MailboxMixin): url = self.live_server_url + reverse('admin:mailboxes_mailbox_add') self.selenium.get(url) - account_input = self.selenium.find_element_by_id('id_account') - account_select = Select(account_input) - account_select.select_by_value(str(self.account.pk)) +# account_input = self.selenium.find_element_by_id('id_account') +# account_select = Select(account_input) +# account_select.select_by_value(str(self.account.pk)) name_field = self.selenium.find_element_by_id('id_name') name_field.send_keys(username) diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py index 0221a611..a5bae270 100644 --- a/orchestra/apps/miscellaneous/admin.py +++ b/orchestra/apps/miscellaneous/admin.py @@ -10,7 +10,7 @@ from .models import MiscService, Miscellaneous class MiscServiceAdmin(admin.ModelAdmin): - list_display = ('name', 'num_instances') + list_display = ('name', 'verbose_name', 'num_instances') def num_instances(self, misc): """ return num slivers as a link to slivers changelist view """ diff --git a/orchestra/apps/miscellaneous/models.py b/orchestra/apps/miscellaneous/models.py index 7b4f82fd..373ea7d5 100644 --- a/orchestra/apps/miscellaneous/models.py +++ b/orchestra/apps/miscellaneous/models.py @@ -3,11 +3,16 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import services +from orchestra.core.validators import validate_name class MiscService(models.Model): - name = models.CharField(_("name"), max_length=256) - description = models.TextField(_("description"), blank=True) + name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name], + help_text=_("Raw name used for internal referenciation, i.e. service match definition")) + verbose_name = models.CharField(_("verbose name"), max_length=256, blank=True, + help_text=_("Human readable name")) + description = models.TextField(_("description"), blank=True, + help_text=_("Optional description")) has_amount = models.BooleanField(_("has amount"), default=False, help_text=_("Designates whether this service has amount " "property or not.")) @@ -17,6 +22,12 @@ class MiscService(models.Model): def __unicode__(self): return self.name + + def clean(self): + self.verbose_name = self.verbose_name.strip() + + def get_verbose_name(self): + return self.verbose_name or self.name class Miscellaneous(models.Model): diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 38994cf7..fe87735d 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -181,7 +181,7 @@ class Order(models.Model): if metric is not None: MetricStorage.store(self, metric) metric = ', metric:{}'.format(metric) - description = "{}: {}".format(handler.description, str(instance)) + description = handler.get_order_description(instance) logger.info("UPDATED order id:{id}, description:{description}{metric}".format( id=self.id, description=description, metric=metric)) if self.description != description: diff --git a/orchestra/apps/payments/actions.py b/orchestra/apps/payments/actions.py index 9d702895..25741b8b 100644 --- a/orchestra/apps/payments/actions.py +++ b/orchestra/apps/payments/actions.py @@ -31,8 +31,18 @@ def process_transactions(modeladmin, request, queryset): if not processes: return opts = modeladmin.model._meta + num = len(queryset) context = { - 'title': _("Huston, be advised"), + 'title': ungettext( + _("Selected transaction has been processed."), + _("%s Selected transactions have been processed.") % num, + num), + 'content_message': ungettext( + _("The following transaction process has been generated, " + "you may want to save it on your computer now."), + _("The following %s transaction processes have been generated, " + "you may want to save it on your computer now.") % len(processes), + len(processes)), 'action_name': _("Process"), 'processes': processes, 'opts': opts, diff --git a/orchestra/apps/payments/admin.py b/orchestra/apps/payments/admin.py index f5eb98ef..99eeeb9f 100644 --- a/orchestra/apps/payments/admin.py +++ b/orchestra/apps/payments/admin.py @@ -92,6 +92,7 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): filter_by_account_fields = ('bill', 'source') change_readonly_fields = ('amount', 'currency') readonly_fields = ('bill_link', 'display_state', 'process_link', 'account_link', 'source_link') + list_select_related = ('account', 'source', 'bill__account') bill_link = admin_link('bill') source_link = admin_link('source') @@ -99,10 +100,6 @@ class TransactionAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): account_link = admin_link('bill__account') display_state = admin_colored('state', colors=STATE_COLORS) - def get_queryset(self, request): - qs = super(TransactionAdmin, self).get_queryset(request) - return qs.select_related('source', 'bill__account') - def get_change_view_actions(self, obj=None): actions = super(TransactionAdmin, self).get_change_view_actions() exclude = [] diff --git a/orchestra/apps/payments/templates/admin/payments/transaction/get_processes.html b/orchestra/apps/payments/templates/admin/payments/transaction/get_processes.html index dff04aef..05c2f8a7 100644 --- a/orchestra/apps/payments/templates/admin/payments/transaction/get_processes.html +++ b/orchestra/apps/payments/templates/admin/payments/transaction/get_processes.html @@ -1,8 +1,9 @@ {% extends "admin/orchestra/generic_confirmation.html" %} {% load i18n admin_urls utils %} + {% block content %} -

The following transaction processes have been generated, you may want to save them on your computer now.

+

{{ content_message }}

    {% for proc in processes %}
  • Process #{{ proc.id }} diff --git a/orchestra/apps/resources/admin.py b/orchestra/apps/resources/admin.py index 4fff58d6..90925a3e 100644 --- a/orchestra/apps/resources/admin.py +++ b/orchestra/apps/resources/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin, messages from django.contrib.contenttypes import generic from django.utils.functional import cached_property +from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin @@ -14,6 +15,8 @@ from .models import Resource, ResourceData, MonitorData class ResourceAdmin(ExtendedModelAdmin): + # TODO error after saving: u"Key 'name' not found in 'ResourceForm'" +# prepopulated_fields = {'name': ('verbose_name',)} list_display = ( 'id', 'verbose_name', 'content_type', 'period', 'on_demand', 'default_allocation', 'unit', 'disable_trigger', 'crontab', @@ -21,11 +24,11 @@ class ResourceAdmin(ExtendedModelAdmin): list_filter = (UsedContentTypeFilter, 'period', 'on_demand', 'disable_trigger') fieldsets = ( (None, { - 'fields': ('name', 'content_type', 'period'), + 'fields': ('verbose_name', 'name', 'content_type', 'period'), }), (_("Configuration"), { - 'fields': ('verbose_name', 'unit', 'scale', 'on_demand', - 'default_allocation', 'disable_trigger', 'is_active'), + 'fields': ('unit', 'scale', 'on_demand', 'default_allocation', 'disable_trigger', + 'is_active'), }), (_("Monitoring"), { 'fields': ('monitors', 'crontab'), @@ -36,10 +39,10 @@ class ResourceAdmin(ExtendedModelAdmin): def add_view(self, request, **kwargs): """ Warning user if the node is not fully configured """ if request.method == 'POST': - messages.warning(request, _( - "Restarting orchestra and celerybeat is required to fully apply changes. " + messages.warning(request, mark_safe(_( + "Restarting orchestra and celerybeat is required to fully apply changes.
    " "Remember that new allocated values will be applied when objects are saved." - )) + ))) return super(ResourceAdmin, self).add_view(request, **kwargs) def save_model(self, request, obj, form, change): diff --git a/orchestra/apps/resources/models.py b/orchestra/apps/resources/models.py index 2a5e907f..45d00ad9 100644 --- a/orchestra/apps/resources/models.py +++ b/orchestra/apps/resources/models.py @@ -8,9 +8,12 @@ from djcelery.models import PeriodicTask, CrontabSchedule from orchestra.core import validators from orchestra.models import queryset, fields +from orchestra.utils.paths import get_project_root +from orchestra.utils.system import run from . import helpers from .backends import ServiceMonitor +from .validators import validate_scale class ResourceQuerySet(models.QuerySet): @@ -34,16 +37,15 @@ class Resource(models.Model): _related = set() # keeps track of related models for resource cleanup name = models.CharField(_("name"), max_length=32, - help_text=_('Required. 32 characters or fewer. Lowercase letters, ' - 'digits and hyphen only.'), + help_text=_("Required. 32 characters or fewer. Lowercase letters, " + "digits and hyphen only."), validators=[validators.validate_name]) verbose_name = models.CharField(_("verbose name"), max_length=256) content_type = models.ForeignKey(ContentType, help_text=_("Model where this resource will be hooked.")) period = models.CharField(_("period"), max_length=16, choices=PERIODS, default=LAST, - help_text=_("Operation used for aggregating this resource monitored" - "data.")) + help_text=_("Operation used for aggregating this resource monitored data.")) on_demand = models.BooleanField(_("on demand"), default=False, help_text=_("If enabled the resource will not be pre-allocated, " "but allocated under the application demand")) @@ -53,8 +55,8 @@ class Resource(models.Model): "on demand resource")) unit = models.CharField(_("unit"), max_length=16, help_text=_("The unit in which this resource is measured. " - "For example GB, KB or subscribers")) - scale = models.PositiveIntegerField(_("scale"), + "For example GB, KB or subscribers")) + scale = models.CharField(_("scale"), max_length=32, validators=[validate_scale], help_text=_("Scale in which this resource monitoring resoults should " "be prorcessed to match with unit. e.g. 10**9")) disable_trigger = models.BooleanField(_("disable trigger"), default=False, @@ -79,6 +81,9 @@ class Resource(models.Model): def __unicode__(self): return "{}-{}".format(str(self.content_type), self.name) + def clean(self): + self.verbose_name = self.verbose_name.strip() + def save(self, *args, **kwargs): created = not self.pk super(Resource, self).save(*args, **kwargs) @@ -102,7 +107,7 @@ class Resource(models.Model): task.save(update_fields=['crontab']) # This only work on tests (multiprocessing used on real deployments) apps.get_app_config('resources').reload_relations() - # TODO touch wsgi.py for code reloading? + run('touch %s/wsgi.py' % get_project_root()) def delete(self, *args, **kwargs): super(Resource, self).delete(*args, **kwargs) diff --git a/orchestra/apps/resources/validators.py b/orchestra/apps/resources/validators.py new file mode 100644 index 00000000..2a75aba9 --- /dev/null +++ b/orchestra/apps/resources/validators.py @@ -0,0 +1,8 @@ +from django.core.validators import ValidationError + + +def validate_scale(value): + try: + int(eval(value)) + except ValueError: + raise ValidationError(_("%s value is not a valid scale expression")) diff --git a/orchestra/apps/services/actions.py b/orchestra/apps/services/actions.py index 9fb27ec8..526bfd2f 100644 --- a/orchestra/apps/services/actions.py +++ b/orchestra/apps/services/actions.py @@ -1,7 +1,7 @@ from django.contrib.admin import helpers from django.core.urlresolvers import reverse from django.db import transaction -from django.shortcuts import render +from django.shortcuts import render, redirect from django.template.response import TemplateResponse from django.utils.safestring import mark_safe from django.utils.translation import ugettext_lazy as _ @@ -62,3 +62,21 @@ def view_help(modeladmin, request, queryset): return TemplateResponse(request, 'admin/services/service/help.html', context) view_help.url_name = 'help' view_help.verbose_name = _("Help") + + +def clone(modeladmin, request, queryset): + service = queryset.get() + fields = ( + 'content_type_id', 'match', 'handler_type', 'is_active', 'ignore_superusers', 'billing_period', + 'billing_point', 'is_fee', 'metric', 'nominal_price', 'tax', 'pricing_period', + 'rate_algorithm', 'on_cancel', 'payment_style', + ) + query = [] + for field in fields: + value = getattr(service, field) + field = field.replace('_id', '') + query.append('%s=%s' % (field, value)) + opts = service._meta + url = reverse('admin:%s_%s_add' % (opts.app_label, opts.model_name)) + url += '?%s' % '&'.join(query) + return redirect(url) diff --git a/orchestra/apps/services/admin.py b/orchestra/apps/services/admin.py index 811769fa..6e645abf 100644 --- a/orchestra/apps/services/admin.py +++ b/orchestra/apps/services/admin.py @@ -9,7 +9,7 @@ from orchestra.admin.filters import UsedContentTypeFilter from orchestra.apps.accounts.admin import AccountAdminMixin from orchestra.core import services -from .actions import update_orders, view_help +from .actions import update_orders, view_help, clone from .models import Plan, ContractedPlan, Rate, Service @@ -42,7 +42,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): }), (_("Billing options"), { 'classes': ('wide',), - 'fields': ('billing_period', 'billing_point', 'is_fee') + 'fields': ('billing_period', 'billing_point', 'is_fee', 'order_description') }), (_("Pricing options"), { 'classes': ('wide',), @@ -51,7 +51,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): }), ) inlines = [RateInline] - actions = [update_orders] + actions = [update_orders, clone] change_view_actions = actions + [view_help] def formfield_for_dbfield(self, db_field, **kwargs): @@ -60,7 +60,7 @@ class ServiceAdmin(ChangeViewActionsMixin, admin.ModelAdmin): models = [model._meta.model_name for model in services.get()] queryset = db_field.rel.to.objects kwargs['queryset'] = queryset.filter(model__in=models) - if db_field.name in ['match', 'metric']: + if db_field.name in ['match', 'metric', 'order_description']: kwargs['widget'] = forms.TextInput(attrs={'size':'160'}) return super(ServiceAdmin, self).formfield_for_dbfield(db_field, **kwargs) diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index 2d39ca5c..5aeeeb75 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -62,6 +62,16 @@ class ServiceHandler(plugins.Plugin): } return eval(self.metric, safe_locals) + def get_order_description(self, instance): + safe_locals = { + 'instance': instance, + 'obj': instance, + instance._meta.model_name: instance, + } + if not self.order_description: + return '%s: %s' % (self.description, instance) + return eval(self.order_description, safe_locals) + def get_billing_point(self, order, bp=None, **options): not_cachable = self.billing_point == self.FIXED_DATE and options.get('fixed_point') if not_cachable or bp is None: diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 8e9f9c02..6b1099d8 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -10,6 +10,7 @@ from django.utils.module_loading import autodiscover_modules from django.utils.translation import ugettext_lazy as _ from orchestra.core import caches, services, accounts +from orchestra.core.validators import validate_name from orchestra.models import queryset from . import settings, rating @@ -17,7 +18,8 @@ from .handlers import ServiceHandler class Plan(models.Model): - name = models.CharField(_("plan"), max_length=128) + name = models.CharField(_("name"), max_length=32, unique=True, validators=[validate_name]) + verbose_name = models.CharField(_("verbose_name"), max_length=128, blank=True) is_default = models.BooleanField(_("default"), default=False, help_text=_("Designates whether this plan is used by default or not.")) is_combinable = models.BooleanField(_("combinable"), default=True, @@ -29,7 +31,10 @@ class Plan(models.Model): return self.name def clean(self): - self.name = self.name.strip() + self.verbose_name = self.verbose_name.strip() + + def get_verbose_name(self): + return self.verbose_name or self.name class ContractedPlan(models.Model): @@ -147,6 +152,12 @@ class Service(models.Model): is_fee = models.BooleanField(_("fee"), default=False, help_text=_("Designates whether this service should be billed as " " membership fee or not")) + order_description = models.CharField(_("Order description"), max_length=128, blank=True, + help_text=_( + "Python expression " + "used for generating the description for the bill lines of this services.
    " + "Defaults to '%s: %s' % (handler.description, instance)" + )) # Pricing metric = models.CharField(_("metric"), max_length=256, blank=True, help_text=_( diff --git a/orchestra/apps/services/tests/functional_tests/test_job.py b/orchestra/apps/services/tests/functional_tests/test_job.py index b5b9f57e..47f4249b 100644 --- a/orchestra/apps/services/tests/functional_tests/test_job.py +++ b/orchestra/apps/services/tests/functional_tests/test_job.py @@ -34,8 +34,7 @@ class JobBillingTest(BaseBillingTest): if not account: account = self.create_account() description = 'Random Job %s' % random_ascii(10) - service, __ = MiscService.objects.get_or_create(name='job', description=description, - has_amount=True) + service, __ = MiscService.objects.get_or_create(name='job', has_amount=True) return account.miscellaneous.create(service=service, description=description, amount=amount) def test_job(self): diff --git a/orchestra/apps/services/tests/functional_tests/test_traffic.py b/orchestra/apps/services/tests/functional_tests/test_traffic.py index d6e5f0a4..30740214 100644 --- a/orchestra/apps/services/tests/functional_tests/test_traffic.py +++ b/orchestra/apps/services/tests/functional_tests/test_traffic.py @@ -43,7 +43,7 @@ class BaseTrafficBillingTest(BaseBillingTest): period=Resource.MONTHLY_SUM, verbose_name='Account Traffic', unit='GB', - scale=10**9, + scale='10**9', on_demand=True, monitors='FTPTraffic', ) diff --git a/orchestra/apps/systemusers/admin.py b/orchestra/apps/systemusers/admin.py index a4f6a2d6..6d6a7f3c 100644 --- a/orchestra/apps/systemusers/admin.py +++ b/orchestra/apps/systemusers/admin.py @@ -4,18 +4,20 @@ from django.contrib import admin from django.contrib.admin.util import unquote from django.contrib.auth.admin import UserAdmin from django.utils.translation import ugettext, ugettext_lazy as _ +from django.utils.safestring import mark_safe from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin from orchestra.admin.utils import wrap_admin_view from orchestra.apps.accounts.admin import SelectAccountAdminMixin from orchestra.forms import UserCreationForm, UserChangeForm +from .filters import IsMainListFilter from .models import SystemUser class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin): - list_display = ('username', 'account_link', 'shell', 'home', 'is_active', 'is_main') - list_filter = ('is_active', 'is_main', 'shell') + list_display = ('username', 'account_link', 'shell', 'home', 'display_active', 'display_main') + list_filter = ('is_active', 'shell', IsMainListFilter) fieldsets = ( (None, { 'fields': ('username', 'password', 'account_link', 'is_active') @@ -26,7 +28,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende ) add_fieldsets = ( (None, { - 'fields': ('username', 'password1', 'password2', 'account') + 'fields': ('account_link', 'username', 'password1', 'password2') }), (_("System"), { 'fields': ('home', 'shell', 'groups'), @@ -41,6 +43,17 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende form = UserChangeForm ordering = ('-id',) + def display_active(self, user): + return user.active + display_active.short_description = _("Active") + display_active.admin_order_field = 'is_active' + display_active.boolean = True + + def display_main(self, user): + return user.is_main + display_main.short_description = _("Main") + display_main.boolean = True + def get_form(self, request, obj=None, **kwargs): """ exclude self reference on groups """ form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs) @@ -51,6 +64,10 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende formfield = form.base_fields['groups'] formfield.queryset = formfield.queryset.exclude(id=obj.id) return form - + + def has_delete_permission(self, request, obj=None): + if obj and obj.is_main: + return False + return super(SystemUserAdmin, self).has_delete_permission(request, obj=obj) admin.site.register(SystemUser, SystemUserAdmin) diff --git a/orchestra/apps/systemusers/api.py b/orchestra/apps/systemusers/api.py index 11d4aa47..7e423f9b 100644 --- a/orchestra/apps/systemusers/api.py +++ b/orchestra/apps/systemusers/api.py @@ -1,4 +1,5 @@ -from rest_framework import viewsets +from django.utils.translation import ugettext_lazy as _ +from rest_framework import viewsets, exceptions from orchestra.api import router, SetPasswordApiMixin from orchestra.apps.accounts.api import AccountApiMixin @@ -11,6 +12,12 @@ class SystemUserViewSet(AccountApiMixin, SetPasswordApiMixin, viewsets.ModelView model = SystemUser serializer_class = SystemUserSerializer filter_fields = ('username',) + + def destroy(self, request, pk=None): + user = self.get_object() + if user.is_main: + raise exceptions.PermissionDenied(_("Main system user can not be deleted.")) + super(SystemUserViewSet, self).destroy(request, pk=pk) router.register(r'systemusers', SystemUserViewSet) diff --git a/orchestra/apps/systemusers/filters.py b/orchestra/apps/systemusers/filters.py new file mode 100644 index 00000000..81fb1143 --- /dev/null +++ b/orchestra/apps/systemusers/filters.py @@ -0,0 +1,23 @@ +from django.contrib.admin import SimpleListFilter +from django.db.models import F +from django.utils.encoding import force_text +from django.utils.translation import ugettext_lazy as _ + + +class IsMainListFilter(SimpleListFilter): + """ Filter Nodes by group according to request.user """ + title = _("main") + parameter_name = 'is_main' + + def lookups(self, request, model_admin): + return ( + ('True', _("Yes")), + ('False', _("No")), + ) + + def queryset(self, request, queryset): + if self.value() == 'True': + return queryset.filter(account__main_systemuser_id=F('id')) + if self.value() == 'False': + return queryset.exclude(account__main_systemuser_id=F('id')) + diff --git a/orchestra/apps/systemusers/models.py b/orchestra/apps/systemusers/models.py index 7df96aa5..c4e71b0c 100644 --- a/orchestra/apps/systemusers/models.py +++ b/orchestra/apps/systemusers/models.py @@ -36,7 +36,7 @@ class SystemUser(models.Model): groups = models.ManyToManyField('self', blank=True, help_text=_("A new group will be created for the user. " "Which additional groups would you like them to be a member of?")) - is_main = models.BooleanField(_("is main"), default=False) +# is_main = models.BooleanField(_("is main"), default=False) is_active = models.BooleanField(_("active"), default=True, help_text=_("Designates whether this account should be treated as active. " "Unselect this instead of deleting accounts.")) @@ -53,6 +53,13 @@ class SystemUser(models.Model): except type(self).account.field.rel.to.DoesNotExist: return self.is_active + @property + def is_main(self): + # On account creation main_systemuser_id is still None + if self.account.main_systemuser_id: + return self.account.main_systemuser_id == self.pk + return self.account.username == self.username + def set_password(self, raw_password): self.password = make_password(raw_password) @@ -63,7 +70,7 @@ class SystemUser(models.Model): } basehome = settings.SYSTEMUSERS_HOME % context else: - basehome = self.account.systemusers.get(is_main=True).get_home() + basehome = self.account.main_systemuser.get_home() basehome = basehome.replace('/./', '/') home = os.path.join(basehome, self.home) # Chrooting diff --git a/orchestra/apps/systemusers/tests/functional_tests/tests.py b/orchestra/apps/systemusers/tests/functional_tests/tests.py index 04ce0c14..b96db807 100644 --- a/orchestra/apps/systemusers/tests/functional_tests/tests.py +++ b/orchestra/apps/systemusers/tests/functional_tests/tests.py @@ -143,6 +143,7 @@ class SystemUserMixin(object): self.validate_user(username) self.delete(username) self.validate_delete(username) + self.assertRaises(Exception, self.delete, self.account.username) def test_add_group(self): username = '%s_systemuser' % random_ascii(10) @@ -190,7 +191,7 @@ class RESTSystemUserMixin(SystemUserMixin): self.rest_login() # create main user self.save(self.account.username) - self.addCleanup(self.delete, self.account.username) + self.addCleanup(self.delete_account, self.account.username) @save_response_on_error def add(self, username, password, shell='/dev/null'): @@ -230,7 +231,7 @@ class AdminSystemUserMixin(SystemUserMixin): self.admin_login() # create main user self.save(self.account.username) - self.addCleanup(self.delete, self.account.username) + self.addCleanup(self.delete_account, self.account.username) @snapshot_on_error def add(self, username, password, shell='/dev/null'): @@ -245,10 +246,6 @@ class AdminSystemUserMixin(SystemUserMixin): password_field = self.selenium.find_element_by_id('id_password2') password_field.send_keys(password) - account_input = self.selenium.find_element_by_id('id_account') - account_select = Select(account_input) - account_select.select_by_value(str(self.account.pk)) - shell_input = self.selenium.find_element_by_id('id_shell') shell_select = Select(shell_input) shell_select.select_by_value(shell) @@ -261,6 +258,10 @@ class AdminSystemUserMixin(SystemUserMixin): user = SystemUser.objects.get(username=username) self.admin_delete(user) + @snapshot_on_error + def delete_account(self, username): + self.admin_delete(self.account) + @snapshot_on_error def disable(self, username): user = SystemUser.objects.get(username=username) @@ -332,7 +333,7 @@ class AdminSystemUserTest(AdminSystemUserMixin, BaseLiveServerTestCase): @snapshot_on_error def test_delete_account(self): - home = self.account.systemusers.get(is_main=True).get_home() + home = self.account.main_systemuser.get_home() delete = reverse('admin:accounts_account_delete', args=(self.account.pk,)) url = self.live_server_url + delete diff --git a/orchestra/apps/webapps/backends/__init__.py b/orchestra/apps/webapps/backends/__init__.py index 66560d8f..5173b5fd 100644 --- a/orchestra/apps/webapps/backends/__init__.py +++ b/orchestra/apps/webapps/backends/__init__.py @@ -46,8 +46,8 @@ class WebAppServiceMixin(object): def get_context(self, webapp): return { - 'user': webapp.account.username, - 'group': webapp.account.username, + 'user': webapp.get_username(), + 'group': webapp.get_groupname(), 'app_name': webapp.name, 'type': webapp.type, 'app_path': webapp.get_path().rstrip('/'), diff --git a/orchestra/apps/webapps/models.py b/orchestra/apps/webapps/models.py index 79ad75a4..aa7b9e92 100644 --- a/orchestra/apps/webapps/models.py +++ b/orchestra/apps/webapps/models.py @@ -52,6 +52,12 @@ class WebApp(models.Model): 'app_name': self.name, } return settings.WEBAPPS_BASE_ROOT % context + + def get_username(self): + return self.account.username + + def get_groupname(self): + return self.get_username() class WebAppOption(models.Model): diff --git a/orchestra/apps/websites/admin.py b/orchestra/apps/websites/admin.py index 350da590..74436223 100644 --- a/orchestra/apps/websites/admin.py +++ b/orchestra/apps/websites/admin.py @@ -13,10 +13,10 @@ class WebsiteOptionInline(admin.TabularInline): model = WebsiteOption extra = 1 - class Media: - css = { - 'all': ('orchestra/css/hide-inline-id.css',) - } +# class Media: +# css = { +# 'all': ('orchestra/css/hide-inline-id.css',) +# } def formfield_for_dbfield(self, db_field, **kwargs): """ Make value input widget bigger """ diff --git a/orchestra/apps/websites/backends/apache.py b/orchestra/apps/websites/backends/apache.py index beb4cb4e..54a7dc04 100644 --- a/orchestra/apps/websites/backends/apache.py +++ b/orchestra/apps/websites/backends/apache.py @@ -164,8 +164,8 @@ class Apache2Backend(ServiceController): 'site_name': site.name, 'ip': settings.WEBSITES_DEFAULT_IP, 'site_unique_name': site.unique_name, - 'user': site.account.username, - 'group': site.account.username, + 'user': site.get_username(), + 'group': site.get_groupname(), 'sites_enabled': sites_enabled, 'sites_available': "%s.conf" % os.path.join(sites_available, site.unique_name), 'logs': os.path.join(settings.WEBSITES_BASE_APACHE_LOGS, site.unique_name), diff --git a/orchestra/apps/websites/models.py b/orchestra/apps/websites/models.py index 01e46a3f..d6a4737a 100644 --- a/orchestra/apps/websites/models.py +++ b/orchestra/apps/websites/models.py @@ -50,6 +50,17 @@ class Website(models.Model): if self.port == 443: return 'https' raise TypeError('No protocol for port "%s"' % self.port) + + def get_absolute_url(self): + domain = self.domains.first() + if domain: + return '%s://%s' % (self.protocol, domain) + + def get_username(self): + return self.account.username + + def get_groupname(self): + return self.get_username() class WebsiteOption(models.Model): @@ -93,6 +104,11 @@ class Content(models.Model): def clean(self): if not self.path.startswith('/'): self.path = '/' + self.path + + def get_absolute_url(self): + domain = self.website.domains.first() + if domain: + return '%s://%s%s' % (self.website.protocol, domain, self.path) services.register(Website) diff --git a/orchestra/utils/system.py b/orchestra/utils/system.py index d100fd11..5ebfe4b0 100644 --- a/orchestra/utils/system.py +++ b/orchestra/utils/system.py @@ -46,7 +46,7 @@ def read_async(fd): return '' -def run(command, display=True, error_codes=[0], silent=False, stdin=''): +def run(command, display=False, error_codes=[0], silent=False, stdin=''): """ Subprocess wrapper for running commands """ if display: sys.stderr.write("\n\033[1m $ %s\033[0m\n" % command) diff --git a/orchestra/utils/tests.py b/orchestra/utils/tests.py index bfc646d6..a802bafb 100644 --- a/orchestra/utils/tests.py +++ b/orchestra/utils/tests.py @@ -69,6 +69,8 @@ class BaseTestCase(TestCase, AppDependencyMixin): class BaseLiveServerTestCase(AppDependencyMixin, LiveServerTestCase): @classmethod def setUpClass(cls): + # Avoid problems with the overlaping menu when clicking + settings.ADMIN_TOOLS_MENU = 'admin_tools.menu.Menu' cls.vdisplay = Xvfb() cls.vdisplay.start() cls.selenium = WebDriver() @@ -180,4 +182,3 @@ def save_response_on_error(test): dumpfile.write(self.rest.last_response.content) raise return inner -