1

I am using Django 3.2

I have written a standalone app social that has models defined like this:

from abc import abstractmethod

class ActionableModel:

    # This MUST be implemented by ALL CONCRETE sub classes
    @abstractmethod
    def get_actionable_object(self):
        pass

    # This MUST be implemented by ALL CONCRETE sub classes
    @abstractmethod
    def get_owner(self):
        pass


    def get_content_info(self):
        actionable_object = self.get_actionable_object()
        ct = ContentType.objects.get_for_model(actionable_object)
        object_id = actionable_object.id

        return (ct, object_id)      
 
    # ...


class Likeable(models.Model, ActionableModel):
    likes = GenericRelation(Like)
    likes_count = models.PositiveIntegerField(default=0, db_index=True)   
    last_liked = models.DateTimeField(editable=False, blank=True, null=True, default=None)
   
    # ...

in my project (that uses the standalone app), I have the following code:

myproject/userprofile/apps.py

class UserprofileConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'userprofile'
    verbose_name = _('User Profile')

    def ready(self):
        # Check if social app is installed
        from django.apps import apps
        if apps.is_installed("social"):
            # check some condition
            # inherit from social.Likeable 
            # ... ?

How can I dynamically get my model userprofile.models.UserAccount to derive from Likeable?

3 Answers 3

1

If I understand the requirement correctly - you need that models from the application userprofile to implement Likeable features.

So the inheritance in this case does not only allow to use implemented methods but also makes it to have the same common fields in database (likes, likes_count, etc)

Which means that such an inheritance also requires migration script to be generated (to add the necessary fields in the database). Thus, doing it in runtime when application is ready might be even not possible since you have to have model defined for migrations.

However, even if there is a way to make such "dynamic" inheritance on-the-fly - I would highly recommend not to go this way because it's really not transparent when some features of the models are defined in such a hidden way.

So the good solution for your request is simple - just do not do it dynamically. I.e.:

myproject/userprofile/models.py

from social.models import Likeable

class UserAccount(Likeable):
    # the definition of the user account class

And so other models that requires Likeable features will just inherit from it directly.

To your question of

How can I dynamically get my model userprofile.models.UserAccount

It can be done via:

from django.apps import apps

app_models = apps.get_app_config('userprofile').get_models()

And as a follow-up for dynamical inheritance you can refer here How to dynamically change base class of instances at runtime?

UPD - a bit different approach to solve the problem

Instead of using inheritance of the classes which is probably even not possible in this case - target models can be extended via relationship with Likeable entity in database.

I.e. instead of carrying Likeable inherited - it can be provided as one-to-one relationship which can be constructed in runtime as follows:

class UserprofileConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'userprofile'
    verbose_name = _('User Profile')

    def ready(self):
        # Check if social app is installed
        from django.apps import apps
        from django.db import models

        if apps.is_installed("social"):
            for obj in apps.get_app_config(self.name).get_models():
                # add relationship
                obj.add_to_class(
                    'likeable', 
                    models.OneToOneField(Likeable, on_delete=models.SET_NULL, null=True
                )

Then, methods of likeable can be accessed on the models of userprofile via likeable field. I.e.:

u = UserAccount.objects.last()
u.likeable.get_content_info()
Sign up to request clarification or add additional context in comments.

6 Comments

You answered a question you wanted - NOT the one I asked. I specifically asked about dynamic inheritance, because I am unable to trivially derive from the base class in the manner in which you did. Userprofile is a standalone app, and the Likeable interface is in a standalone app too. If the Likeable Interface is present (app is installed), then the Userprofile class should dynamically inherit the Likeable interface.
@HomunculusReticulli, I believe the dynamic inheritance is not really possible due to complexity of django classes. However, there can be easier solution without involving inheritance - see answer update
Hmm, thanks for pointing out the fact that migrations WILL need to be carried out - I had (foolishly), not taken that into consideration - and my Likeable class was abstract (as you correctly guessed). That said, you are also going to run a migration after adding a new field to the model - so the migration issue is not sidestepped by your alternative solution (which is composition, not inheritance).
@HomunculusReticulli, migrations will be generated well in that case. (Adding foreign key likeable to all models) - I've checked that it works
I'm thinking of using your solution (it will require a fair bit of refactoring to my codebase). Is there an opposite of add_to_class that can remove a field (so that field is removed when app is uninstalled?).
|
1

To dynamically inherit another class, we usually do:

def make_class(cls, cls_to_inherit):
    return type(
        cls.__name__,
        (cls, cls_to_inherit),
        {},
    )

To dynamically inherit another Django model that has metaclass=ModelBase, we do:

def make_model(model, model_to_inherit):
    model._meta.abstract = True
    if model._meta.pk.auto_created:
        model._meta.local_fields.remove(model._meta.pk)
    return type(
        model.__name__,
        (model, model_to_inherit),
        {'__module__': model.__module__},
    )

Usage:

class UserprofileConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'userprofile'
    verbose_name = _('User Profile')

    def ready(self):
        # Check if social app is installed
        from django.apps import apps
        if apps.is_installed("social"):
            # check some condition
            # inherit from social.models.Likeable
            from social.models import Likeable
            from . import models
            models.UserAccount = make_model(models.UserAccount, Likeable)

9 Comments

I like this approach, since it does what I want. One slight complication (which I foolishly omitted in my question) - was the fact that the class Actionable is actually an abstract class - so there will have to be migrations ... :/ Any chance of a few lines extra of code to show how to migrate the Likeable fields on to the user model?
Do you mean database migrations? This solution will generate those migrations when you run python manage.py makemigrations as usual.
Thanks. I am now getting the following errors: admin.LogEntry.user: (fields.E300) Field defines a relation with model 'UserAccount', which is either not installed, or is abstract. admin.LogEntry.user: (fields.E311) 'UserAccount.id' must be unique because it is referenced by a foreign key. HINT: Add unique=True to this field or add a UniqueConstraint (without condition) in the model Meta.constraints.
Can you create a repro project on GitHub?
Yeah, it might be worth the effort of pulling it out into a separate project. There is a RuntimeWarning issued before the error message I pasted above: RuntimeWarning: Model 'userprofile.useraccount' was already registered. Reloading models is not advised as it can lead to inconsistencies, most notably with related models. new_class._meta.apps.register_model(new_class._meta.app_label, new_class) - so looks like somethings gone awry...
|
0

You can do this at class creation time in your models.py, but it probably causes more problems than it solves.

# myproject/userprofile/models.py
from django.db import models
from django.apps import apps

if apps.is_installed('someotherapp'):
    from someotherapp.models import Likeable
    BaseKlass = Likeable
else:
    BaseKlass = models.Model

class UserAccount(BaseKlass):
    username = models.CharField(max_length=64)

You can't control inheritance for a model class that has already been created from your AppConfig.ready. Models in your models.py are already created by the point in time that Django calls the ready method. Django's model metaclass, which performs essential operations to correctly form fields, only works at class creation time; you can't edit a model after it has been created.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.