From ad2b1b143ce77f455d5ae9f17271b4d25c41afe4 Mon Sep 17 00:00:00 2001 From: Marc Aymerich Date: Tue, 22 Sep 2015 10:24:04 +0000 Subject: [PATCH] Added bounces mailbox to phplist SaaS service --- orchestra/contrib/accounts/models.py | 4 +- orchestra/contrib/mailboxes/models.py | 4 +- orchestra/contrib/mailer/admin.py | 2 +- orchestra/contrib/orchestration/admin.py | 12 +++- orchestra/contrib/payments/models.py | 14 ++-- orchestra/contrib/saas/backends/__init__.py | 2 +- orchestra/contrib/saas/forms.py | 2 +- orchestra/contrib/saas/services/phplist.py | 71 +++++++++++++++++++-- orchestra/contrib/saas/settings.py | 7 ++ orchestra/contrib/saas/signals.py | 1 + 10 files changed, 99 insertions(+), 20 deletions(-) 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']