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!
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:
settings.py
to restrict which models are allowed in this
generic foreign key;{% 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)