Search code examples
djangodrycode-reusedjango-cms

How do I adapt Django ForeignKey to a reusable app?


I have written a carousel plugin for Django-CMS which displays screenshots. The underlying model has some carousel-related parameters (height, animation style etc), and a ForeignKey to ScreenshotGroup:

class ScreenshotGroup(models.Model):
    name = models.CharField(max_length=60)
    screenshots = models.ManyToManyField(Screenshot, through="ScreenshotGroupMember")

class Screenshot(models.Model):
    name = models.CharField(max_length=60)
    desc = models.TextField(_("description"), blank=True)
    img = models.ImageField(upload_to='img/')

class CarouselPluginModel(CMSPlugin):
    group = models.ForeignKey(ScreenshotGroup)
    height = models.IntegerField()
    ...

The carousel's view method contains:

context['item_list'] = instance.group.screenshots.all()

(Actually since I'm using Django-CMS, it's in the cms_plugins.py render method, not a view method.)

The template refers to the screenshot fields via:

{% for item in item_list %}
    {{ item.name }}
    {{ item.desc }}
    ...{{ item.img }}...
{% endfor %}

My question is: I want to generalise my carousel plugin to reuse it in other projects, so that does not depend on the Screenshot model. I can replace the contents of the template's for loop with an include to allow each project to specify how to display the item in a carousel. But how do I generalise the CarouselPluginModel's ForeignKey?

In any particular application, I only want one type of model allowed (ScreenshotGroup in my example) - I don't want the admin console to allow any other models to be included.

Thanks!


Solution

  • Based on the Generic Foreign Key idea suggested by karthikr, here is the full solution I've adopted. The other pieces of the puzzle are:

    • using an entry in settings.py to restrict which models are allowed in this generic foreign key;
    • sniffing for the chosen model's many-to-many field;
    • using {% include "carousel_item.html" %} in the template to generalize the item display. I'll provide a default implementation in the app, but this way the ultimate user does not have to conform to the fields I pre-define.

    In models.py:

    from django.contrib.contenttypes.models import ContentType
    from django.contrib.contenttypes import generic
    from django.conf import settings
    
    allowed_models = getattr(settings, 'ALLOWED_MODELS_IN_CAROUSEL', [])
    # must be a list of dictionaries with keys: app_label and model, e.g:
    # ALLOWED_MODELS_IN_CAROUSEL=[{'app_label':'myapp', 'model':'screenshotgroup'},]
    
    fk_models = None
    if allowed_models:
        # don't like this repetition - how can I improve this?
        fk_models = models.Q(app_label = allowed_models[0]['app_label'].lower(), 
                             model = allowed_models[0]['model'].lower())
        for m in allowed_models[1:]:
            fk_models = fk_models | models.Q(app_label = m['app_label'].lower(),
                                             model = m['model'].lower())
    
    class CarouselPluginModel(CMSPlugin):
        content_type = models.ForeignKey(ContentType, limit_choices_to = fk_models)
        object_id = models.PositiveIntegerField()
        content_group = generic.GenericForeignKey('content_type', 'object_id')
        ...
    

    The view needs to find the ManyToManyField in the chosen model, e.g:

    if instance.content_group and instance.content_group._meta.many_to_many:
        m2m_fieldname = instance.content_group._meta.many_to_many[0].name
        context['item_list'] = getattr(instance.content_group, m2m_fieldname).all()
    

    The template can look like this:

    {% for item in item_list %}
        {% include "carousel_item.html" %}
    {% endfor %}
    

    And finally I'll include a recommendation that the model you use include its id in its description, since the admin panel will have to choose it by id, e.g:

    class ScreenshotGroup(models.Model):
        name = models.CharField(max_length=60)
        screenshots = models.ManyToManyField(Screenshot, through="ScreenshotGroupMember")
        def __unicode__(self):
            return u"{0} (id {1})".format(self.name, self.id)