Question

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!

Was it helpful?

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)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top