diff --git a/TODO.md b/TODO.md index 5e39f70b..bc86b844 100644 --- a/TODO.md +++ b/TODO.md @@ -119,7 +119,6 @@ Remember that, as always with QuerySets, any subsequent chained methods which im * delete main user -> delete account or prevent delete main user * Ansible orchestration *method* (methods.py) -* pip upgrade or install * multiple domains creation; line separated domains * Move MU webapps to SaaS? @@ -156,5 +155,6 @@ textwrap.dedent( \\) * parmiko write to a channel instead of transfering files? http://sysadmin.circularvale.com/programming/paramiko-channel-hangs/ * strip leading and trailing whitre spaces of most input fields -* Examples of service.match and service.metric +* better modeling of the interdependency between webapps and websites (settings) +* webapp options cfig agnostic diff --git a/orchestra/apps/domains/backends.py b/orchestra/apps/domains/backends.py index 282953a1..cc263a3d 100644 --- a/orchestra/apps/domains/backends.py +++ b/orchestra/apps/domains/backends.py @@ -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 '"none"', } context.update({ 'conf_path': settings.DOMAINS_SLAVES_PATH, diff --git a/orchestra/apps/domains/forms.py b/orchestra/apps/domains/forms.py index fead09de..0c46670d 100644 --- a/orchestra/apps/domains/forms.py +++ b/orchestra/apps/domains/forms.py @@ -24,6 +24,46 @@ class DomainAdminForm(forms.ModelForm): return cleaned_data +#class BatchDomainCreationAdminForm(DomainAdminForm): +# # TODO +# name = forms.CharField(widget=forms.Textarea, label=_("Names"), +# help_text=_("Domain per line. All domains will share the same attributes.")) +# +# def clean_name(self): +# self.names = [] +# target = None +# for name in self.cleaned_data['name'].splitlines(): +# name = name.strip() +# if target is None: +# target = name +# else: +# domain = Domain(name=name) +# try: +# domain.full_clean(exclude=['top']) +# except ValidationError as e: +# raise ValidationError(e.error_dict['name']) +# self.names.append(name) +# return target +# +# def save_model(self, request, obj, form, change): +# # TODO thsi is modeladmin +# """ batch domain creation support """ +# super(DomainAdmin, self).save_model(request, obj, form, change) +# if not change: +# for name in form.names: +# domain = Domain.objects.create(name=name, account_id=obj.account_id) +# +# def save_related(self, request, form, formsets, change): +# # TODO thsi is modeladmin +# """ batch domain creation support """ +# super(DomainAdmin, self).save_related(request, form, formsets, change) +# if not change: +# for name in form.names: +# for formset in formsets: +# formset.instance = form.instance +# self.save_formset(request, form, formset, change=change) + + class RecordInlineFormSet(forms.models.BaseInlineFormSet): def clean(self): """ Checks if everything is consistent """ diff --git a/orchestra/apps/domains/models.py b/orchestra/apps/domains/models.py index 01171103..1b40eb99 100644 --- a/orchestra/apps/domains/models.py +++ b/orchestra/apps/domains/models.py @@ -11,9 +11,10 @@ from . import settings, validators, utils class Domain(models.Model): name = models.CharField(_("name"), max_length=256, unique=True, - validators=[validate_hostname, validators.validate_allowed_domain]) + validators=[validate_hostname, validators.validate_allowed_domain], + help_text=_("Domain or subdomain name.")) account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), - related_name='domains', blank=True, help_text=_("Automatically selected for subdomains")) + related_name='domains', blank=True, help_text=_("Automatically selected for subdomains.")) top = models.ForeignKey('domains.Domain', null=True, related_name='subdomains') serial = models.IntegerField(_("serial"), default=utils.generate_zone_serial, help_text=_("Serial number")) diff --git a/orchestra/apps/miscellaneous/models.py b/orchestra/apps/miscellaneous/models.py index 6de14273..7b4f82fd 100644 --- a/orchestra/apps/miscellaneous/models.py +++ b/orchestra/apps/miscellaneous/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from orchestra.core import services @@ -34,6 +35,13 @@ class Miscellaneous(models.Model): def __unicode__(self): return "{0}-{1}".format(str(self.service), str(self.account)) + + @cached_property + def active(self): + try: + return self.is_active and self.account.is_active + except type(self).account.field.rel.to.DoesNotExist: + return self.is_active services.register(Miscellaneous) diff --git a/orchestra/apps/services/handlers.py b/orchestra/apps/services/handlers.py index c666dc9c..2d39ca5c 100644 --- a/orchestra/apps/services/handlers.py +++ b/orchestra/apps/services/handlers.py @@ -45,6 +45,9 @@ class ServiceHandler(plugins.Plugin): return ContentType.objects.get_by_natural_key(app_label, model.lower()) def matches(self, instance): + if not self.match: + # Blank expressions always evaluate True + return True safe_locals = { 'instance': instance, 'obj': instance, diff --git a/orchestra/apps/services/models.py b/orchestra/apps/services/models.py index aa8e3351..6647e439 100644 --- a/orchestra/apps/services/models.py +++ b/orchestra/apps/services/models.py @@ -100,8 +100,19 @@ class Service(models.Model): } description = models.CharField(_("description"), max_length=256, unique=True) - content_type = models.ForeignKey(ContentType, verbose_name=_("content type")) - match = models.CharField(_("match"), max_length=256, blank=True) + content_type = models.ForeignKey(ContentType, verbose_name=_("content type"), + help_text=_("Content type of the related service objects.")) + match = models.CharField(_("match"), max_length=256, blank=True, + help_text=_( + "Python expression " + "that designates wheter a content_type object is related to this service " + "or not, always evaluates True when left blank. " + "Related instance can be instantiated with instance keyword or " + "content_type.model_name.
" + " databaseuser.type == 'MYSQL'
" + " miscellaneous.active and miscellaneous.service.name.lower() == 'domain .es'
" + " contractedplan.plan.name == 'association_fee''
" + " instance.active")) handler_type = models.CharField(_("handler"), max_length=256, blank=True, help_text=_("Handler used for processing this Service. A handler " "enables customized behaviour far beyond what options " @@ -132,8 +143,16 @@ class Service(models.Model): " membership fee or not")) # Pricing metric = models.CharField(_("metric"), max_length=256, blank=True, - help_text=_("Metric used to compute the pricing rate. " - "Number of orders is used when left blank.")) + help_text=_( + "Python expression " + "used for obtinging the metric value for the pricing rate computation. " + "Number of orders is used when left blank. Related instance can be instantiated " + "with instance keyword or content_type.model_name.
" + " max((mailbox.resources.disk.allocated or 0) -1, 0)
" + " miscellaneous.amount
" + " max((account.resources.traffic.used or 0) -" + " getattr(account.miscellaneous.filter(is_active=True," + " 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,