Given the nature of the site we need to manage, which has lots of time-sensitive content AND lots of relatively static content, figuring out how to deal with the more static, often free-form content was a big issue for us in formulating a plan to use Django to create Sputnik. Built into Django is the notion of flatpages – static files that are served in situations where a URL doesn't map to a normal Django view/function. We were looking for something that would allow content managers to work within the standard Django admin interface to maintain the content of their basically static, free-form pages, while still taking advantage of Django's templates for basic "window dressing" kinds of stuff (banners, standard navigation, etc.)
To accomplish this we decided we'd create a beefier version of flatpages. Here's the rundown of what we wanted out of a good static-page manager.
- Easy to Use – for many of our content managers, maintaining a handful of web pages is way out on the periphery of their normal job responsibilities. Ease of use is key.
- Managed - while these static pages may have content that doesn't change frequently, we wanted to take advantage of our Django templates and templatetags to make these static pages good team players that are reponsive to centralized management.
- Powerful – Users need to be able to edit/override any block of a template, and blocks also need decent defaults so they don't have to be overridden. We want users to be able to do as much as they are able to do.
Overview
These are the steps for using our page module:
First, an administrator must create a PageTemplate object that registers a template as accessible via the Page module. Then, the administrator relates a list of PageBlocks to that PageTemplate – these PageBlocks are the names of all the blocks that are editable in the given template.
After a PageTemplate and its PageBlocks are defined, the user can then go into the Page module, add a new page, and select a PageTemplate. The user will then be able to put content into the PageBlocks that exist on that PageTemplate.
How it's Done
Our Page app is implemented with four models (severely simplified for demo purposes):
path = models.CharField(maxlength=255)
template = models.ForeignKey('PageTemplate', null=True, blank=True)
class PageTemplateBlock(models.Model):
name = models.CharField(maxlength=255, verbose_name="Block Name")
class PageTemplate(models.Model):
name = models.CharField(maxlength=255, verbose_name="Template Name")
path = models.CharField(maxlength=255, verbose_name="Template Path")
blocks = models.ManyToManyField(PageTemplateBlock,
verbose_name='Editable Blocks')
class PageContent(models.Model):
page = models.ForeignKey(Page)
#name of block that this content relates to
block_name = models.CharField(maxlength=255)
block_content = models.TextField(blank=True)
Breaking this down, each Page has a path (which is more complex than a string in our situation – utilizing Topics – but that's for another blog post), an associated PageTemplate, and blocks of content (PageContent). Each PageContent object just has a name that relates to a block in a template and the content to put in that block.
Each PageTemplate has a name, a path to its associated template, and a list of blocks (PageTemplateBlock). These PageTemplateBlocks are just a list of the overridable blocks that are available in the associated template.
When a user creates a page, he picks the template, specifies the URL path, and fills out the blocks of content he wishes. Any blocks left empty will be left with the default, unoverridden content from the template, if there is any. And that's essentially it – the page is done!
A big question left open is how we get the URLs to actually be browsable. This is done through urls.py – at the very end of our urlpatterns, we have a catch-all expression:
(r'^(?P<urlpath>[a-zA-Z0-9_/]+)$', 'sputnik.page.views.get_static'),
Which will test every URL that got this far to see if it is a Page from our page app. If not, it will just raise a 404:
"'
Workflow:
step 1: get this path from the DB. if not, raise 404
step 2: get all the blocks within this page
step 3: assemble the blocks into a dict
step 4: render
'"
content_dict = {}
page = get_object_or_404(Page, path__exact=urlpath)
for content in page.pagecontent_set.all():
content_dict[content.block_name] = content.block_content
return render_to_response(page.template.path, content_dict)
And this will essentially give us all the functionality we need. I don't claim this to be the best solution, but it's doing what we need it to do. One area that we decided to bracket out of our initial effort is any ability to change a page's template – the logic of comparing the PageBlocks available on the original template against the blocks available on the replacement seemed like it would add way too much complexity for the occasional case that it might be useful.
If you have any suggestions, we're always open to hearing them.
-Chris Dary
P.S. As a matter of fact, I have a request of you, the reader. A large problem we've been trying to deal with comes up when a user visits the edit page. At first we would just activate 4 or 5 instances of TinyMCE. This quickly became a resource hog, though, and would bring weaker computers to a grinding halt on large pages. So we decided we would dynamically activate and deactivate TinyMCE editors when a user clicks on a block to edit it. This is slightly better, but provides little gain because the html still needs to be parsed and rendered by TinyMCE every time it is activated – which is also slow! Any ideas (as crazy or mundane as they might sound) on how to speed up this process would be greatly appreciated.






August 21st, 2006 at 10:11 am
Hiya- enjoying your posts, keep 'em coming. Can I ask — did you handle adding in your "Preview your page" buttons in submit_row by javascript/DOM injection or overriding a template block somehow? It looks to me like the submit_row fragment isn't overridable with a custom template so injection is required(?)
Apparently pre-release django had preview'ing functionality, but Adrian said nobody used it. Which seems odd to me but maybe Lawrence.com just had very careful and HTML-savvy producers. Anyway, any tips would be appreciated!
Thanks – ToddG
August 21st, 2006 at 10:41 am
Thanks for the encouraging words Todd,
As far as preview goes – thats actually a custom admin interface we implemented to look very similar to the actual django admin. So I just wrote the HTML to add a preview button that pops open a new window with a preview of what the page will look like. No funky injection necessary – just plain old javascript!
I can see how previewing would be pretty strange for a lot of models where it isn't really necessary, but here it was a pretty big deal, so I took the time to write it up custom. I wouldn't mind having the option in django, but I don't think it's really a top priority either.
Hope this answered your questions. Let me know if you've got any more. Comments are always welcomed.
-Chris Dary
August 21st, 2006 at 11:24 am
Ah so the whole admin page is a custom view — got ya. I'm exploring the teetering line between stretching the built-in admin pages and just forging ahead and writing my own overall view. Still learning where the lines are… thanks again.
August 21st, 2006 at 11:33 am
It's a tough decision – it took me a while to take the plunge too. Once I did I was pretty happy though – I feel like the Django admin interface is really nice for simple applications, but trying to wrestle with it when things get more advanced really just feels like a waste of time.
You'd be surprised how fast development goes once you decide you're starting from scratch. Using all of Django's other features without the admin is almost less of a headache once you get used to it.
August 25th, 2006 at 12:14 pm
Chris,
I am trying to implement a simpler Page app that inherits the FlatPage class and overrides the template_name attrib to select from a group of template names (via a choices tuple). I am however running into an error (when creating the page via admin) that looks like the template_name attrib is being looked over now that it's returning a choice. Had you tried an approach similar to this before jumping into the steroids version? My model and error are at http://paste.e-scribe.com/1329/. Any help would be much appreciated.
Thanks,
Adam
August 25th, 2006 at 2:02 pm
Hi Adam,
If I remember right, subclassing in Django hasn't been implemented for some time – in the past it was implemented in a shoddy fashion, and they are working now to create a better interface for it.
My suggestion would be to take the Flatpages app and just copy it and modify it to fit your needs (instead of subclassing). Looks like it probably wouldn't be too hard given the stuff you're doing.
August 25th, 2006 at 2:02 pm
Aha, found the reference:
"As Iām writing this, the option to subclass is only available in older releases of Django ā model subclassing is being refactored in trunk right now ā and even then, getting Django to use your subclassed User model instead of the original (via replaces_module, which was ugly and undocumented and was thankfully removed a while back) can involve quite a bit of work."
Source: http://www.b-list.org/weblog/2006/06/06/django-tips-extending-user-model
August 28th, 2006 at 2:53 pm
Chris,
Thanks a ton! I decided to dig even more after my post here and saw this [http://code.djangoproject.com/wiki/ModelInheritance] post in the code section at the django project website. I started implementing my own Page app based on a mixing from the Flatpage app and what I've read here. So far I'm still having trouble with the same problem as before. I'm allowing users to choose a premade template from a list (models.CharField with choices=). Django isn't allowing this to happen. It still passes over it like the user never enter anything. I guess my biggest question is what is this returning? I thought it was returning the first (the non-human readable) element of the tuple in the CHOICES tuple…
August 28th, 2006 at 3:50 pm
Glad to help, Adam.
Are you still trying to do this with model inheritance? If you notice, they say in that page you reference that the text is how subclassing 'should' work – that is, it's not implemented in Django yet!
I'm pretty sure the reason it isn't working for you is because you're trying to get it to do something that hasn't been implemented quite yet. Try copying the flatpage app and modifying it!
I think you'll get it to work much faster than trying to wrestle with broken inheritance.
-Chris
August 29th, 2006 at 3:23 pm
Hey Chris,
We should just IM or email…we've got a lot going back and forth here. =) No, I'm not trying inheritence still. I folded that when I read the article on code.djangoproject. My code isn't happy (what I stole from the original FlatPages app) when I change template_name to a CharField using a choices= tuple. It's like it doesn't get the returned arg even when I set a default field. Any thoughts? Doesn't a choices= tuple return the first string arg in the susequent tuple?
Adam
November 18th, 2006 at 6:16 pm
Hi Chris,
Just wondering if you got far with the menu object system thing that you talked about in a previous post. I'm working on a CMS like application as well, and I could definitely use some help on that front.
Thanks!
Seemant
March 26th, 2009 at 3:58 am
Is this still an effective approach? I'm interested for a new site I'm working on.