Prerequisites:
* Python 2.5.4+ <http://www.python.org/>
* Django 1.2+ <http://www.djangoproject.com/>
+ * django-mptt 0.4+ <https://github.com/django-mptt/django-mptt/>
* (Optional) django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>
* (Optional) recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>
+ * (Optional) south 0.7.2+ <http://south.aeracode.org/>
-To contribute, please visit the project website <http://philo.ithinksw.org/>.
+To contribute, please visit the project website <http://philo.ithinksw.org/>. Feel free to join us on IRC at irc://irc.oftc.net/#philo.
+
+====
+Using philo
+====
+After installing philo and mptt on your python path, make sure to complete the following steps:
+
+1. add 'philo.middleware.RequestNodeMiddleware' to settings.MIDDLEWARE_CLASSES.
+2. add 'philo' and 'mptt' to settings.INSTALLED_APPS.
+3. include 'philo.urls' somewhere in your urls.py file.
+4. Optionally add a root node to your current Site.
+
+Philo should be ready to go!
\ No newline at end of file
--- /dev/null
+Philo is a foundation for developing web content management systems.
+
+Prerequisites:
+
+ * [Python 2.5.4+ <http://www.python.org>](http://www.python.org/)
+ * [Django 1.2+ <http://www.djangoproject.com/>](http://www.djangoproject.com/)
+ * [django-mptt 0.4+ <https://github.com/django-mptt/django-mptt/>](https://github.com/django-mptt/django-mptt/)
+ * (Optional) [django-grappelli 2.0+ <http://code.google.com/p/django-grappelli/>](http://code.google.com/p/django-grappelli/)
+ * (Optional) [south 0.7.2+ <http://south.aeracode.org/)](http://south.aeracode.org/)
+ * (Optional) [recaptcha-django r6 <http://code.google.com/p/recaptcha-django/>](http://code.google.com/p/recaptcha-django/)
+
+To contribute, please visit the [project website](http://philo.ithinksw.org/). Feel free to join us on IRC at [irc://irc.oftc.net/#philo](irc://irc.oftc.net/#philo>).
+
+Using philo
+===========
+
+After installing philo and mptt on your python path, make sure to complete the following steps:
+
+1. add 'philo.middleware.RequestNodeMiddleware' to settings.MIDDLEWARE_CLASSES.
+2. add 'philo' and 'mptt' to settings.INSTALLED_APPS.
+3. include 'philo.urls' somewhere in your urls.py file.
+4. Optionally add a root node to your current Site.
+
+Philo should be ready to go!
\ No newline at end of file
class NodeAdmin(TreeEntityAdmin):
- pass
+ list_display = ('slug', 'view', 'accepts_subpath')
+
+ def accepts_subpath(self, obj):
+ return obj.accepts_subpath
+ accepts_subpath.boolean = True
class ViewAdmin(EntityAdmin):
class Meta:
ordering = ['-date']
verbose_name_plural = "blog entries"
+ get_latest_by = "date"
register_value_model(BlogEntry)
def urlpatterns(self):
urlpatterns = patterns('',
url(r'^', include(self.feed_patterns(self.get_all_entries, self.index_page, 'index'))),
- url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/%s/' % (self.tag_permalink_base, self.feed_suffix), self.feed_view(self.get_entries_by_tag, 'entries_by_tag_feed'), name='entries_by_tag_feed'),
+ )
+ if self.feeds_enabled:
+ urlpatterns += patterns('',
+ url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/%s/' % (self.tag_permalink_base, self.feed_suffix), self.feed_view(self.get_entries_by_tag, 'entries_by_tag_feed'), name='entries_by_tag_feed'),
+ )
+ urlpatterns += patterns('',
url(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)/' % self.tag_permalink_base, self.page_view(self.get_entries_by_tag, self.tag_page), name='entries_by_tag')
)
if self.tag_archive_page:
'title': title
}
defaults.update(kwargs or {})
- return super(NewsletterView, self).get_feed(feed_type, extra_context, defaults)
\ No newline at end of file
+ return super(NewsletterView, self).get_feed(feed_type, extra_context, defaults)
return self.atom_feed(**defaults)
def feed_patterns(self, object_fetcher, page, base_name):
- feed_name = '%s_feed' % base_name
urlpatterns = patterns('',
- url(r'^%s/$' % self.feed_suffix, self.feed_view(object_fetcher, feed_name), name=feed_name),
url(r'^$', self.page_view(object_fetcher, page), name=base_name)
)
+ if self.feeds_enabled:
+ feed_name = '%s_feed' % base_name
+ urlpatterns = patterns('',
+ url(r'^%s/$' % self.feed_suffix, self.feed_view(object_fetcher, feed_name), name=feed_name),
+ ) + urlpatterns
return urlpatterns
def add_item(self, feed, obj, kwargs=None):
new_user = User.objects.create_user(username, email, password)
new_user.is_active = False
new_user.save()
- return new_user
\ No newline at end of file
+ return new_user
+
+
+class UserAccountForm(forms.ModelForm):
+ first_name = User._meta.get_field('first_name').formfield(required=True)
+ last_name = User._meta.get_field('last_name').formfield(required=True)
+ email = User._meta.get_field('email').formfield(required=True, widget=EmailInput)
+
+ def __init__(self, user, *args, **kwargs):
+ kwargs['instance'] = user
+ super(UserAccountForm, self).__init__(*args, **kwargs)
+
+ class Meta:
+ model = User
+ fields = ('first_name', 'last_name', 'email')
\ No newline at end of file
from django.contrib.auth.tokens import default_token_generator as password_token_generator
from django.contrib.sites.models import Site
from django.core.mail import EmailMultiAlternatives, send_mail
-from django.core.urlresolvers import reverse
from django.db import models
from django.http import Http404, HttpResponseRedirect
from django.shortcuts import render_to_response, get_object_or_404
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_protect
from philo.models import MultiView, Page
-from philo.contrib.waldo.forms import LOGIN_FORM_KEY, LoginForm, RegistrationForm
+from philo.contrib.waldo.forms import LOGIN_FORM_KEY, LoginForm, RegistrationForm, UserAccountForm
from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
import urlparse
ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
-def get_field_data(obj, fields):
- if fields == None:
- fields = [field.name for field in obj._meta.fields if field.editable]
-
- return dict([(field.name, field.value_from_object(obj)) for field in obj._meta.fields if field.name in fields])
-
-
class LoginMultiView(MultiView):
"""
Handles login, registration, and forgotten passwords. In other words, this
return urlpatterns
- def get_context(self, extra_dict=None):
- context = {}
- context.update(extra_dict or {})
- return context
+ def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
+ current_site = Site.objects.get_current()
+ token = token_generator.make_token(user, *(token_args or []))
+ kwargs = {
+ 'uidb36': int_to_base36(user.id),
+ 'token': token
+ }
+ kwargs.update(reverse_kwargs or {})
+ return 'http://%s%s' % (current_site.domain, self.reverse(confirmation_view, kwargs=kwargs, node=node))
+
+ def get_context(self):
+ """Hook for providing instance-specific context - such as the value of a Field - to all views."""
+ return {}
def display_login_page(self, request, message, extra_context=None):
request.session.set_test_cookie()
form = LoginForm(request.POST)
else:
form = LoginForm()
- context = self.get_context({
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update({
'message': message,
'form': form
})
- context.update(extra_context or {})
return self.login_page.render_to_response(request, extra_context=context)
def login(self, request, extra_context=None):
if request.user.is_authenticated():
return HttpResponseRedirect(request.node.get_absolute_url())
- context = self.get_context(extra_context)
+ context = self.get_context()
+ context.update(extra_context or {})
from django.contrib.auth.models import User
def login_required(self, view):
def inner(request, *args, **kwargs):
if not request.user.is_authenticated():
- login_url = reverse('login', urlconf=self).strip('/')
- return HttpResponseRedirect('%s%s/' % (request.node.get_absolute_url(), login_url))
+ return HttpResponseRedirect(self.reverse('login', node=request.node))
return view(request, *args, **kwargs)
return inner
if form.is_valid():
current_site = Site.objects.get_current()
for user in form.users_cache:
- token = token_generator.make_token(user)
- link = 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse('password_reset_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
context = {
- 'link': link,
+ 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node),
'username': user.username
}
self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
else:
form = PasswordResetForm()
- context = self.get_context({'form': form})
+ context = self.get_context()
context.update(extra_context or {})
+ context.update({
+ 'form': form
+ })
return self.password_reset_page.render_to_response(request, extra_context=context)
def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
if form.is_valid():
form.save()
messages.add_message(request, messages.SUCCESS, "Password reset successful.")
- return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('login', urlconf=self).strip('/')))
+ return HttpResponseRedirect(self.reverse('login', node=request.node))
else:
form = SetPasswordForm(user)
- context = self.get_context({'form': form})
+ context = self.get_context()
+ context.update(extra_context or {})
+ context.update({
+ 'form': form
+ })
return self.password_set_page.render_to_response(request, extra_context=context)
raise Http404
else:
form = PasswordChangeForm(request.user)
- context = self.get_context({'form': form})
+ context = self.get_context()
context.update(extra_context or {})
+ context.update({
+ 'form': form
+ })
return self.password_change_page.render_to_response(request, extra_context=context)
def register(self, request, extra_context=None, token_generator=registration_token_generator):
form = RegistrationForm(request.POST)
if form.is_valid():
user = form.save()
- current_site = Site.objects.get_current()
- token = token_generator.make_token(user)
- link = 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse('register_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
context = {
- 'link': link
+ 'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node)
}
+ current_site = Site.objects.get_current()
self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
return HttpResponseRedirect(request.node.get_absolute_url())
else:
form = RegistrationForm()
- context = self.get_context({'form': form})
+ context = self.get_context()
context.update(extra_context or {})
+ context.update({
+ 'form': form
+ })
return self.register_page.render_to_response(request, extra_context=context)
def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
if token_generator.check_token(user, token):
user.is_active = True
true_password = user.password
+ temp_password = token_generator.make_token(user)
try:
- user.set_password('temp_password')
+ user.set_password(temp_password)
user.save()
- authenticated_user = authenticate(username=user.username, password='temp_password')
+ authenticated_user = authenticate(username=user.username, password=temp_password)
login(request, authenticated_user)
finally:
# if anything goes wrong, ABSOLUTELY make sure that the true password is restored.
class AccountMultiView(LoginMultiView):
"""
- Subclasses may define an account_profile model, fields from the User model
- to include in the account, and fields from the account profile to use in
- the account.
+ By default, the `account` consists of the first_name, last_name, and email fields
+ of the User model. Using a different account model is as simple as writing a form that
+ accepts a User instance as the first argument.
"""
manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related')
email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related')
- user_fields = ['first_name', 'last_name', 'email']
- required_user_fields = user_fields
- account_profile = None
- account_profile_fields = None
+ account_form = UserAccountForm
@property
def urlpatterns(self):
)
return urlpatterns
- def get_account_forms(self):
- user_form = forms.models.modelform_factory(User, fields=self.user_fields)
-
- if self.account_profile is None:
- profile_form = None
- else:
- profile_form = forms.models.modelform_factory(self.account_profile, fields=self.account_profile_fields or [field.name for field in self.account_profile._meta.fields if field.editable and field.name != 'user'])
-
- for field_name, field in user_form.base_fields.items():
- if field_name in self.required_user_fields:
- field.required = True
- return user_form, profile_form
-
- def get_account_form_instances(self, user, data=None):
- form_instances = []
- user_form, profile_form = self.get_account_forms()
- if data is None:
- form_instances.append(user_form(instance=user))
- if profile_form:
- form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
- else:
- form_instances.append(user_form(data, instance=user))
- if profile_form:
- form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
-
- return form_instances
-
def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
if request.method == 'POST':
- form_instances = self.get_account_form_instances(request.user, request.POST)
- current_email = request.user.email
+ form = self.account_form(request.user, request.POST, request.FILES)
- for form in form_instances:
- if not form.is_valid():
- break
- else:
- # When the user_form is validated, it changes the model instance, i.e. request.user, in place.
- email = request.user.email
- if current_email != email:
-
- request.user.email = current_email
+ if form.is_valid():
+ if 'email' in form.changed_data:
+ # ModelForms modify their instances in-place during validation,
+ # so reset the instance's email to its previous value here,
+ # then remove the new value from cleaned_data.
+ request.user.email = form.initial['email']
- for form in form_instances:
- form.cleaned_data.pop('email', None)
+ email = form.cleaned_data.pop('email')
- current_site = Site.objects.get_current()
- token = token_generator.make_token(request.user, email)
- link = 'http://%s/%s/%s/' % (current_site.domain, request.node.get_absolute_url().strip('/'), reverse('email_change_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(request.user.id), 'email': email.replace('@', '+'), 'token': token}).strip('/'))
context = {
- 'link': link
+ 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')})
}
+ current_site = Site.objects.get_current()
self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
messages.add_message(request, messages.SUCCESS, "An email has be sent to %s to confirm the email change." % email)
-
- for form in form_instances:
- form.save()
+
+ form.save()
messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
return HttpResponseRedirect('')
else:
- form_instances = self.get_account_form_instances(request.user)
+ form = self.account_form(request.user)
- context = self.get_context({
- 'forms': form_instances
- })
+ context = self.get_context()
context.update(extra_context or {})
+ context.update({
+ 'form': form
+ })
return self.manage_account_page.render_to_response(request, extra_context=context)
def has_valid_account(self, user):
def post_register_confirm_redirect(self, request):
messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
- return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
+ return HttpResponseRedirect(self.reverse('account', node=request.node))
def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
"""
user.email = email
user.save()
messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
- return HttpResponseRedirect('/%s/%s/' % (request.node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
+ return HttpReponseRedirect(self.reverse('account', node=request.node))
raise Http404
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
+from philo.models import Node, Template
+
class Migration(SchemaMigration):
# Adding field 'Template.level'
db.add_column('philo_template', 'level', self.gf('django.db.models.fields.PositiveIntegerField')(default=1, db_index=True), keep_default=False)
+ # Rebuild trees!
+ Template._tree_manager.rebuild()
+ Node._tree_manager.rebuild()
+
def backwards(self, orm):
if deepest_level == depth:
# This should happen if nothing is found with any part of the given path.
+ if root is not None and deepest_found is None:
+ return root, build_path(segments)
raise
return find_obj(segments, depth, deepest_found)
qs = self.get_ancestors()
if root is not None:
- qs = qs.filter(level__gt=root.level)
+ qs = qs.filter(**{'%s__gt' % self._mptt_meta.level_attr: root.get_level()})
return pathsep.join([getattr(parent, field, '?') for parent in list(qs) + [self]])
path = property(get_path)
class JSONFormField(forms.Field):
+ default_validators = [json_validator]
+
def clean(self, value):
+ if value == '' and not self.required:
+ return None
try:
return json.loads(value)
except Exception, e:
class JSONField(models.TextField):
- def __init__(self, *args, **kwargs):
- super(JSONField, self).__init__(*args, **kwargs)
- self.validators.append(json_validator)
+ default_validators = [json_validator]
def get_attname(self):
return "%s_json" % self.name
kwargs['extra_context'] = extra_context
return view(request, *args, **kwargs)
+ def reverse(self, view_name, args=None, kwargs=None, node=None):
+ """Shortcut method to handle the common pattern of getting the absolute url for a multiview's
+ subpaths."""
+ subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
+ if node is not None:
+ return '/%s/%s/' % (node.get_absolute_url().strip('/'), subpath.strip('/'))
+ return subpath
+
class Meta:
abstract = True
view_about_to_render = Signal(providing_args=['request', 'extra_context'])
view_finished_rendering = Signal(providing_args=['response'])
page_about_to_render_to_string = Signal(providing_args=['request', 'extra_context'])
-page_finished_rendering_to_string = Signal(providing_args=['string'])
-
-
-def replace_sender_response(sender, response):
- """Helper function to swap in a new response."""
- def render_to_response(self, *args, **kwargs):
- return response
- sender.actually_render_to_response = render_to_response
\ No newline at end of file
+page_finished_rendering_to_string = Signal(providing_args=['string'])
\ No newline at end of file
old_extends_node_init = ExtendsNode.__init__
-def get_embed_dict(nodelist):
+def get_embed_dict(embed_list, context):
embeds = {}
- for n in nodelist.get_nodes_by_type(ConstantEmbedNode):
- if n.content_type not in embeds:
- embeds[n.content_type] = [n]
+ for e in embed_list:
+ ct = e.get_content_type(context)
+ if ct is None:
+ # Then the embed doesn't exist for this context.
+ continue
+ if ct not in embeds:
+ embeds[ct] = [e]
else:
- embeds[n.content_type].append(n)
+ embeds[ct].append(e)
return embeds
def extends_node_init(self, nodelist, *args, **kwargs):
- self.embeds = get_embed_dict(nodelist)
+ self.embed_list = nodelist.get_nodes_by_type(ConstantEmbedNode)
old_extends_node_init(self, nodelist, *args, **kwargs)
def render_extends_node(self, context):
compiled_parent = self.get_parent(context)
+ embeds = get_embed_dict(self.embed_list, context)
if BLOCK_CONTEXT_KEY not in context.render_context:
context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
# Add the block nodes from this node to the block context
# Do the equivalent for embed nodes
block_context.add_blocks(self.blocks)
- embed_context.add_embeds(self.embeds)
+ embed_context.add_embeds(embeds)
# If this block's parent doesn't have an extends node it is the root,
# and its block nodes also need to be added to the block context.
if not isinstance(node, ExtendsNode):
blocks = dict([(n.name, n) for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode)])
block_context.add_blocks(blocks)
- embeds = get_embed_dict(compiled_parent.nodelist)
+ embeds = get_embed_dict(compiled_parent.nodelist.get_nodes_by_type(ConstantEmbedNode), context)
embed_context.add_embeds(embeds)
break
-
+
+ # Explicitly render all direct embed children of this node.
+ if self.embed_list:
+ for node in self.nodelist:
+ if isinstance(node, ConstantEmbedNode):
+ node.render(context)
+
# Call Template._render explicitly so the parser context stays
# the same.
return compiled_parent._render(context)
try:
return template.loader.get_template(template_name)
except template.TemplateDoesNotExist:
- if not hasattr(self, 'template_name') and settings.TEMPLATE_DEBUG:
+ if hasattr(self, 'template') and settings.TEMPLATE_DEBUG:
# Then it's a constant node.
raise
return False
try:
t = context.render_context[EMBED_CONTEXT_KEY].get_embed_template(self, context)
except (KeyError, IndexError):
- if settings.TEMPLATE_DEBUG:
- raise
+ self.mark_rendered_for(context)
return settings.TEMPLATE_STRING_IF_INVALID
context.push()
context['embedded'] = instance
- kwargs = {}
for k, v in self.kwargs.items():
- kwargs[k] = v.resolve(context)
- context.update(kwargs)
+ context[k] = v.resolve(context)
t_rendered = t.render(context)
context.pop()
self.mark_rendered_for(context)
self.object_pk = object_pk
else:
self.object_pk = None
- self.instance = None
if template_name is not None:
self.template_name = template_name
else:
self.template_name = None
- self.template = None
def get_instance(self, context):
if self.object_pk is None:
return self.instance.resolve(context)
def get_content_type(self, context):
- return ContentType.objects.get_for_model(self.get_instance(context))
+ instance = self.get_instance(context)
+ if not instance:
+ return None
+ return ContentType.objects.get_for_model(instance)
def get_embedded(self):
subpath = reverse(view_name, urlconf=node.view, args=args, kwargs=kwargs)
except NoReverseMatch:
if self.as_var is None:
- raise
+ if settings.TEMPLATE_DEBUG:
+ raise
+ return settings.TEMPLATE_STRING_IF_INVALID
else:
if subpath[0] == '/':
subpath = subpath[1:]
@register.tag(name='node_url')
def do_node_url(parser, token):
"""
- {% node_url [for <node>] [as <var] %}
+ {% node_url [for <node>] [as <var>] %}
{% node_url with <obj> [for <node>] [as <var>] %}
{% node_url <view_name> [<arg1> [<arg2> ...] ] [for <node>] [as <var>] %}
{% node_url <view_name> [<key1>=<value1> [<key2>=<value2> ...] ] [for <node>] [as <var>]%}
expected_invalid_str = 'INVALID'
failures = []
-
+ tests = template_tests.items()
+ tests.sort()
# Run tests
- for name, vals in template_tests.items():
+ for name, vals in tests:
xx, context, result = vals
try:
test_template = loader.get_template(name)
# Blocks and includes
'block-include01': ('{% extends "simple01" %}{% embed penfield.blog with "embed03" %}{% block one %}{% include "simple01" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%sSimple%s is a lie!" % (blog.title, blog.title, blog.title)),
'block-include02': ('{% extends "simple01" %}{% block one %}{% include "simple04" %}{% embed penfield.blog with "embed03" %}{% include "simple04" %}{% embed penfield.blog 1 %}{% endblock %}', {}, "%sSimple%s%s is a lie!%s is a lie!" % (blog.title, blog.title, blog.title, blog.title)),
+
+ # Tests for more complex situations...
+ 'complex01': ('{% block one %}{% endblock %}complex{% block two %}{% endblock %}', {}, 'complex'),
+ 'complex02': ('{% extends "complex01" %}', {}, 'complex'),
+ 'complex03': ('{% extends "complex02" %}{% embed penfield.blog with "embed01" %}', {}, 'complex'),
+ 'complex04': ('{% extends "complex03" %}{% block one %}{% embed penfield.blog 1 %}{% endblock %}', {}, '%scomplex' % blog.title),
+ 'complex05': ('{% extends "complex03" %}{% block one %}{% include "simple04" %}{% endblock %}', {}, '%scomplex' % blog.title),
}