Question

I'm making a django site with plugins. Each plugin is a simple django app depending on a main one (or even other plugins).

While dependency between applications/plugins are clear to me, it should be acceptable to add column (as foreign key to plugin specific models) via monkey patching to avoid the main app to depends on the plugin.

Since main application already have a south management and so have all plugins, I can't change migrations directory in settings for those modules.

So, How do I monkey patch a south application model from an other south application?

ps: I'm French, feel free to correct my question if you spot any error or to ask anything if I'm unclear.

Edit: I added an answer on how I do now on django migrations.

Was it helpful?

Solution

For now my best solution have been to make my own migration file in the plug-in (which implied add tables in the models dictionary of the migration file).

I'll see later on next migration if all models will follow automatically.

In my new migration file:

class Migration(SchemaMigration):
    def forwards(self, orm):
        db.add_column(u'remoteapp_model', 'fieldname',
                      self.gf('django.db.models.fields.related.ForeignKey',
                      (to=orm["my_plugin.MyModel"], default=None, null=True, blank=True),
                      keep_default=False)


    def backwards(self, orm):
        db.delete_column(u'remoteapp_model', 'fieldname')

    # for models, you may want to copy from a previous migration file
    # and add from the models of the main application the related tables
    models = {} 

In my models file:

from remoteapp.models import RemoteModel
from django.db import models

class MyModel(models.Model):
    pass

models.ForeignKey(MyModel, null=True, blank=True,
                  default=None).contribute_to_class(RemoteModel, 'my_model')

OTHER TIPS

I post a new answer as I migrated to django 1.7 and django migration, the solution was not obvious, I had to create my own migration class to add foreign key to remote table.

from django.db.migrations import AddField

class AddRemoteField(AddField):
    def __init__(self, remote_app, *args, **kwargs):
        super(AddRemoteField, self).__init__(*args, **kwargs)
        self.remote_app = remote_app

    def state_forwards(self, app_label, *args, **kwargs):
        super(AddRemoteField, self).state_forwards(self.remote_app, *args, **kwargs)

    def database_forwards(self, app_label, *args, **kwargs):
        super(AddRemoteField, self).database_forwards(
            self.remote_app, *args, **kwargs)

    def database_backwards(self, app_label, *args, **kwargs):
        super(AddRemoteField, self).database_backwards(
            self.remote_app, *args, **kwargs)

And then I make a migration file:

from __future__ import unicode_literals

from django.db import models, migrations
from my_app.tools import AddRemoteField
from my_app.models import Client


class Migration(migrations.Migration):

    dependencies = [
        ('anikit', '0002_manual_user_migration'),
    ]

    operations = [
        AddRemoteField(
            remote_app='auth',
            model_name='user',
            name='client',
            field=models.ForeignKey(Client, verbose_name='client',
                                    null=True, blank=True),
            preserve_default=True,
        ),
    ]

For the love of christ don't use monkeypatching for this. Use inheritance, that's what it's for.

Simply have your plugins which need to work with more fields extend existing models, and then use one of the many techniques to get the most specialised class instance of a model. I use the class below as a mixin on all classes which will be used in this way, in order to achieve this.

class Specializable(object):

    @classmethod
    def all_classes(selfclass):
        """ Returns the class this is called on, plus its known subclasses """
        subs = selfclass.__subclasses__()
        subs.insert(0, selfclass)
        return subs

    def get_sub_instance(self):
        """ Gets concrete instance of object of deepest subtype which has its ancestor link pointing to this object (depth first search behaviour). """
        selftype = type(self)
        for klass in selftype.__subclasses__():
            this_ptr_name = klass._meta.get_ancestor_link(selftype).name
            try:
                sub = klass.objects.get(**{this_ptr_name: self})
                subsub = sub.get_sub_instance()
                if subsub: return subsub
                else: return sub
            except ObjectDoesNotExist:
                pass

    @classmethod
    def new_with_translator(selfclazz, name):
        def new(cls, *args, **kwargs):
            selfclazz.create_subclass_translator(cls, install = name)
            return models.Model.__new__(cls, *args, **kwargs)

        return new

    @classmethod
    def create_subclass_translator(selfclazz, Baseclass, install=None):
        """ Creates a classmethod object for installation on Baseclass,
        which acts as a factory for instances of subclasses of Baseclass,
        when called on that subclass. The factory takes as input an instance
        of a subclass (old_instance), and a dictionary of properties with which
        to initialise the new instance. It also installs the base class instance
        of old_instance as the base instance for the new instance. All three will
        share the same pk.

        if install is set, this will also install the function on Baseclass under
        that name if it does not have a property of that name. """

        def create_from_other_instance(selfclass, old_instance, properties):
            """ Creates an instance of this class using properties and old_instance.
            In particular, it will try to re-use the superclass instance of old_instance.
            properties should contain all of the fields for this class (but need not include the superclass values)
            This *must* be called on a subclass of the baseclass - it will give odd results if called on the baseclass itself.
            This will NOT delete old_instance and it will NOT SAVE the object created - the caller is responsible for
            those things."""

            if selfclass is Baseclass: raise TypeError("This method cannot be used on the base class")

            ancestor_link = selfclass._meta.get_ancestor_link(Baseclass).name
            properties.update({ancestor_link: getattr(old_instance,ancestor_link)})
            for f in get_model_fields(Baseclass):
                val = getattr(old_instance, f)
                if val and not properties.get(f):
                    properties[f] = val

            return selfclass(**properties)

        new_method = classmethod(create_from_other_instance)

        if install and not hasattr(Baseclass, install):
            setattr(Baseclass, install, new_method)

        return new_method

Here's an example of its use, from some of my code:

#KYCable inherits from Model, so must precede Model
class Director(KYCable, models.Model, Specializable, DateFormatter, AdminURL, Supercedable):
    def get_individual(self):
        return self.get_sub_instance().get_individual()

As you can see, Specializable classes need to be aware that their methods may be overridden by subclasses, and be coded appropriately.

If your plugin knows about the subclass relationships, then it can use tricks like searching on the same model id in subclasses it knows about, to get the corresponding subclass model instance when presented with a superclass instance.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top