From a6734ea1d1918198f2368c74bddc363cef3dfaf9 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Thu, 26 Mar 2015 16:00:30 +0000 Subject: [PATCH] Added seafile incon andn fixed random bugs --- TODO.md | 61 +++++++++++-------------- orchestra/admin/options.py | 2 +- orchestra/apps/accounts/actions.py | 2 +- orchestra/apps/accounts/admin.py | 36 +++++++++++++-- orchestra/apps/accounts/filters.py | 21 +++++++++ orchestra/apps/accounts/models.py | 11 ++++- orchestra/apps/bills/admin.py | 8 ++-- orchestra/apps/domains/validators.py | 1 - orchestra/apps/mailboxes/admin.py | 9 ++-- orchestra/apps/miscellaneous/admin.py | 2 +- orchestra/apps/orchestration/admin.py | 2 +- orchestra/apps/orders/models.py | 7 +-- orchestra/apps/payments/models.py | 2 +- orchestra/apps/saas/services/bscw.py | 1 - orchestra/apps/saas/services/options.py | 3 ++ orchestra/apps/saas/settings.py | 11 ++++- orchestra/apps/services/actions.py | 6 ++- orchestra/apps/services/handlers.py | 5 +- orchestra/apps/services/models.py | 17 +++---- orchestra/apps/services/settings.py | 2 +- orchestra/apps/systemusers/actions.py | 30 +++++++++++- orchestra/apps/systemusers/admin.py | 17 +++---- orchestra/apps/systemusers/backends.py | 1 + orchestra/plugins/admin.py | 4 +- 24 files changed, 176 insertions(+), 85 deletions(-) diff --git a/TODO.md b/TODO.md index 70324e94..7af04f4d 100644 --- a/TODO.md +++ b/TODO.md @@ -19,8 +19,6 @@ * backend logs with hal logo -* set_password orchestration method? - * LAST version of this shit http://wkhtmltopdf.org/downloads.html @@ -39,8 +37,6 @@ * mail backend related_models = ('resources__content_type') ?? -* Domain backend PowerDNS Bind validation support? - * Maildir billing tests/ webdisk billing tests (avg metric) @@ -50,10 +46,6 @@ * rename accounts register to "account", and reated api and admin references - -* Disable services is_active should be computed on the fly in order to distinguish account.is_active from service.is_active when reactivation. - * Perhaps it is time to create a ServiceModel ? - * prevent deletion of main user by the user itself * AccountAdminMixin auto adds 'account__name' on searchfields @@ -119,8 +111,6 @@ * Make main systemuser able to write/read everything on its home, including stuff created by the CGI user and secondary users * Prevent users from accessing other users home while at the same time allow access Apache/fcgid/fpm and secondary users (x) -* public_html/webapps directory with root owner and permissions - * resource min max allocation with validation * mailman needs both aliases when address_name is provided (default messages and bounces and all) @@ -144,28 +134,11 @@ * Create an admin service_view with icons (like SaaS app) - * Resource graph for each related object - -* multitenant webapps modeled on WepApp -> name unique for all accounts - -* webapp compat webapp-options -* webapps modeled on classes instead of settings? - * Service.account change and orders consistency -* Mix webapps type with backends (two for the price of one) - -* Webapp options and type compatibility - -* SaaS model splitted into SaaSUser and SaaSSite? - -Multi-tenant WebApps --------------------- -* SaaS - Those apps that can't use custom domain -* WebApp - Those apps that can use custom domain - +* SaaS model splitted into SaaSUser and SaaSSite? inherit from SaaS * prevent @pangea.org email addresses on contacts, enforce at least one email without @pangea.org @@ -192,19 +165,17 @@ Php binaries should have this format: /usr/bin/php5.2-cgi * and other IfModule on backend SecRule -* Orchestra global search box on the header, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields +* Orchestra global search box on the page head, based https://github.com/django/django/blob/master/django/contrib/admin/options.py#L866 and iterating over all registered services and inspectin its admin.search_fields * contain error on plugin missing key (plugin dissabled): NOP, fail hard is better than silently, perhaps fail at starttime? apploading machinary * contact.alternative_phone on a phone.tooltip, email:to - * better validate options and directives (url locations, filesystem paths, etc..) * make sure that you understand the risks - * full support for deactivation of services/accounts * Display admin.is_active (disabled account special icon and order by support) @@ -232,17 +203,39 @@ require_once(‘/etc/moodles/’.$moodle_host.‘config.php’);``` moodle/drupl * normurlpath '' return '/' -* initial configuration of multisite sas apps with password stored in DATA +* initial configuration of multisite sas apps with password stored in DATA ?? Dsign decission: initial pwds vs eventual consistency vs externa service vs backend raise exception? * webapps installation complete, passowrd protected * saas.initial_password autogenerated (ok because its random and not user provided) vs saas.password /change_Form provided + send email with initial_password * more robust backend error handling, continue executing but exit code > 0 if failure, replace exit_code=0; do_sometging || exit_code=1 -* saas require unique emails? connect to backend server to find out because they change - * automaitcally set passwords and email users? * website directives uniquenes validation on serializers ++ is_Active custom filter with support for instance.account.is_Active +* django virtual field for saas and webapps related objects (db) to show on delete confirmation + if only extra related objects are databases and user databases why not make them first class relations????? + * >>> Account._meta.virtual_fields[0].bulk_related_objects([Account.objects.all()[0]]) + [, ] + https://github.com/django/django/blob/master/django/db/models/deletion.py#L232 + https://github.com/django/django/blob/master/django/contrib/contenttypes/fields.py#L282 + + from django.contrib.contenttypes.fields import GenericRelation + from django.db import DEFAULT_DB_ALIAS + from orchestra.apps.databases.models import Database + class VirtualRelation(GenericRelation): + def bulk_related_objects(self, objs, using=DEFAULT_DB_ALIAS): + return [] + # return Database.objects.filter(name__in= + # obj.service_instance.get_related() for obj in objs + ## return self.remote_field.model._base_manager.db_manager(using).all() + relation = VirtualRelation('databases.Database') + SaaS.add_to_class('databases', relation) + + * one to one relation deleteion on both sides?? + + +* webapps/saas delete related db by id not name !! type!=Mysql diff --git a/orchestra/admin/options.py b/orchestra/admin/options.py index 71bc69bd..6f56f219 100644 --- a/orchestra/admin/options.py +++ b/orchestra/admin/options.py @@ -124,7 +124,7 @@ class ChangeAddFieldsMixin(object): def get_readonly_fields(self, request, obj=None): fields = super(ChangeAddFieldsMixin, self).get_readonly_fields(request, obj) if obj: - return fields + self.get_change_readonly_fields(request, obj=obj) + return fields + self.get_change_readonly_fields(request, obj) return fields def get_fieldsets(self, request, obj=None): diff --git a/orchestra/apps/accounts/actions.py b/orchestra/apps/accounts/actions.py index fd583fd2..69e92d3b 100644 --- a/orchestra/apps/accounts/actions.py +++ b/orchestra/apps/accounts/actions.py @@ -31,7 +31,7 @@ disable.verbose_name = _("Disable") def list_contacts(modeladmin, request, queryset): ids = queryset.values_list('id', flat=True) if not ids: - message.warning(request, "Select at least one account.") + messages.warning(request, "Select at least one account.") return url = reverse('admin:contacts_contact_changelist') url += '?account__in=%s' % ','.join(map(str, ids)) diff --git a/orchestra/apps/accounts/admin.py b/orchestra/apps/accounts/admin.py index 32908ac9..3ae363a3 100644 --- a/orchestra/apps/accounts/admin.py +++ b/orchestra/apps/accounts/admin.py @@ -95,7 +95,7 @@ class AccountAdmin(ChangePasswordAdminMixin, auth.UserAdmin, ExtendedModelAdmin) form_url=form_url, extra_context=context) def get_fieldsets(self, request, obj=None): - fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj=obj) + fieldsets = super(AccountAdmin, self).get_fieldsets(request, obj) if not obj: fields = AccountCreationForm.create_related_fields if fields: @@ -153,6 +153,17 @@ class AccountAdminMixin(object): account = None list_select_related = ('account',) + def display_active(self, instance): + if not instance.is_active: + return 'False' + elif not instance.account.is_active: + msg = _("Account disabled") + return 'False' % msg + return 'True' + display_active.short_description = _("active") + display_active.allow_tags = True + display_active.admin_order_field = 'is_active' + def account_link(self, instance): account = instance.account if instance.pk else self.account url = change_url(account) @@ -161,6 +172,23 @@ class AccountAdminMixin(object): account_link.allow_tags = True account_link.admin_order_field = 'account__username' + def render_change_form(self, request, context, *args, **kwargs): + """ Warns user when object's account is disabled """ + try: + field = context['adminform'].form.fields['is_active'] + except KeyError: + pass + else: + help_text = ( + "Designates whether this account should be treated as active. " + "Unselect this instead of deleting accounts." + ) + obj = kwargs.get('obj') + if obj and not obj.account.is_active: + help_text += "
This user's account is dissabled" + field.help_text = _(help_text) + return super(AccountAdminMixin, self).render_change_form(request, context, *args, **kwargs) + def get_fields(self, request, obj=None): """ remove account or account_link depending on the case """ fields = super(AccountAdminMixin, self).get_fields(request, obj) @@ -181,7 +209,7 @@ class AccountAdminMixin(object): """ provide account for filter_by_account_fields """ if obj: self.account = obj.account - return super(AccountAdminMixin, self).get_readonly_fields(request, obj=obj) + return super(AccountAdminMixin, self).get_readonly_fields(request, obj) def formfield_for_dbfield(self, db_field, **kwargs): """ Filter by account """ @@ -207,7 +235,7 @@ class AccountAdminMixin(object): def get_formset(self, request, obj=None, **kwargs): """ provides form.account for convinience """ - formset = super(AccountAdminMixin, self).get_formset(request, obj=obj, **kwargs) + formset = super(AccountAdminMixin, self).get_formset(request, obj, **kwargs) formset.form.account = self.account formset.account = self.account return formset @@ -263,7 +291,7 @@ class AccountAdminMixin(object): class SelectAccountAdminMixin(AccountAdminMixin): """ Provides support for accounts on ModelAdmin """ def get_inline_instances(self, request, obj=None): - inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj=obj) + inlines = super(AccountAdminMixin, self).get_inline_instances(request, obj) if self.account: account = self.account else: diff --git a/orchestra/apps/accounts/filters.py b/orchestra/apps/accounts/filters.py index 84a27831..3db9857e 100644 --- a/orchestra/apps/accounts/filters.py +++ b/orchestra/apps/accounts/filters.py @@ -1,4 +1,5 @@ from django.contrib.admin import SimpleListFilter +from django.db.models import Q from django.utils.translation import ugettext_lazy as _ @@ -18,3 +19,23 @@ class HasMainUserListFilter(SimpleListFilter): return queryset.filter(users__isnull=False).distinct() if self.value() == 'False': return queryset.filter(users__isnull=True).distinct() + + +class IsActiveListFilter(SimpleListFilter): + title = _("Is active") + parameter_name = 'active' + + def lookups(self, request, model_admin): + return ( + ('True', _("True")), + ('False', _("False")), + ) + + def queryset(self, request, queryset): + if self.value() == 'True': + return queryset.filter(is_active=True, account__is_active=True) + elif self.value() == 'False': + return queryset.filter(Q(is_active=False) | Q(account__is_active=False)) + return queryset + + diff --git a/orchestra/apps/accounts/models.py b/orchestra/apps/accounts/models.py index d20849ef..76890be8 100644 --- a/orchestra/apps/accounts/models.py +++ b/orchestra/apps/accounts/models.py @@ -6,6 +6,8 @@ from django.db.models.loading import get_model from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from orchestra.apps.orchestration.middlewares import OperationsMiddleware +from orchestra.apps.orchestration.models import BackendOperation as Operation from orchestra.core import services, accounts from orchestra.utils import send_email_template @@ -63,11 +65,15 @@ class Account(auth.AbstractBaseUser): def save(self, active_systemuser=False, *args, **kwargs): created = not self.pk + if not created: + was_active = Account.objects.filter(pk=self.pk).values_list('is_active', flat=True)[0] super(Account, self).save(*args, **kwargs) if created: self.main_systemuser = self.systemusers.create(account=self, username=self.username, password=self.password, is_active=active_systemuser) self.save(update_fields=['main_systemuser']) + elif was_active != self.is_active: + self.notify_related() def clean(self): self.short_name = self.short_name.strip() @@ -76,12 +82,15 @@ class Account(auth.AbstractBaseUser): def disable(self): self.is_active = False self.save(update_fields=['is_active']) + self.notify_related() + + def notify_related(self): # Trigger save() on related objects that depend on this account for rel in self._meta.get_all_related_objects(): source = getattr(rel, 'related_model', rel.model) if source in services and hasattr(source, 'active'): for obj in getattr(self, rel.get_accessor_name()).all(): - obj.save(update_fields=[]) + OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[]) def send_email(self, template, context, contacts=[], attachments=[], html=None): contacts = self.contacts.filter(email_usages=contacts) diff --git a/orchestra/apps/bills/admin.py b/orchestra/apps/bills/admin.py index ad8791f1..7521c755 100644 --- a/orchestra/apps/bills/admin.py +++ b/orchestra/apps/bills/admin.py @@ -145,19 +145,19 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): display_payment_state.short_description = _("Payment") def get_readonly_fields(self, request, obj=None): - fields = super(BillAdmin, self).get_readonly_fields(request, obj=obj) + fields = super(BillAdmin, self).get_readonly_fields(request, obj) if obj and not obj.is_open: fields += self.add_fields return fields def get_fieldsets(self, request, obj=None): - fieldsets = super(BillAdmin, self).get_fieldsets(request, obj=obj) + fieldsets = super(BillAdmin, self).get_fieldsets(request, obj) if obj and obj.is_open: fieldsets = (fieldsets[0],) return fieldsets def get_change_view_actions(self, obj=None): - actions = super(BillAdmin, self).get_change_view_actions(obj=obj) + actions = super(BillAdmin, self).get_change_view_actions(obj) exclude = [] if obj: if not obj.is_open: @@ -165,7 +165,7 @@ class BillAdmin(AccountAdminMixin, ExtendedModelAdmin): return [action for action in actions if action.__name__ not in exclude] def get_inline_instances(self, request, obj=None): - inlines = super(BillAdmin, self).get_inline_instances(request, obj=obj) + inlines = super(BillAdmin, self).get_inline_instances(request, obj) if obj and not obj.is_open: return [inline for inline in inlines if not type(inline) == BillLineInline] return [inline for inline in inlines if not type(inline) == ClosedBillLineInline] diff --git a/orchestra/apps/domains/validators.py b/orchestra/apps/domains/validators.py index e9897d8e..2e116ad4 100644 --- a/orchestra/apps/domains/validators.py +++ b/orchestra/apps/domains/validators.py @@ -110,7 +110,6 @@ def validate_zone(zone): zone_name = zone.split()[0][:-1] checkzone = settings.DOMAINS_CHECKZONE_BIN_PATH cmd = ' '.join(["echo -e '%s'" % zone, '|', checkzone, zone_name, '/dev/stdin']) - print cmd check = run(cmd, error_codes=[0, 1], display=False) if check.return_code == 1: errors = re.compile(r'zone.*: (.*)').findall(check.stdout)[:-1] diff --git a/orchestra/apps/mailboxes/admin.py b/orchestra/apps/mailboxes/admin.py index 2ca53893..ec527880 100644 --- a/orchestra/apps/mailboxes/admin.py +++ b/orchestra/apps/mailboxes/admin.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from orchestra.admin import ExtendedModelAdmin, ChangePasswordAdminMixin from orchestra.admin.utils import admin_link, change_url from orchestra.apps.accounts.admin import SelectAccountAdminMixin, AccountAdminMixin +from orchestra.apps.accounts.filters import IsActiveListFilter from . import settings from .actions import SendMailboxEmail @@ -30,9 +31,9 @@ class AutoresponseInline(admin.StackedInline): class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedModelAdmin): list_display = ( - 'name', 'account_link', 'filtering', 'display_addresses' + 'name', 'account_link', 'filtering', 'display_addresses', 'display_active', ) - list_filter = (HasAddressListFilter, 'filtering') + list_filter = (IsActiveListFilter, HasAddressListFilter, 'filtering') search_fields = ('account__username', 'account__short_name', 'account__full_name', 'name') add_fieldsets = ( (None, { @@ -81,7 +82,7 @@ class MailboxAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, ExtendedMo return super(MailboxAdmin, self).get_actions(request) def get_fieldsets(self, request, obj=None): - fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj=obj) + fieldsets = super(MailboxAdmin, self).get_fieldsets(request, obj) if obj and obj.filtering == obj.CUSTOM: # not collapsed filtering when exists fieldsets = copy.deepcopy(fieldsets) @@ -147,7 +148,7 @@ class AddressAdmin(SelectAccountAdminMixin, ExtendedModelAdmin): def get_fields(self, request, obj=None): """ Remove mailboxes field when creating address from a popup i.e. from mailbox add form """ - fields = super(AddressAdmin, self).get_fields(request, obj=obj) + fields = super(AddressAdmin, self).get_fields(request, obj) if '_to_field' in parse_qs(request.META['QUERY_STRING']): # Add address popup fields = list(fields) diff --git a/orchestra/apps/miscellaneous/admin.py b/orchestra/apps/miscellaneous/admin.py index de58b6db..a221079e 100644 --- a/orchestra/apps/miscellaneous/admin.py +++ b/orchestra/apps/miscellaneous/admin.py @@ -88,7 +88,7 @@ class MiscellaneousAdmin(AccountAdminMixin, SelectPluginAdminMixin, admin.ModelA return fields def get_form(self, request, obj=None, **kwargs): - form = super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs) + form = super(SelectPluginAdminMixin, self).get_form(request, obj, **kwargs) service = self.get_service(obj) def clean_identifier(self, service=service): identifier = self.cleaned_data['identifier'] diff --git a/orchestra/apps/orchestration/admin.py b/orchestra/apps/orchestration/admin.py index 8c83a0b4..470e800e 100644 --- a/orchestra/apps/orchestration/admin.py +++ b/orchestra/apps/orchestration/admin.py @@ -62,7 +62,7 @@ class RouteAdmin(admin.ModelAdmin): def get_form(self, request, obj=None, **kwargs): """ Include dynamic help text for existing objects """ - form = super(RouteAdmin, self).get_form(request, obj=obj, **kwargs) + form = super(RouteAdmin, self).get_form(request, obj, **kwargs) if obj: form.base_fields['backend'].help_text = self.BACKEND_HELP_TEXT.get(obj.backend, '') return form diff --git a/orchestra/apps/orders/models.py b/orchestra/apps/orders/models.py index 0cf2d507..1638cd04 100644 --- a/orchestra/apps/orders/models.py +++ b/orchestra/apps/orders/models.py @@ -128,7 +128,7 @@ class Order(models.Model): get_latest_by = 'id' def __unicode__(self): - return str(self.service) + return unicode(self.service) @classmethod def update_orders(cls, instance, service=None, commit=True): @@ -178,8 +178,9 @@ class Order(models.Model): MetricStorage.store(self, metric) metric = ', metric:{}'.format(metric) description = handler.get_order_description(instance) - logger.info("UPDATED order id:{id}, description:{description}{metric}".format( - id=self.id, description=description, metric=metric)) + logger.info(u"UPDATED order id:{id}, description:{description}{metric}".format( + id=self.id, description=description, metric=metric).encode('ascii', 'ignore') + ) if self.description != description: self.description = description self.save(update_fields=['description']) diff --git a/orchestra/apps/payments/models.py b/orchestra/apps/payments/models.py index fb91d2cc..5ae3b7d9 100644 --- a/orchestra/apps/payments/models.py +++ b/orchestra/apps/payments/models.py @@ -34,7 +34,7 @@ class PaymentSource(models.Model): return PaymentMethod.get_plugin(self.method) @cached_property - def service_instance(self): + def method_instance(self): """ Per request lived method_instance """ return self.method_class(self) diff --git a/orchestra/apps/saas/services/bscw.py b/orchestra/apps/saas/services/bscw.py index ecf6abf9..5215d528 100644 --- a/orchestra/apps/saas/services/bscw.py +++ b/orchestra/apps/saas/services/bscw.py @@ -27,6 +27,5 @@ class BSCWService(SoftwareService): form = BSCWForm serializer = BSCWDataSerializer icon = 'orchestra/icons/apps/BSCW.png' - # TODO override from settings site_domain = settings.SAAS_BSCW_DOMAIN change_readonly_fileds = ('email',) diff --git a/orchestra/apps/saas/services/options.py b/orchestra/apps/saas/services/options.py index 0cce80a5..f3468c6f 100644 --- a/orchestra/apps/saas/services/options.py +++ b/orchestra/apps/saas/services/options.py @@ -93,3 +93,6 @@ class SoftwareService(plugins.Plugin): def delete(self): pass + + def get_related(self): + return [] diff --git a/orchestra/apps/saas/settings.py b/orchestra/apps/saas/settings.py index 02081b0d..dfc03814 100644 --- a/orchestra/apps/saas/settings.py +++ b/orchestra/apps/saas/settings.py @@ -9,6 +9,7 @@ SAAS_ENABLED_SERVICES = getattr(settings, 'SAAS_ENABLED_SERVICES', ( 'orchestra.apps.saas.services.wordpress.WordPressService', 'orchestra.apps.saas.services.dokuwiki.DokuWikiService', 'orchestra.apps.saas.services.drupal.DrupalService', + 'orchestra.apps.saas.services.seafile.SeaFileService', )) @@ -42,16 +43,24 @@ SAAS_PHPLIST_BASE_DOMAIN = getattr(settings, 'SAAS_PHPLIST_BASE_DOMAIN', ) +SAAS_SEAFILE_DOMAIN = getattr(settings, 'SAAS_SEAFILE_DOMAIN', + 'seafile.orchestra.lan' +) + +SAAS_SEAFILE_DEFAULT_QUOTA = getattr(settings, 'SAAS_SEAFILE_DEFAULT_QUOTA', + 50 +) + SAAS_BSCW_DOMAIN = getattr(settings, 'SAAS_BSCW_DOMAIN', 'bscw.orchestra.lan' ) - SAAS_BSCW_DEFAULT_QUOTA = getattr(settings, 'SAAS_BSCW_DEFAULT_QUOTA', 50 ) + SAAS_GITLAB_ROOT_PASSWORD = getattr(settings, 'SAAS_GITLAB_ROOT_PASSWORD', 'secret' ) diff --git a/orchestra/apps/services/actions.py b/orchestra/apps/services/actions.py index c4243885..eeebe155 100644 --- a/orchestra/apps/services/actions.py +++ b/orchestra/apps/services/actions.py @@ -67,11 +67,13 @@ view_help.verbose_name = _("Help") def clone(modeladmin, request, queryset): service = queryset.get() fields = modeladmin.get_fields(request) - fk_fields = ('content_type',) query = [] for field in fields: - if field in fk_fields: + model_field = type(service)._meta.get_field_by_name(field)[0] + if model_field.rel: value = getattr(service, field + '_id') + elif 'Boolean' in model_field.__class__.__name__: + value = 'True' if getattr(service, field) else '' else: value = getattr(service, field) query.append('%s=%s' % (field, value)) diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index 80525769..5b705d2a 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -4,6 +4,7 @@ import decimal from dateutil import relativedelta from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import ValidationError from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -44,7 +45,7 @@ class ServiceHandler(plugins.Plugin): def validate_match(self, service): if not service.match: - raise ValidationError(_("Match should be provided.")) + service.match = 'True' try: obj = service.content_type.model_class().objects.all()[0] except IndexError: @@ -125,7 +126,7 @@ class ServiceHandler(plugins.Plugin): instance._meta.model_name: instance, } if not self.order_description: - return '%s: %s' % (self.description, instance) + return u'%s: %s' % (self.description, instance) return eval(self.order_description, safe_locals) def get_billing_point(self, order, bp=None, **options): diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index 53fd99d1..037ea1b5 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -53,7 +53,7 @@ class Service(models.Model): "Related instance can be instantiated with instance keyword or " "content_type.model_name.
" " databaseuser.type == 'MYSQL'
" - " miscellaneous.active and miscellaneous.identifier.endswith(('.org', '.net', '.com'))'
" + " miscellaneous.active and miscellaneous.identifier.endswith(('.org', '.net', '.com'))
" " contractedplan.plan.name == 'association_fee''
" " instance.active")) handler_type = models.CharField(_("handler"), max_length=256, blank=True, @@ -94,7 +94,7 @@ class Service(models.Model): help_text=_("Period in which orders will be ignored if cancelled. " "Useful for designating trial periods"), choices=( - (NEVER, _("No ignore")), + (NEVER, _("Never")), (ONE_DAY, _("One day")), (TWO_DAYS, _("Two days")), (TEN_DAYS, _("Ten days")), @@ -112,7 +112,7 @@ class Service(models.Model): " miscellaneous.amount
" " max((account.resources.traffic.used or 0) -" " getattr(account.miscellaneous.filter(is_active=True," - " service__name='traffic prepay').last(), 'amount', 0), 0)")) + " service__name='traffic-prepay').last(), 'amount', 0), 0)")) nominal_price = models.DecimalField(_("nominal price"), max_digits=12, decimal_places=2) tax = models.PositiveIntegerField(_("tax"), choices=settings.SERVICES_SERVICE_TAXES, @@ -174,11 +174,12 @@ class Service(models.Model): def clean(self): self.description = self.description.strip() - validators.all_valid({ - 'content_type': (self.handler.validate_content_type, self), - 'match': (self.handlers.validate_match, self), - 'metric': (self.handlers.validate_metric, self), - }) + if hasattr(self, 'content_type'): + validators.all_valid({ + 'content_type': (self.handler.validate_content_type, self), + 'match': (self.handler.validate_match, self), + 'metric': (self.handler.validate_metric, self), + }) def get_pricing_period(self): if self.pricing_period == self.BILLING_PERIOD: diff --git a/orchestra/apps/services/settings.py b/orchestra/apps/services/settings.py index 65ee8cc0..ebb64422 100644 --- a/orchestra/apps/services/settings.py +++ b/orchestra/apps/services/settings.py @@ -18,4 +18,4 @@ SERVICES_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'SERVICES_SERVICE_ANUAL SERVICES_ORDER_MODEL = getattr(settings, 'SERVICES_ORDER_MODEL', 'orders.Order') -SERVICES_DEFAULT_IGNORE_PERIOD = getattr(settings, 'SERVICES_DEFAULT_IGNORE_PERIOD', 'TWO_DAYS') +SERVICES_DEFAULT_IGNORE_PERIOD = getattr(settings, 'SERVICES_DEFAULT_IGNORE_PERIOD', 'TEN_DAYS') diff --git a/orchestra/apps/systemusers/actions.py b/orchestra/apps/systemusers/actions.py index b5a0b5d2..dd326f3e 100644 --- a/orchestra/apps/systemusers/actions.py +++ b/orchestra/apps/systemusers/actions.py @@ -1,7 +1,8 @@ from functools import partial from django import forms -from django.contrib import messages +from django.contrib import messages, admin +from django.core.exceptions import PermissionDenied from django.db import transaction from django.shortcuts import render from django.utils.safestring import mark_safe @@ -28,3 +29,30 @@ def grant_permission(modeladmin, request, queryset): # TODO grant_permission.url_name = 'grant-permission' grant_permission.verbose_name = _("Grant permission") + + +def delete_selected(modeladmin, request, queryset): + """ wrapper arround admin.actions.delete_selected to prevent main system users deletion """ + opts = modeladmin.model._meta + app_label = opts.app_label + # Check that the user has delete permission for the actual model + if not modeladmin.has_delete_permission(request): + raise PermissionDenied + else: + accounts = [] + for user in queryset: + if user.is_main: + accounts.append(user.username) + if accounts: + n = len(accounts) + messages.error(request, ungettext( + "You have selected one main system user (%(accounts)s), which can not be deleted.", + "You have selected some main system users which can not be deleted (%(accounts)s).", + n) % { + 'accounts': ', '.join(accounts[:10]+['...'] if n > 10 else accounts) + } + ) + return + return admin.actions.delete_selected(modeladmin, request, queryset) +delete_selected.short_description = _("Delete selected %(verbose_name_plural)s") + diff --git a/orchestra/apps/systemusers/admin.py b/orchestra/apps/systemusers/admin.py index 61117c5b..9a4a5b23 100644 --- a/orchestra/apps/systemusers/admin.py +++ b/orchestra/apps/systemusers/admin.py @@ -12,10 +12,11 @@ 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.apps.accounts.filters import IsActiveListFilter from orchestra.forms import UserCreationForm, UserChangeForm from . import settings -from .actions import grant_permission +from .actions import grant_permission, delete_selected from .filters import IsMainListFilter from .forms import SystemUserCreationForm, SystemUserChangeForm from .models import SystemUser @@ -25,7 +26,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende list_display = ( 'username', 'account_link', 'shell', 'display_home', 'display_active', 'display_main' ) - list_filter = ('is_active', 'shell', IsMainListFilter) + list_filter = (IsActiveListFilter, 'shell', IsMainListFilter) fieldsets = ( (None, { 'fields': ('username', 'password', 'account_link', 'is_active') @@ -50,15 +51,9 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende add_form = SystemUserCreationForm form = SystemUserChangeForm ordering = ('-id',) - actions = (grant_permission,) + actions = (delete_selected, grant_permission,) change_view_actions = actions - 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") @@ -70,7 +65,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende display_home.admin_order_field = 'home' def get_form(self, request, obj=None, **kwargs): - form = super(SystemUserAdmin, self).get_form(request, obj=obj, **kwargs) + form = super(SystemUserAdmin, self).get_form(request, obj, **kwargs) form.account = self.account if obj: # Has to be done here and not in the form because of strange phenomenon @@ -83,7 +78,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende 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) + return super(SystemUserAdmin, self).has_delete_permission(request, obj) admin.site.register(SystemUser, SystemUserAdmin) diff --git a/orchestra/apps/systemusers/backends.py b/orchestra/apps/systemusers/backends.py index f9ffee97..ed48f059 100644 --- a/orchestra/apps/systemusers/backends.py +++ b/orchestra/apps/systemusers/backends.py @@ -18,6 +18,7 @@ class SystemUserBackend(ServiceController): context = self.get_context(user) groups = ','.join(self.get_groups(user)) context['groups_arg'] = '--groups %s' % groups if groups else '' + # TODO userd add will fail if %(user)s group already exists self.append(textwrap.dedent(""" if [[ $( id %(user)s ) ]]; then usermod %(user)s --password '%(password)s' --shell %(shell)s %(groups_arg)s diff --git a/orchestra/plugins/admin.py b/orchestra/plugins/admin.py index 4435fadd..4729cef0 100644 --- a/orchestra/plugins/admin.py +++ b/orchestra/plugins/admin.py @@ -19,11 +19,11 @@ class SelectPluginAdminMixin(object): else: plugin = self.plugin.get_plugin(self.plugin_value)() self.form = plugin.get_form() - return super(SelectPluginAdminMixin, self).get_form(request, obj=obj, **kwargs) + return super(SelectPluginAdminMixin, self).get_form(request, obj, **kwargs) def get_fields(self, request, obj=None): """ Try to maintain original field ordering """ - fields = super(SelectPluginAdminMixin, self).get_fields(request, obj=obj) + fields = super(SelectPluginAdminMixin, self).get_fields(request, obj) head_fields = list(self.get_readonly_fields(request, obj)) head, tail = [], [] for field in fields: