Merge branch 'master' of git://github.com/melinath/philo
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Thu, 6 Jan 2011 19:15:09 +0000 (14:15 -0500)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Thu, 6 Jan 2011 19:15:09 +0000 (14:15 -0500)
* 'master' of git://github.com/melinath/philo:
  Added markdown-formatted README, and added a note to the readme re: the IRC channel. Removed the replace_sender_response function as it was a terrible abomination that was bound to encourage hackish solutions.
  Improved NodeAdmin list_display options. Added use of default validators to JSONField and JSONFormField.
  Simplified waldo's AccountMultiView to rely just on an AccountForm rather than a complicated mix of a UserForm and an AccountForm generated based on a number of attributes.
  Minor correction to TreeModel.get_path to allow for subclasses that use an alternate level attribute. Minor correction to TreeManager.get_with_path to only return root as the deepest node if there is actually no deepest_found node. Added MultiView.reverse() shortcut method to handle the common pattern of getting the absolute url for the subpath of a multiview. Adjusted waldo MultiViews to use their reverse() method. Added make_confirmation_link() method to LoginMultiView to provide a standard, reusable way to handle this task. Brought waldo's multiview get_context method use in line with penfield by removing the optional extra dictionary passed in.
  Minor correction to embed template tag: updating the context adds an additional context layer, which means the explicitly pushed layer was never popped.
  Minor changes to penfield to return feed urls only when feeds are actually enabled and to allow BlogEntry.objects.latest(). Corrected TreeManager.get_with_path to return the root node (if defined) as the deepest node if nothing else is found along the path. Removed embed tag error raising for an expected behavior.
  Minor corrections to node_url and embed templatetags to avoid raising unnecessary errors.
  Corrected embed handling to ignore embedded instances which do not appear in the context.
  Further polished embedding system - allowed for context-dependent embed nodes to be correctly added to the embed context and to correctly render in extending templates even if not within blocks. Added notes to README about how to use philo. Fixed docstring typo for {% node_url %} templatetag.
  Improved the initial mptt migration by adding automated tree rebuilds.

15 files changed:
README
README.markdown [new file with mode: 0644]
admin/nodes.py
contrib/penfield/models.py
contrib/penfield/utils.py
contrib/waldo/forms.py
contrib/waldo/models.py
migrations/0009_auto__add_field_node_lft__add_field_node_rght__add_field_node_tree_id_.py
models/base.py
models/fields.py
models/nodes.py
signals.py
templatetags/embed.py
templatetags/nodes.py
tests.py

diff --git a/README b/README
index e718287..5ce7b93 100644 (file)
--- a/README
+++ b/README
@@ -3,7 +3,21 @@ Philo is a foundation for developing web content management systems.
 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
diff --git a/README.markdown b/README.markdown
new file mode 100644 (file)
index 0000000..0e695c5
--- /dev/null
@@ -0,0 +1,24 @@
+Philo is a foundation for developing web content management systems.
+
+Prerequisites:
+
+ * [Python 2.5.4+ &lt;http://www.python.org&gt;](http://www.python.org/)
+ * [Django 1.2+ &lt;http://www.djangoproject.com/&gt;](http://www.djangoproject.com/)
+ * [django-mptt 0.4+ &lt;https://github.com/django-mptt/django-mptt/&gt;](https://github.com/django-mptt/django-mptt/)
+ * (Optional) [django-grappelli 2.0+ &lt;http://code.google.com/p/django-grappelli/&gt;](http://code.google.com/p/django-grappelli/)
+ * (Optional) [south 0.7.2+ &lt;http://south.aeracode.org/)](http://south.aeracode.org/)
+ * (Optional) [recaptcha-django r6 &lt;http://code.google.com/p/recaptcha-django/&gt;](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
index 45a3172..a576d44 100644 (file)
@@ -4,7 +4,11 @@ from philo.models import Node, Redirect, 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):
index 8248340..d3eea16 100644 (file)
@@ -37,6 +37,7 @@ class BlogEntry(Entity, Titled):
        class Meta:
                ordering = ['-date']
                verbose_name_plural = "blog entries"
+               get_latest_by = "date"
 
 
 register_value_model(BlogEntry)
@@ -104,7 +105,12 @@ class BlogView(MultiView, FeedMultiViewMixin):
        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:
@@ -436,4 +442,4 @@ class NewsletterView(MultiView, FeedMultiViewMixin):
                        '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)
index 0386d38..43c7c91 100644 (file)
@@ -87,11 +87,14 @@ class FeedMultiViewMixin(object):
                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):
index 18c22a4..615d302 100644 (file)
@@ -56,4 +56,18 @@ class RegistrationForm(UserCreationForm):
                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
index f1ee05a..e3dd079 100644 (file)
@@ -7,7 +7,6 @@ from django.contrib.auth.models import User
 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
@@ -17,7 +16,7 @@ from django.utils.translation import ugettext_lazy, ugettext as _
 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
 
@@ -25,13 +24,6 @@ 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
@@ -66,10 +58,19 @@ class LoginMultiView(MultiView):
                
                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()
@@ -97,11 +98,12 @@ class LoginMultiView(MultiView):
                        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):
@@ -111,7 +113,8 @@ class LoginMultiView(MultiView):
                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
                
@@ -169,8 +172,7 @@ class LoginMultiView(MultiView):
        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
@@ -195,10 +197,8 @@ class LoginMultiView(MultiView):
                        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)
@@ -207,8 +207,11 @@ class LoginMultiView(MultiView):
                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):
@@ -231,11 +234,15 @@ class LoginMultiView(MultiView):
                                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
@@ -250,8 +257,11 @@ class LoginMultiView(MultiView):
                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):
@@ -262,20 +272,21 @@ class LoginMultiView(MultiView):
                        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):
@@ -294,10 +305,11 @@ class LoginMultiView(MultiView):
                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.
@@ -316,16 +328,13 @@ class LoginMultiView(MultiView):
 
 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):
@@ -336,71 +345,37 @@ class AccountMultiView(LoginMultiView):
                )
                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):
@@ -430,7 +405,7 @@ class AccountMultiView(LoginMultiView):
        
        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):
                """
@@ -455,7 +430,7 @@ class AccountMultiView(LoginMultiView):
                        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
        
index a6f58fd..d77affa 100644 (file)
@@ -3,6 +3,8 @@ import datetime
 from south.db import db
 from south.v2 import SchemaMigration
 from django.db import models
+from philo.models import Node, Template
+
 
 class Migration(SchemaMigration):
 
@@ -32,6 +34,10 @@ 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):
         
index 202c2f3..c7b1c26 100644 (file)
@@ -358,6 +358,8 @@ class TreeManager(models.Manager):
                                
                                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)
@@ -411,7 +413,7 @@ class TreeModel(MPTTModel):
                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)
index 3e43c0f..19a6006 100644 (file)
@@ -200,7 +200,11 @@ class TemplateField(models.TextField):
 
 
 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:
@@ -231,9 +235,7 @@ class JSONDescriptor(object):
 
 
 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
index 0ece55f..de10ed1 100644 (file)
@@ -119,6 +119,14 @@ class MultiView(View):
                        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
 
index 875039d..3653c54 100644 (file)
@@ -5,11 +5,4 @@ entity_class_prepared = Signal(providing_args=['class'])
 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
index ef2eeb2..db5cea5 100644 (file)
@@ -61,23 +61,28 @@ class EmbedContext(object):
 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()
@@ -90,7 +95,7 @@ def render_extends_node(self, context):
        # 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.
@@ -100,10 +105,16 @@ def render_extends_node(self, 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)
@@ -152,7 +163,7 @@ class ConstantEmbedNode(template.Node):
                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
@@ -199,16 +210,13 @@ class ConstantEmbedNode(template.Node):
                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)
@@ -225,13 +233,11 @@ class EmbedNode(ConstantEmbedNode):
                        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:
@@ -256,7 +262,10 @@ class InstanceEmbedNode(EmbedNode):
                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):
index 338ac2d..73492d4 100644 (file)
@@ -51,7 +51,9 @@ class NodeURLNode(template.Node):
                                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:]
@@ -68,7 +70,7 @@ class NodeURLNode(template.Node):
 @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>]%}
index b0f40d5..96ac7b6 100644 (file)
--- a/tests.py
+++ b/tests.py
@@ -39,9 +39,10 @@ class TemplateTestCase(TestCase):
                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)
@@ -96,6 +97,13 @@ class TemplateTestCase(TestCase):
                        # 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),
                }