Fixed bug with m2m without intermediary model backends not being executed

This commit is contained in:
Marc Aymerich 2015-02-27 16:57:39 +00:00
parent f2dbc0ed42
commit 66fa3bb4c6
11 changed files with 78 additions and 40 deletions

View File

@ -77,10 +77,11 @@ class Account(auth.AbstractBaseUser):
self.save(update_fields=['is_active']) self.save(update_fields=['is_active'])
# Trigger save() on related objects that depend on this account # Trigger save() on related objects that depend on this account
for rel in self._meta.get_all_related_objects(): for rel in self._meta.get_all_related_objects():
if not rel.model in services: source = getattr(rel, 'related_model', rel.model)
if not source in services:
continue continue
try: try:
rel.model._meta.get_field_by_name('is_active') source._meta.get_field_by_name('is_active')
except models.FieldDoesNotExist: except models.FieldDoesNotExist:
continue continue
else: else:

View File

@ -171,7 +171,7 @@ class MailmanTraffic(ServiceMonitor):
def monitor(self, mail_list): def monitor(self, mail_list):
context = self.get_context(mail_list) context = self.get_context(mail_list)
self.append( self.append(
'monitor %(object_id)i %(last_date)s "%(list_name)s" "%(log_file)s{,.1}"' % context) 'monitor %(object_id)i %(last_date)s "%(list_name)s" "%(mailman_log)s{,.1}"' % context)
def get_context(self, mail_list): def get_context(self, mail_list):
last_date = timezone.localtime(self.get_last_date(mail_list.pk)) last_date = timezone.localtime(self.get_last_date(mail_list.pk))

View File

@ -117,6 +117,10 @@ class PasswdVirtualUserBackend(ServiceController):
class PostfixAddressBackend(ServiceController): class PostfixAddressBackend(ServiceController):
verbose_name = _("Postfix address") verbose_name = _("Postfix address")
model = 'mailboxes.Address' model = 'mailboxes.Address'
# TODO
related_models = (
('mailboxes.Mailbox', 'addresses'),
)
def include_virtual_alias_domain(self, context): def include_virtual_alias_domain(self, context):
self.append(textwrap.dedent(""" self.append(textwrap.dedent("""

View File

@ -115,7 +115,7 @@ class Address(models.Model):
def destination(self): def destination(self):
destinations = list(self.mailboxes.values_list('name', flat=True)) destinations = list(self.mailboxes.values_list('name', flat=True))
if self.forward: if self.forward:
destinations += self.forward destinations += self.forward.split()
return ' '.join(destinations) return ' '.join(destinations)
def clean(self): def clean(self):

View File

@ -1,6 +1,6 @@
from functools import partial from functools import partial
from django.db.models.loading import get_model from django.apps import apps
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -93,12 +93,16 @@ class ServiceBackend(plugins.Plugin):
return None return None
@classmethod @classmethod
def get_backends(cls, instance=None, action=None): def get_backends(cls, instance=None, action=None, active=True):
from .models import Route
backends = cls.get_plugins() backends = cls.get_plugins()
included = [] included = []
if active:
active_backends = Route.objects.filter(is_active=True).values_list('backend', flat=True)
# Filter for instance or action # Filter for instance or action
if instance or action:
for backend in backends: for backend in backends:
if active and backend.get_name() not in active_backends:
continue
include = True include = True
if instance: if instance:
opts = instance._meta opts = instance._meta
@ -109,8 +113,7 @@ class ServiceBackend(plugins.Plugin):
include = False include = False
if include: if include:
included.append(backend) included.append(backend)
backends = included return included
return backends
@classmethod @classmethod
def get_backend(cls, name): def get_backend(cls, name):
@ -118,7 +121,7 @@ class ServiceBackend(plugins.Plugin):
@classmethod @classmethod
def model_class(cls): def model_class(cls):
return get_model(cls.model) return apps.get_model(cls.model)
@property @property
def scripts(self): def scripts(self):

View File

@ -1,7 +1,7 @@
from threading import local from threading import local
from django.core.urlresolvers import resolve from django.core.urlresolvers import resolve
from django.db.models.signals import pre_delete, post_save from django.db.models.signals import pre_delete, post_save, m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from django.http.response import HttpResponseServerError from django.http.response import HttpResponseServerError
@ -23,6 +23,17 @@ def pre_delete_collector(sender, *args, **kwargs):
if sender not in [BackendLog, Operation]: if sender not in [BackendLog, Operation]:
OperationsMiddleware.collect(Operation.DELETE, **kwargs) OperationsMiddleware.collect(Operation.DELETE, **kwargs)
@receiver(m2m_changed, dispatch_uid='orchestration.m2m_collector')
def m2m_collector(sender, *args, **kwargs):
# m2m relations without intermediary models are shit
# model.post_save is not sent and by the time related.post_save is sent
# the objects are not accessible with RelatedManager.all()
# We have to use this inefficient technique of collecting the instances via m2m_changed.post_add
if kwargs.pop('action') == 'post_add':
for pk in kwargs['pk_set']:
kwargs['instance'] = kwargs['model'].objects.get(pk=pk)
OperationsMiddleware.collect(Operation.SAVE, **kwargs)
class OperationsMiddleware(object): class OperationsMiddleware(object):
""" """
@ -51,19 +62,25 @@ class OperationsMiddleware(object):
pending_operations = cls.get_pending_operations() pending_operations = cls.get_pending_operations()
for backend in ServiceBackend.get_backends(): for backend in ServiceBackend.get_backends():
# Check if there exists a related instance to be executed for this backend # Check if there exists a related instance to be executed for this backend
instance = None instances = []
if backend.is_main(kwargs['instance']): if backend.is_main(kwargs['instance']):
instance = kwargs['instance'] instances = [(kwargs['instance'], action)]
else: else:
candidate = backend.get_related(kwargs['instance']) candidate = backend.get_related(kwargs['instance'])
if candidate: if candidate:
if candidate.__class__.__name__ == 'ManyRelatedManager':
candidates = candidate.all()
else:
candidates = [candidate]
for candidate in candidates:
# Check if a delete for candidate is in pending_operations
delete = Operation.create(backend, candidate, Operation.DELETE) delete = Operation.create(backend, candidate, Operation.DELETE)
if delete not in pending_operations: if delete not in pending_operations:
instance = candidate
# related objects with backend.model trigger save() # related objects with backend.model trigger save()
action = Operation.SAVE action = Operation.SAVE
instances.append((candidate, action))
for instance, action in instances:
# Maintain consistent state of pending_operations based on save/delete behaviour # Maintain consistent state of pending_operations based on save/delete behaviour
if instance is not None:
# Prevent creating a deleted instance by deleting existing saves # Prevent creating a deleted instance by deleting existing saves
if action == Operation.DELETE: if action == Operation.DELETE:
save = Operation.create(backend, instance, Operation.SAVE) save = Operation.create(backend, instance, Operation.SAVE)

View File

@ -7,7 +7,7 @@ from orchestra.forms.widgets import ShowTextWidget, ReadOnlyWidget
class ResourceForm(forms.ModelForm): class ResourceForm(forms.ModelForm):
verbose_name = forms.CharField(label=_("Name"), required=False, verbose_name = forms.CharField(label=_("Name"), required=False,
widget=ShowTextWidget(bold=True)) widget=ShowTextWidget(bold=True))
allocated = forms.IntegerField(label=_("Allocated")) allocated = forms.DecimalField(label=_("Allocated"))
unit = forms.CharField(label=_("Unit"), widget=ShowTextWidget(), required=False) unit = forms.CharField(label=_("Unit"), widget=ShowTextWidget(), required=False)
class Meta: class Meta:

View File

@ -25,6 +25,7 @@ class SystemUserBackend(ServiceController):
useradd %(username)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s useradd %(username)s --home %(home)s --password '%(password)s' --shell %(shell)s %(groups_arg)s
fi fi
mkdir -p %(home)s mkdir -p %(home)s
chmod 750 %(home)s
chown %(username)s:%(username)s %(home)s""" % context chown %(username)s:%(username)s %(home)s""" % context
)) ))
for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS: for member in settings.SYSTEMUSERS_DEFAULT_GROUP_MEMBERS:
@ -46,7 +47,7 @@ class SystemUserBackend(ServiceController):
# TODO setacl # TODO setacl
def delete_home(self, context, user): def delete_home(self, context, user):
if user.is_main: if user.home.rstrip('/') == user.get_base_home().rstrip('/'):
# TODO delete instead of this shit # TODO delete instead of this shit
context['deleted'] = context['home'].rstrip('/') + '.deleted' context['deleted'] = context['home'].rstrip('/') + '.deleted'
self.append("mv %(home)s %(deleted)s" % context) self.append("mv %(home)s %(deleted)s" % context)

View File

@ -13,7 +13,7 @@ from . import settings
class Website(models.Model): class Website(models.Model):
""" Models a web site, also known as virtual host """ """ Models a web site, also known as virtual host """
name = models.CharField(_("name"), max_length=128, unique=True, name = models.CharField(_("name"), max_length=128,
validators=[validators.validate_name]) validators=[validators.validate_name])
account = models.ForeignKey('accounts.Account', verbose_name=_("Account"), account = models.ForeignKey('accounts.Account', verbose_name=_("Account"),
related_name='websites') related_name='websites')
@ -25,12 +25,21 @@ class Website(models.Model):
contents = models.ManyToManyField('webapps.WebApp', through='websites.Content') contents = models.ManyToManyField('webapps.WebApp', through='websites.Content')
is_active = models.BooleanField(_("active"), default=True) is_active = models.BooleanField(_("active"), default=True)
class Meta:
unique_together = ('name', 'account')
def __unicode__(self): def __unicode__(self):
return self.name return self.name
@property @property
def unique_name(self): def unique_name(self):
return "%s-%i" % (self.name, self.pk) return settings.WEBSITES_UNIQUE_NAME_FORMAT % {
'id': self.id,
'pk': self.pk,
'account': self.account.username,
'port': self.port,
'name': self.name,
}
@property @property
def protocol(self): def protocol(self):
@ -53,8 +62,8 @@ class Website(models.Model):
def get_www_log_path(self): def get_www_log_path(self):
context = { context = {
'user_home': self.account.main_systemuser.get_home(), 'home': self.account.main_systemuser.get_home(),
'username': self.account.username, 'account': self.account.username,
'name': self.name, 'name': self.name,
'unique_name': self.unique_name 'unique_name': self.unique_name
} }

View File

@ -2,6 +2,10 @@ from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
WEBSITES_UNIQUE_NAME_FORMAT = getattr(settings, 'WEBSITES_UNIQUE_NAME_FORMAT',
'%(account)s-%(name)s')
WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', ( WEBSITES_PORT_CHOICES = getattr(settings, 'WEBSITES_PORT_CHOICES', (
(80, 'HTTP'), (80, 'HTTP'),
(443, 'HTTPS'), (443, 'HTTPS'),
@ -87,8 +91,7 @@ WEBSITES_WEBALIZER_PATH = getattr(settings, 'WEBSITES_WEBALIZER_PATH',
WEBSITES_WEBSITE_WWW_LOG_PATH = getattr(settings, 'WEBSITES_WEBSITE_WWW_LOG_PATH', WEBSITES_WEBSITE_WWW_LOG_PATH = getattr(settings, 'WEBSITES_WEBSITE_WWW_LOG_PATH',
# %(user_home)s %(name)s %(unique_name)s %(username)s '/var/log/apache2/virtual/%(unique_name)s.log')
'/var/log/apache2/virtual/%(unique_name)s')
WEBSITES_TRAFFIC_IGNORE_HOSTS = getattr(settings, 'WEBSITES_TRAFFIC_IGNORE_HOSTS', WEBSITES_TRAFFIC_IGNORE_HOSTS = getattr(settings, 'WEBSITES_TRAFFIC_IGNORE_HOSTS',

View File

@ -95,7 +95,7 @@ INSTALLED_APPS = (
'admin_tools', 'admin_tools',
'admin_tools.theming', 'admin_tools.theming',
'admin_tools.menu', 'admin_tools.menu',
'admin_tools.dashboard', # 'admin_tools.dashboard',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'passlib.ext.django', 'passlib.ext.django',