diff --git a/orchestra/contrib/accounts/models.py b/orchestra/contrib/accounts/models.py
index 4977427d..a0d264e6 100644
--- a/orchestra/contrib/accounts/models.py
+++ b/orchestra/contrib/accounts/models.py
@@ -80,7 +80,7 @@ class Account(auth.AbstractBaseUser):
def disable(self):
self.is_active = False
- self.save(update_fields=['is_active'])
+ self.save(update_fields=('is_active',))
self.notify_related()
def get_services_to_disable(self):
@@ -93,7 +93,7 @@ class Account(auth.AbstractBaseUser):
def notify_related(self):
""" Trigger save() on related objects that depend on this account """
for obj in self.get_services_to_disable():
- OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=[])
+ OperationsMiddleware.collect(Operation.SAVE, instance=obj, update_fields=())
def send_email(self, template, context, email_from=None, contacts=[], attachments=[], html=None):
contacts = self.contacts.filter(email_usages=contacts)
diff --git a/orchestra/contrib/mailboxes/models.py b/orchestra/contrib/mailboxes/models.py
index ac0e2704..4ce7a353 100644
--- a/orchestra/contrib/mailboxes/models.py
+++ b/orchestra/contrib/mailboxes/models.py
@@ -13,9 +13,9 @@ class Mailbox(models.Model):
CUSTOM = 'CUSTOM'
name = models.CharField(_("name"), max_length=64, unique=True,
- help_text=_("Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only."),
+ help_text=_("Required. 30 characters or fewer. Letters, digits and ./-/_ only."),
validators=[
- RegexValidator(r'^[\w.@+-]+$', _("Enter a valid mailbox name.")),
+ RegexValidator(r'^[\w.-]+$', _("Enter a valid mailbox name.")),
])
password = models.CharField(_("password"), max_length=128)
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
diff --git a/orchestra/contrib/mailer/admin.py b/orchestra/contrib/mailer/admin.py
index 5ed378da..ca6c948d 100644
--- a/orchestra/contrib/mailer/admin.py
+++ b/orchestra/contrib/mailer/admin.py
@@ -120,7 +120,7 @@ class MessageAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super(MessageAdmin, self).get_queryset(request)
- return qs.annotate(Count('logs')).prefetch_related('logs')
+ return qs.annotate(Count('logs')).prefetch_related('logs').defer('content')
def send_pending_view(self, request):
task(send_pending).apply_async()
diff --git a/orchestra/contrib/orchestration/admin.py b/orchestra/contrib/orchestration/admin.py
index c7242b96..acaef8c2 100644
--- a/orchestra/contrib/orchestration/admin.py
+++ b/orchestra/contrib/orchestration/admin.py
@@ -31,6 +31,7 @@ class RouteAdmin(ExtendedModelAdmin):
)
list_editable = ('host', 'match', 'async', 'is_active')
list_filter = ('host', 'is_active', 'async', 'backend')
+ list_prefetch_related = ('host',)
ordering = ('backend',)
add_fields = ('backend', 'host', 'match', 'async', 'is_active')
change_form = RouteForm
@@ -60,7 +61,16 @@ class RouteAdmin(ExtendedModelAdmin):
""" Provides dynamic help text on backend form field """
if db_field.name == 'backend':
kwargs['widget'] = RouteBackendSelect('this.id', self.BACKEND_HELP_TEXT, self.DEFAULT_MATCH)
- return super(RouteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+ field = super(RouteAdmin, self).formfield_for_dbfield(db_field, **kwargs)
+ if db_field.name == 'host':
+ # Cache host choices
+ request = kwargs['request']
+ choices = getattr(request, '_host_choices_cache', None)
+ if choices is None:
+ request._host_choices_cache = choices = list(field.choices)
+ field.choices = choices
+ return field
+
def get_form(self, request, obj=None, **kwargs):
""" Include dynamic help text for existing objects """
diff --git a/orchestra/contrib/payments/models.py b/orchestra/contrib/payments/models.py
index 28fa835d..2d4e0409 100644
--- a/orchestra/contrib/payments/models.py
+++ b/orchestra/contrib/payments/models.py
@@ -134,21 +134,21 @@ class Transaction(models.Model):
def mark_as_processed(self):
self.check_state(self.WAITTING_PROCESSING)
self.state = self.WAITTING_EXECUTION
- self.save(update_fields=['state', 'modified_at'])
+ self.save(update_fields=('state', 'modified_at'))
def mark_as_executed(self):
self.check_state(self.WAITTING_EXECUTION)
self.state = self.EXECUTED
- self.save(update_fields=['state', 'modified_at'])
+ self.save(update_fields=('state', 'modified_at'))
def mark_as_secured(self):
self.check_state(self.EXECUTED)
self.state = self.SECURED
- self.save(update_fields=['state', 'modified_at'])
+ self.save(update_fields=('state', 'modified_at'))
def mark_as_rejected(self):
self.state = self.REJECTED
- self.save(update_fields=['state', 'modified_at'])
+ self.save(update_fields=('state', 'modified_at'))
class TransactionProcess(models.Model):
@@ -187,18 +187,18 @@ class TransactionProcess(models.Model):
self.state = self.EXECUTED
for transaction in self.transactions.all():
transaction.mark_as_executed()
- self.save(update_fields=['state'])
+ self.save(update_fields=('state',))
def abort(self):
self.check_state(self.CREATED, self.EXCECUTED)
self.state = self.ABORTED
for transaction in self.transaction.all():
transaction.mark_as_aborted()
- self.save(update_fields=['state'])
+ self.save(update_fields=('state',))
def commit(self):
self.check_state(self.CREATED, self.EXECUTED)
self.state = self.COMMITED
for transaction in self.transactions.processing():
transaction.mark_as_secured()
- self.save(update_fields=['state'])
+ self.save(update_fields=('state',))
diff --git a/orchestra/contrib/saas/backends/__init__.py b/orchestra/contrib/saas/backends/__init__.py
index 45a66f6c..678c5b9b 100644
--- a/orchestra/contrib/saas/backends/__init__.py
+++ b/orchestra/contrib/saas/backends/__init__.py
@@ -89,7 +89,7 @@ class ApacheTrafficByHost(ServiceMonitor):
sys.stderr.write(str(e)+'\\n')
for opts in sites.values():
ini_date, object_id, size = opts
- sys.stdout.write('%s %s\n' % (object_id, size))
+ sys.stdout.write('%s %s\\n' % (object_id, size))
""").format(**context)
)
diff --git a/orchestra/contrib/saas/forms.py b/orchestra/contrib/saas/forms.py
index 3d317683..7007ba00 100644
--- a/orchestra/contrib/saas/forms.py
+++ b/orchestra/contrib/saas/forms.py
@@ -70,7 +70,7 @@ class SaaSPasswordForm(SaaSBaseForm):
return password2
def save(self, commit=True):
- obj = super(SoftwareServiceForm, self).save(commit=commit)
+ obj = super(SaaSPasswordForm, self).save(commit=commit)
if not self.is_change:
obj.set_password(self.cleaned_data["password1"])
return obj
diff --git a/orchestra/contrib/saas/services/phplist.py b/orchestra/contrib/saas/services/phplist.py
index 555ec816..7d73632e 100644
--- a/orchestra/contrib/saas/services/phplist.py
+++ b/orchestra/contrib/saas/services/phplist.py
@@ -1,10 +1,12 @@
from django import forms
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse
+from django.db.models import Q
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from orchestra.contrib.databases.models import Database, DatabaseUser
+from orchestra.contrib.mailboxes.models import Mailbox
from orchestra.forms.widgets import SpanWidget
from .. import settings
@@ -14,7 +16,7 @@ from .options import SoftwareService
class PHPListForm(SaaSPasswordForm):
admin_username = forms.CharField(label=_("Admin username"), required=False,
- widget=SpanWidget(display='admin'))
+ widget=SpanWidget(display='admin'))
def __init__(self, *args, **kwargs):
super(PHPListForm, self).__init__(*args, **kwargs)
@@ -26,7 +28,9 @@ class PHPListForm(SaaSPasswordForm):
class PHPListChangeForm(PHPListForm):
database = forms.CharField(label=_("Database"), required=False,
- help_text=_("Database used for this webapp."))
+ help_text=_("Database used for this instance."))
+ mailbox = forms.CharField(label=_("Bounces mailbox"), required=False,
+ help_text=_("Mailbox used for reciving bounces."), widget=SpanWidget(display=''))
def __init__(self, *args, **kwargs):
super(PHPListChangeForm, self).__init__(*args, **kwargs)
@@ -39,6 +43,18 @@ class PHPListChangeForm(PHPListForm):
db_url = reverse('admin:databases_database_change', args=(db.pk,))
db_link = mark_safe('%s' % (db_url, db.name))
self.fields['database'].widget = SpanWidget(original=db.name, display=db_link)
+ # Mailbox link
+ mailbox_id = self.instance.data.get('mailbox_id')
+ if mailbox_id:
+ try:
+ mailbox = Mailbox.objects.get(id=mailbox_id)
+ except Mailbox.DoesNotExist:
+ pass
+ else:
+ mailbox_url = reverse('admin:mailboxes_mailbox_change', args=(mailbox.pk,))
+ mailbox_link = mark_safe('%s' % (mailbox_url, mailbox.name))
+ self.fields['mailbox'].widget = SpanWidget(
+ original=mailbox.name, display=mailbox_link)
class PHPListService(SoftwareService):
@@ -50,6 +66,11 @@ class PHPListService(SoftwareService):
site_base_domain = settings.SAAS_PHPLIST_BASE_DOMAIN
def get_db_name(self):
+ context = {
+ 'name': self.instance.name,
+ 'site_name': self.instance.name,
+ }
+ return settings.SAAS_PHPLIST_DB_NAME % context
db_name = 'phplist_mu_%s' % self.instance.name
# Limit for mysql database names
return db_name[:65]
@@ -57,6 +78,13 @@ class PHPListService(SoftwareService):
def get_db_user(self):
return settings.SAAS_PHPLIST_DB_USER
+ def get_mailbox_name(self):
+ context = {
+ 'name': self.instance.name,
+ 'site_name': self.instance.name,
+ }
+ return settings.SAAS_PHPLIST_BOUNCES_MAILBOX_NAME % context
+
def get_account(self):
account_model = self.instance._meta.get_field_by_name('account')[0]
return account_model.rel.to.objects.get_main()
@@ -65,12 +93,17 @@ class PHPListService(SoftwareService):
super(PHPListService, self).validate()
create = not self.instance.pk
if create:
+ account = self.get_account()
+ # Validated Database
db_user = self.get_db_user()
try:
DatabaseUser.objects.get(username=db_user)
except DatabaseUser.DoesNotExist:
- raise ValidationError(_("Global database user for PHPList '%s' does not exists."))
- account = self.get_account()
+ raise ValidationError(
+ _("Global database user for PHPList '%(db_user)s' does not exists.") % {
+ 'db_user': db_user
+ }
+ )
db = Database(name=self.get_db_name(), account=account)
try:
db.full_clean()
@@ -78,12 +111,40 @@ class PHPListService(SoftwareService):
raise ValidationError({
'name': e.messages,
})
+ # Validate mailbox
+ mailbox = Mailbox(name=self.get_mailbox_name(), account=account)
+ try:
+ mailbox.full_clean()
+ except ValidationError as e:
+ raise ValidationError({
+ 'name': e.messages,
+ })
def save(self):
+ account = self.get_account()
+ # Database
db_name = self.get_db_name()
db_user = self.get_db_user()
- account = self.get_account()
db, db_created = account.databases.get_or_create(name=db_name, type=Database.MYSQL)
user = DatabaseUser.objects.get(username=db_user)
db.users.add(user)
self.instance.database_id = db.pk
+ # Mailbox
+ mailbox_name = self.get_mailbox_name()
+ mailbox, mb_created = account.mailboxes.get_or_create(name=mailbox_name)
+ if mb_created:
+ mailbox.set_password(settings.SAAS_PHPLIST_BOUNCES_MAILBOX_PASSWORD)
+ mailbox.save(update_fields=('password',))
+ self.instance.data.update({
+ 'mailbox_id': mailbox.pk,
+ 'mailbox_name': mailbox_name,
+ })
+
+ def delete(self):
+ account = self.get_account()
+ # delete Mailbox (database will be deleted by ORM's cascade behaviour
+ mailbox_name = self.instance.data.get('mailbox_name') or self.get_mailbox_name()
+ mailbox_id = self.instance.data.get('mailbox_id')
+ qs = Q(Q(name=mailbox_name) | Q(id=mailbox_id))
+ for mailbox in account.mailboxes.filter(qs):
+ mailbox.delete()
diff --git a/orchestra/contrib/saas/settings.py b/orchestra/contrib/saas/settings.py
index 8fe794b8..0f13678f 100644
--- a/orchestra/contrib/saas/settings.py
+++ b/orchestra/contrib/saas/settings.py
@@ -117,6 +117,13 @@ SAAS_PHPLIST_DB_HOST = Setting('SAAS_PHPLIST_DB_HOST',
help_text=_("Needed for password changing support."),
)
+SAAS_PHPLIST_BOUNCES_MAILBOX_NAME = Setting('SAAS_PHPLIST_BOUNCES_MAILBOX_NAME',
+ '%(site_name)s-list-bounces',
+)
+
+SAAS_PHPLIST_BOUNCES_MAILBOX_PASSWORD = Setting('SAAS_PHPLIST_BOUNCES_MAILBOX_PASSWORD',
+ 'secret',
+)
SAAS_PHPLIST_BASE_DOMAIN = Setting('SAAS_PHPLIST_BASE_DOMAIN',
'lists.{}'.format(ORCHESTRA_BASE_DOMAIN),
diff --git a/orchestra/contrib/saas/signals.py b/orchestra/contrib/saas/signals.py
index 95a88eca..c3354b1a 100644
--- a/orchestra/contrib/saas/signals.py
+++ b/orchestra/contrib/saas/signals.py
@@ -12,6 +12,7 @@ def type_save(sender, *args, **kwargs):
instance = kwargs['instance']
instance.service_instance.save()
+
@receiver(pre_delete, sender=SaaS, dispatch_uid='saas.service.delete')
def type_delete(sender, *args, **kwargs):
instance = kwargs['instance']