How to agnostically link any object/Model from ano

2020-07-30 02:59发布

问题:

I'm writing a simple CMS based on Django. Most content management systems rely on having a fixed page, on a fixed URL, using a template that has one or many editable regions. To have an editable region, you require a Page. For the system to work out which page, you require the URL.

The problem comes when you're no longer dealing with "pages" (be those FlatPages pages, or something else), but rather instances from another Model. For example if I have a Model of products, I may wish to create a detail page that has multiple editable regions within.

I could build those regions into the Model but in my case, there are several Models and is a lot of variance in how much data I want to show.

Therefore, I want to build the CMS at template level and specify what a block (an editable region) is based on the instance of "page" or the model it uses.

I've had the idea that perhaps I could dump custom template tags on the page like this:

{% block unique_object "unique placeholder name" %}

And that would find a "block" based on the two arguments passed in. An example:

<h1>{{ product_instance.name }}</h1>
{% block product_instance "detail: product short description" %}
{% block product_instance "detail: product video" %}
{% block product_instance "detail: product long description" %}

Sounds spiffy, right? Well the problem I'm running into is how do I create a "key" for a zone so I can pull the correct block out? I'll be dealing with a completely unknown object (it could be a "page" object, a URL, a model instance, anything - it could even be a boat</fg>).

Other Django micro-applications must do this. You can tag anything with django-tagging, right? I've tried to understand how that works but I'm drawing blanks.

So, firstly, am I mad? And assuming I not, and this looks like a relatively sane idea to persue, how should I go about linking an object+string to a block/editable-region?

Note: Editing will be done on-the-page so there's no real issue in letting the users edit the zones. I won't have to do any reverse-mumbo-jumbo in the admin. My eventual dream is to allow a third argument to specify what sort of content area this is (text, image, video, etc). If you have any comments on any of this, I'm happy to read them!

回答1:

django-tagging uses Django's contenttypes framework. The docs do a much better job of explaining it than I can, but the simplest description of it would be "generic foreign key that can point to any other model."

This may be what you are looking for, but from your description it also sounds like you want to do something very similar to some other existing projects:

  • django-flatblocks ("... acts like django.contrib.flatpages but for parts of a page; like an editable help box you want show alongside the main content.")

  • django-better-chunks ("Think of it as flatpages for small bits of reusable content you might want to insert into your templates and manage from the admin interface.")

and so on. If these are similar then they'll make a good starting point for you.



回答2:

You want a way to display some object-specific content on a generic template, given a specific object, correct?

In order to support both models and other objects, we need two intermediate models; one to handle strings, and one to handle models. We could do it with one model, but this is less performant. These models will provide the link between content and string/model.

from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes import generic

CONTENT_TYPE_CHOICES = (
    ("video", "Video"),
    ("text", "Text"),
    ("image", "Image"),
)

def _get_template(name, type):
    "Returns a list of templates to load given a name and a type"
    return ["%s_%s.html" % (type, name), "%s.html" % name, "%s.html" % type]

class ModelContentLink(models.Model):
    key = models.CharField(max_length=255) # Or whatever you find appropriate
    type = models.CharField(max_length=31, choices= CONTENT_TYPE_CHOICES)
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    object = generic.GenericForeignKey('content_type', 'object_id')

    def get_template(self):
        model_name = self.object.__class__.__name__.lower()
        return _get_template(model_name, self.type)

class StringContentLink(models.Model):
    key = models.CharField(max_length=255) # Or whatever length you find appropriate
    type = models.CharField(max_length=31, choices= CONTENT_TYPE_CHOICES)
    content = models.TextField()

    def get_template(self):
        return _get_template(self.content, self.type)

Now, all we need is a template tag to grab these, and then try to load the templates given by the models' get_template() method. I'm a bit pressed on time so I'll leave it at this and update it in ~1 hour. Let me know if you think this approach seems fine.



回答3:

It's pretty straightforward to use the contenttypes framework to implement the lookup strategy you are describing:

class Block(models.Model):
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    object = generic.GenericForeignKey() # not actually used here, but may be handy
    key = models.CharField(max_length=255)
    ... other fields ...

    class Meta:
       unique_together = ('content_type', 'object_id', 'key')

def lookup_block(object, key):
    return Block.objects.get(content_type=ContentType.objects.get_for_model(object),
                             object_id=object.pk,
                             key=key)

@register.simple_tag
def block(object, key)
   block = lookup_block(object, key)
   ... generate template content using 'block' ...

One gotcha to be aware of is that you can't use the object field in the Block.objects.get call, because it's not a real database field. You must use content_type and object_id.

I called the model Block, but if you have some cases where more than one unique (object, key) tuple maps to the same block, it may in fact be an intermediate model that itself has a ForeignKey to your actual Block model or to the appropriate model in a helper app like the ones Van Gale has mentioned.