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,