Merge branch 'master' of git://github.com/melinath/philo
authorJoseph Spiros <joseph.spiros@ithinksw.com>
Fri, 18 Feb 2011 15:40:14 +0000 (10:40 -0500)
committerJoseph Spiros <joseph.spiros@ithinksw.com>
Fri, 18 Feb 2011 15:40:14 +0000 (10:40 -0500)
* 'master' of git://github.com/melinath/philo:
  Switched feed_patterns to return urlpatterns suitable for addition to urlpatterns instead of inclusion in urlpatterns. Adjusted penfield urlpatterns accordingly. Added default behavior for FeedView.get_feed in cases where a known feed_type is not supplied for whatever reason. Added RegistrationMultiView.registration_form attribute.
  Switched user authentication in waldo from the login view to an AuthenticationForm for clearer organization of code. Split LoginMultiView into three layered views: LoginMultiView, PasswordMultiView, and RegistrationMultiView, where each layer adds additional (optional) functionality. This structure then naturally includes AccountMultiView as well. Improved redirection for login_required and account_required views.
  Corrected penfield BlogView handling of tags, particularly related to feed links. Added an optional slash at the beginning of the feed urlpattern in FeedView.feed_patterns to handle cases where this is included from something with no trailing slash.
  Corrected Waldo AccountMultiView typo and improved View error reporting / TargetURL error catching.
  Minor tweak to has_navigation filter to silence all errors and return False.
  Corrected penfield migration which introduces the feed_type column. Switched NavigationManager.update_targets_for() method to search for node pks instead of doing a JOIN - should be more efficient. Added check for node id before updating the target_node; this was causing an unwanted KeyError in cases where the NavigationItem had no target node.

contrib/penfield/migrations/0003_auto__add_field_newsletterview_feed_type__add_field_newsletterview_ite.py
contrib/penfield/models.py
contrib/shipherd/models.py
contrib/shipherd/templatetags/shipherd.py
contrib/waldo/forms.py
contrib/waldo/models.py
models/nodes.py

index 1f6d829..eae496e 100644 (file)
@@ -9,7 +9,7 @@ class Migration(SchemaMigration):
     def forwards(self, orm):
         
         # Adding field 'NewsletterView.feed_type'
-        db.add_column('penfield_newsletterview', 'feed_type', self.gf('django.db.models.fields.CharField')(default='atom', max_length=50), keep_default=False)
+        db.add_column('penfield_newsletterview', 'feed_type', self.gf('django.db.models.fields.CharField')(default='application/atom+xml', max_length=50), keep_default=False)
 
         # Adding field 'NewsletterView.item_title_template'
         db.add_column('penfield_newsletterview', 'item_title_template', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='penfield_newsletterview_title_related', null=True, to=orm['philo.Template']), keep_default=False)
index bb71ba2..065d033 100644 (file)
@@ -19,6 +19,7 @@ try:
 except:
        mimeparse = None
 
+
 ATOM = feedgenerator.Atom1Feed.mime_type
 RSS = feedgenerator.Rss201rev2Feed.mime_type
 FEEDS = SortedDict([
@@ -52,20 +53,23 @@ class FeedView(MultiView):
        
        description = ""
        
-       def feed_patterns(self, get_items_attr, page_attr, reverse_name):
+       def feed_patterns(self, base, get_items_attr, page_attr, reverse_name):
                """
                Given the name to be used to reverse this view and the names of
                the attributes for the function that fetches the objects, returns
                patterns suitable for inclusion in urlpatterns.
                """
-               urlpatterns = patterns('',
-                       url(r'^$', self.page_view(get_items_attr, page_attr), name=reverse_name)
-               )
+               urlpatterns = patterns('')
                if self.feeds_enabled:
                        feed_reverse_name = "%s_feed" % reverse_name
+                       feed_view = self.feed_view(get_items_attr, feed_reverse_name)
+                       feed_pattern = r'%s%s%s$' % (base, (base and base[-1] != "^") and "/" or "", self.feed_suffix)
                        urlpatterns += patterns('',
-                               url(r'^%s$' % self.feed_suffix, self.feed_view(get_items_attr, feed_reverse_name), name=feed_reverse_name),
+                               url(feed_pattern, feed_view, name=feed_reverse_name),
                        )
+               urlpatterns += patterns('',
+                       url(r"%s$" % base, self.page_view(get_items_attr, page_attr), name=reverse_name)
+               )
                return urlpatterns
        
        def get_object(self, request, **kwargs):
@@ -121,6 +125,8 @@ class FeedView(MultiView):
        
        def get_feed_type(self, request):
                feed_type = self.feed_type
+               if feed_type not in FEEDS:
+                       feed_type = FEEDS.keys()[0]
                accept = request.META.get('HTTP_ACCEPT')
                if accept and feed_type not in accept and "*/*" not in accept and "%s/*" % feed_type.split("/")[0] not in accept:
                        # Wups! They aren't accepting the chosen format. Is there another format we can use?
@@ -333,6 +339,7 @@ class BlogView(FeedView):
        
        index_page = models.ForeignKey(Page, related_name='blog_index_related')
        entry_page = models.ForeignKey(Page, related_name='blog_entry_related')
+       # TODO: entry_archive is misleading. Rename to ymd_page or timespan_page.
        entry_archive_page = models.ForeignKey(Page, related_name='blog_entry_archive_related', null=True, blank=True)
        tag_page = models.ForeignKey(Page, related_name='blog_tag_related')
        tag_archive_page = models.ForeignKey(Page, related_name='blog_tag_archive_related', null=True, blank=True)
@@ -359,7 +366,7 @@ class BlogView(FeedView):
                                                if self.entry_permalink_style == 'D':
                                                        kwargs.update({'day': str(obj.date.day).zfill(2)})
                                return self.entry_view, [], kwargs
-               elif isinstance(obj, Tag) or (isinstance(obj, models.QuerySet) and obj.model == Tag and obj):
+               elif isinstance(obj, Tag) or (isinstance(obj, models.query.QuerySet) and obj.model == Tag and obj):
                        if isinstance(obj, Tag):
                                obj = [obj]
                        slugs = [tag.slug for tag in obj if tag in self.get_tag_queryset()]
@@ -376,16 +383,11 @@ class BlogView(FeedView):
        
        @property
        def urlpatterns(self):
-               urlpatterns = patterns('',
-                       url(r'^', include(self.feed_patterns('get_all_entries', 'index_page', 'index'))),
-               )
+               urlpatterns = self.feed_patterns(r'^', 'get_all_entries', 'index_page', 'index')
+               
                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('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('get_entries_by_tag', 'tag_page'), name='entries_by_tag')
-               )
+                       urlpatterns += self.feed_patterns(r'^%s/(?P<tag_slugs>[-\w]+[-+/\w]*)$' % self.tag_permalink_base, 'get_entries_by_tag', 'tag_page', 'get_entries_by_tag')
+               
                if self.tag_archive_page:
                        urlpatterns += patterns('',
                                url((r'^%s$' % self.tag_permalink_base), self.tag_archive_view, name='tag_archive')
@@ -393,17 +395,11 @@ class BlogView(FeedView):
                
                if self.entry_archive_page:
                        if self.entry_permalink_style in 'DMY':
-                               urlpatterns += patterns('',
-                                       url(r'^(?P<year>\d{4})', include(self.feed_patterns('get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')))
-                               )
+                               urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_year')
                                if self.entry_permalink_style in 'DM':
-                                       urlpatterns += patterns('',
-                                               url(r'^(?P<year>\d{4})/(?P<month>\d{2})$', include(self.feed_patterns('get_entries_by_ymd', 'entry_archive_page', 'entries_by_month'))),
-                                       )
+                                       urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_month')
                                        if self.entry_permalink_style == 'D':
-                                               urlpatterns += patterns('',
-                                                       url(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})$', include(self.feed_patterns('get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')))
-                                               )
+                                               urlpatterns += self.feed_patterns(r'^(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})', 'get_entries_by_ymd', 'entry_archive_page', 'entries_by_day')
                
                if self.entry_permalink_style == 'D':
                        urlpatterns += patterns('',
@@ -514,7 +510,7 @@ class BlogView(FeedView):
                        
                        if 'tags' in extra_context:
                                tags = extra_context['tags']
-                               feed.feed['link'] = request.node.construct_url(self.reverse(tags), with_domain=True, request=request, secure=request.is_secure())
+                               feed.feed['link'] = request.node.construct_url(self.reverse(obj=tags), with_domain=True, request=request, secure=request.is_secure())
                        else:
                                tags = obj.entry_tags
                        
@@ -653,9 +649,8 @@ class NewsletterView(FeedView):
        
        @property
        def urlpatterns(self):
-               urlpatterns = patterns('',
-                       url(r'^', include(self.feed_patterns('get_all_articles', 'index_page', 'index'))),
-                       url(r'^%s/(?P<numbering>.+)' % self.issue_permalink_base, include(self.feed_patterns('get_articles_by_issue', 'issue_page', 'issue')))
+               urlpatterns = self.feed_patterns(r'^', 'get_all_articles', 'index_page', 'index') + patterns('',
+                       url(r'^%s/(?P<numbering>.+)$' % self.issue_permalink_base, self.page_view('get_articles_by_issue', 'issue_page'), name='issue')
                )
                if self.issue_archive_page:
                        urlpatterns += patterns('',
@@ -666,17 +661,11 @@ class NewsletterView(FeedView):
                                url(r'^%s' % self.article_permalink_base, include(self.feed_patterns('get_all_articles', 'article_archive_page', 'articles')))
                        )
                        if self.article_permalink_style in 'DMY':
-                               urlpatterns += patterns('',
-                                       url(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, include(self.feed_patterns('get_articles_by_ymd', 'article_archive_page', 'articles_by_year')))
-                               )
+                               urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_year')
                                if self.article_permalink_style in 'DM':
-                                       urlpatterns += patterns('',
-                                               url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})' % self.article_permalink_base, include(self.feed_patterns('get_articles_by_ymd', 'article_archive_page', 'articles_by_month')))
-                                       )
+                                       urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_month')
                                        if self.article_permalink_style == 'D':
-                                               urlpatterns += patterns('',
-                                                       url(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})' % self.article_permalink_base, include(self.feed_patterns('get_articles_by_ymd', 'article_archive_page', 'articles_by_day')))
-                                               )
+                                               urlpatterns += self.feed_patterns(r'^%s/(?P<year>\d{4})/(?P<month>\d{2})/(?P<day>\d{2})' % self.article_permalink_base, 'get_articles_by_ymd', 'article_archive_page', 'articles_by_day')
                
                if self.article_permalink_style == 'Y':
                        urlpatterns += patterns('',
index 8efc57a..654f5f8 100644 (file)
@@ -148,18 +148,19 @@ class NavigationManager(models.Manager):
                # about that. TODO: Benchmark it.
                caches = self.__class__._cache[self.db][node].values()
                
-               items = []
+               target_pks = set()
                
                for cache in caches:
-                       items += cache['items']
+                       target_pks |= set([item.target_node_id for item in cache['items']])
                
                # A distinct query is not strictly necessary. TODO: benchmark the efficiency
                # with/without distinct.
-               targets = list(Node.objects.filter(shipherd_navigationitem_related__in=items).distinct())
+               targets = list(Node.objects.filter(pk__in=target_pks).distinct())
                
                for cache in caches:
                        for item in cache['items']:
-                               item.target_node = targets[targets.index(item.target_node)]
+                               if item.target_node_id:
+                                       item.target_node = targets[targets.index(item.target_node)]
        
        def clear_cache(self):
                self.__class__._cache.pop(self.db, None)
index 98e3e6b..fa4ec3e 100644 (file)
@@ -84,13 +84,16 @@ def recursenavigation(parser, token):
 
 @register.filter
 def has_navigation(node, key=None):
-       nav = node.navigation
-       if key is not None:
-               if key in nav and bool(node.navigation[key]):
-                       return True
-               elif key not in node.navigation:
-                       return False
-       return bool(node.navigation)
+       try:
+               nav = node.navigation
+               if key is not None:
+                       if key in nav and bool(node.navigation[key]):
+                               return True
+                       elif key not in node.navigation:
+                               return False
+               return bool(node.navigation)
+       except:
+               return False
 
 
 @register.filter
index 615d302..6f2b59b 100644 (file)
@@ -1,6 +1,7 @@
 from datetime import date
 from django import forms
 from django.conf import settings
+from django.contrib.auth import authenticate
 from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
 from django.contrib.auth.models import User
 from django.core.exceptions import ValidationError
@@ -8,12 +9,6 @@ from django.utils.translation import ugettext_lazy as _
 from philo.contrib.waldo.tokens import REGISTRATION_TIMEOUT_DAYS
 
 
-LOGIN_FORM_KEY = 'this_is_the_login_form'
-LoginForm = type('LoginForm', (AuthenticationForm,), {
-       LOGIN_FORM_KEY: forms.BooleanField(widget=forms.HiddenInput, initial=True)
-})
-
-
 class EmailInput(forms.TextInput):
        input_type = 'email'
 
@@ -70,4 +65,38 @@ class UserAccountForm(forms.ModelForm):
        
        class Meta:
                model = User
-               fields = ('first_name', 'last_name', 'email')
\ No newline at end of file
+               fields = ('first_name', 'last_name', 'email')
+
+
+class WaldoAuthenticationForm(AuthenticationForm):
+       ERROR_MESSAGE = _("Please enter a correct username and password. Note that both fields are case-sensitive.")
+       
+       def clean(self):
+               username = self.cleaned_data['username']
+               password = self.cleaned_data['password']
+               message = self.ERROR_MESSAGE
+               
+               if username and password:
+                       self.user_cache = authenticate(username=username, password=password)
+                       if self.user_cache is None:
+                               if u'@' in username:
+                                       # Maybe they entered their email? Look it up, but still raise a ValidationError.
+                                       try:
+                                               user = User.objects.get(email=username)
+                                       except (User.DoesNotExist, User.MultipleObjectsReturned):
+                                               pass
+                                       else:
+                                               if user.check_password(password):
+                                                       message = _("Your e-mail address is not your username. Try '%s' instead.") % user.username
+                               raise ValidationError(message)
+                       elif not self.user_cache.is_active:
+                               raise ValidationError(message)
+               self.check_for_test_cookie()
+               return self.cleaned_data
+       
+       def check_for_test_cookie(self):
+               # This method duplicates the Django 1.3 AuthenticationForm method.
+               if self.request and not self.request.session.test_cookie_worked():
+                       raise forms.ValidationError(
+                               _("Your Web browser doesn't appear to have cookies enabled. "
+                                 "Cookies are required for logging in."))
\ No newline at end of file
index 3286aa0..4ff609b 100644 (file)
@@ -12,166 +12,152 @@ from django.http import Http404, HttpResponseRedirect
 from django.shortcuts import render_to_response, get_object_or_404
 from django.template.defaultfilters import striptags
 from django.utils.http import int_to_base36, base36_to_int
-from django.utils.translation import ugettext_lazy, ugettext as _
+from django.utils.translation import 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, UserAccountForm
+from philo.contrib.waldo.forms import WaldoAuthenticationForm, 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.")
-
-
 class LoginMultiView(MultiView):
        """
-       Handles login, registration, and forgotten passwords. In other words, this
-       multiview provides exclusively view and methods related to usernames and
-       passwords.
+       Handles exclusively methods and views related to logging users in and out.
        """
        login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
-       password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
-       password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related')
-       password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related')
-       password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
-       register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
-       register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
+       login_form = WaldoAuthenticationForm
        
        @property
        def urlpatterns(self):
-               urlpatterns = patterns('',
+               return patterns('',
                        url(r'^login$', self.login, name='login'),
                        url(r'^logout$', self.logout, name='logout'),
-                       
-                       url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
-                       url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
-                       
-                       url(r'^register$', csrf_protect(self.register), name='register'),
-                       url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
                )
-               
-               if self.password_change_page:
-                       urlpatterns += patterns('',
-                               url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
-                       )
-               
-               return urlpatterns
        
-       def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
-               token = token_generator.make_token(user, *(token_args or []))
-               kwargs = {
-                       'uidb36': int_to_base36(user.id),
-                       'token': token
-               }
-               kwargs.update(reverse_kwargs or {})
-               return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True)
-       
-       def display_login_page(self, request, message, extra_context=None):
-               request.session.set_test_cookie()
-               
-               referrer = request.META.get('HTTP_REFERER', None)
-               
-               if referrer is not None:
-                       referrer = urlparse.urlparse(referrer)
-                       host = referrer[1]
-                       if host != request.get_host():
-                               referrer = None
-                       else:
-                               redirect = '%s?%s' % (referrer[2], referrer[4])
+       def set_requirement_redirect(self, request, redirect=None):
+               "Figure out where someone should end up after landing on a `requirement` page like the login page."
+               if redirect is not None:
+                       pass
+               elif 'requirement_redirect' in request.session:
+                       return
+               else:
+                       referrer = request.META.get('HTTP_REFERER', None)
                
-               if referrer is None:
-                       redirect = request.node.get_absolute_url()
+                       if referrer is not None:
+                               referrer = urlparse.urlparse(referrer)
+                               host = referrer[1]
+                               if host != request.get_host():
+                                       referrer = None
+                               else:
+                                       redirect = '%s?%s' % (referrer[2], referrer[4])
                
-               path = request.get_full_path()
-               if redirect != path:
-                       if redirect is None:
-                               redirect = '/'.join(path.split('/')[:-2])
-                       request.session['redirect'] = redirect
+                       path = request.get_full_path()
+                       if referrer is None or redirect == path:
+                               # Default to the index page if we can't find a referrer or
+                               # if we'd otherwise redirect to where we already are.
+                               redirect = request.node.get_absolute_url()
                
-               if request.POST:
-                       form = LoginForm(request.POST)
+               request.session['requirement_redirect'] = redirect
+       
+       def get_requirement_redirect(self, request, default=None):
+               redirect = request.session.pop('requirement_redirect', None)
+               # Security checks a la django.contrib.auth.views.login
+               if not redirect or ' ' in redirect:
+                       redirect = default
                else:
-                       form = LoginForm()
-               context = self.get_context()
-               context.update(extra_context or {})
-               context.update({
-                       'message': message,
-                       'form': form
-               })
-               return self.login_page.render_to_response(request, extra_context=context)
+                       netloc = urlparse.urlparse(redirect)[1]
+                       if netloc and netloc != request.get_host():
+                               redirect = default
+               if redirect is None:
+                       redirect = request.node.get_absolute_url()
+               return redirect
        
+       @never_cache
        def login(self, request, extra_context=None):
                """
                Displays the login form for the given HttpRequest.
                """
-               if request.user.is_authenticated():
-                       return HttpResponseRedirect(request.node.get_absolute_url())
+               self.set_requirement_redirect(request)
                
-               context = self.get_context()
-               context.update(extra_context or {})
-               
-               from django.contrib.auth.models import User
+               # Redirect already-authenticated users to the index page.
+               if request.user.is_authenticated():
+                       messages.add_message(request, messages.INFO, "You are already authenticated. Please log out if you wish to log in as a different user.")
+                       return HttpResponseRedirect(self.get_requirement_redirect(request))
                
-               # If this isn't already the login page, display it.
-               if not request.POST.has_key(LOGIN_FORM_KEY):
-                       if request.POST:
-                               message = _("Please log in again, because your session has expired.")
-                       else:
-                               message = ""
-                       return self.display_login_page(request, message, context)
-
-               # Check that the user accepts cookies.
-               if not request.session.test_cookie_worked():
-                       message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
-                       return self.display_login_page(request, message, context)
+               if request.method == 'POST':
+                       form = self.login_form(request=request, data=request.POST)
+                       if form.is_valid():
+                               redirect = self.get_requirement_redirect(request)
+                               login(request, form.get_user())
+                               
+                               if request.session.test_cookie_worked():
+                                       request.session.delete_test_cookie()
+                               
+                               return HttpResponseRedirect(redirect)
                else:
-                       request.session.delete_test_cookie()
+                       form = self.login_form()
                
-               # Check the password.
-               username = request.POST.get('username', None)
-               password = request.POST.get('password', None)
-               user = authenticate(username=username, password=password)
-               if user is None:
-                       message = ERROR_MESSAGE
-                       if username is not None and u'@' in username:
-                               # Mistakenly entered e-mail address instead of username? Look it up.
-                               try:
-                                       user = User.objects.get(email=username)
-                               except (User.DoesNotExist, User.MultipleObjectsReturned):
-                                       message = _("Usernames cannot contain the '@' character.")
-                               else:
-                                       if user.check_password(password):
-                                               message = _("Your e-mail address is not your username."
-                                                                       " Try '%s' instead.") % user.username
-                                       else:
-                                               message = _("Usernames cannot contain the '@' character.")
-                       return self.display_login_page(request, message, context)
-
-               # The user data is correct; log in the user in and continue.
-               else:
-                       if user.is_active:
-                               login(request, user)
-                               try:
-                                       redirect = request.session.pop('redirect')
-                               except KeyError:
-                                       redirect = request.node.get_absolute_url()
-                               return HttpResponseRedirect(redirect)
-                       else:
-                               return self.display_login_page(request, ERROR_MESSAGE, context)
-       login = never_cache(login)
+               request.session.set_test_cookie()
+               
+               context = self.get_context()
+               context.update(extra_context or {})
+               context.update({
+                       'form': form
+               })
+               return self.login_page.render_to_response(request, extra_context=context)
        
-       def logout(self, request):
+       @never_cache
+       def logout(self, request, extra_context=None):
                return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
        
        def login_required(self, view):
                def inner(request, *args, **kwargs):
                        if not request.user.is_authenticated():
+                               self.set_requirement_redirect(request, redirect=request.path)
+                               if request.POST:
+                                       messages.add_message(request, messages.ERROR, "Please log in again, because your session has expired.")
                                return HttpResponseRedirect(self.reverse('login', node=request.node))
                        return view(request, *args, **kwargs)
                
                return inner
        
+       class Meta:
+               abstract = True
+
+
+class PasswordMultiView(LoginMultiView):
+       "Adds on views for password-related functions."
+       password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
+       password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
+       password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
+       password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
+       
+       @property
+       def urlpatterns(self):
+               urlpatterns = super(PasswordMultiView, self).urlpatterns
+               
+               if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
+                       urlpatterns += patterns('',
+                               url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
+                               url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
+                       )
+               
+               if self.password_change_page:
+                       urlpatterns += patterns('',
+                               url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
+                       )
+               return urlpatterns
+       
+       def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None):
+               token = token_generator.make_token(user, *(token_args or []))
+               kwargs = {
+                       'uidb36': int_to_base36(user.id),
+                       'token': token
+               }
+               kwargs.update(reverse_kwargs or {})
+               return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True)
+       
        def send_confirmation_email(self, subject, email, page, extra_context):
                text_content = page.render_to_string(extra_context=extra_context)
                from_email = 'noreply@%s' % Site.objects.get_current().domain
@@ -259,12 +245,32 @@ class LoginMultiView(MultiView):
                })
                return self.password_change_page.render_to_response(request, extra_context=context)
        
+       class Meta:
+               abstract = True
+
+
+class RegistrationMultiView(PasswordMultiView):
+       """Adds on the pages necessary for letting new users register."""
+       register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
+       register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
+       registration_form = RegistrationForm
+       
+       @property
+       def urlpatterns(self):
+               urlpatterns = super(RegistrationMultiView, self).urlpatterns
+               if self.register_page and self.register_confirmation_email:
+                       urlpatterns += patterns('',
+                               url(r'^register$', csrf_protect(self.register), name='register'),
+                               url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
+                       )
+               return urlpatterns
+       
        def register(self, request, extra_context=None, token_generator=registration_token_generator):
                if request.user.is_authenticated():
                        return HttpResponseRedirect(request.node.get_absolute_url())
                
                if request.method == 'POST':
-                       form = RegistrationForm(request.POST)
+                       form = self.registration_form(request.POST)
                        if form.is_valid():
                                user = form.save()
                                context = {
@@ -275,7 +281,7 @@ class LoginMultiView(MultiView):
                                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()
+                       form = self.registration_form()
                
                context = self.get_context()
                context.update(extra_context or {})
@@ -307,7 +313,7 @@ class LoginMultiView(MultiView):
                                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.
+                               # if anything goes wrong, do our best make sure that the true password is restored.
                                user.password = true_password
                                user.save()
                        return self.post_register_confirm_redirect(request)
@@ -321,23 +327,28 @@ class LoginMultiView(MultiView):
                abstract = True
 
 
-class AccountMultiView(LoginMultiView):
+class AccountMultiView(RegistrationMultiView):
        """
        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')
+       manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
+       email_change_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_email_change_confirmation_email_related', blank=True, null=True, help_text="If this is left blank, email changes will be performed without confirmation.")
+       
        account_form = UserAccountForm
        
        @property
        def urlpatterns(self):
                urlpatterns = super(AccountMultiView, self).urlpatterns
-               urlpatterns += patterns('',
-                       url(r'^account$', self.login_required(self.account_view), name='account'),
-                       url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
-               )
+               if self.manage_account_page:
+                       urlpatterns += patterns('',
+                               url(r'^account$', self.login_required(self.account_view), name='account'),
+                       )
+               if self.email_change_confirmation_email:
+                       urlpatterns += patterns('',
+                               url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
+                       )
                return urlpatterns
        
        def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
@@ -345,10 +356,14 @@ class AccountMultiView(LoginMultiView):
                        form = self.account_form(request.user, request.POST, request.FILES)
                        
                        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.
+                               message = "Account information saved."
+                               redirect = self.get_requirement_redirect(request, default='')
+                               if 'email' in form.changed_data and self.email_change_confirmation_email:
+                                       # 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. We only do this if an email
+                                       # change confirmation email is available.
                                        request.user.email = form.initial['email']
                                        
                                        email = form.cleaned_data.pop('email')
@@ -358,11 +373,19 @@ class AccountMultiView(LoginMultiView):
                                        }
                                        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)
+                                       
+                                       message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
+                                       if not request.user.email:
+                                               message += " You will need to confirm the email before accessing pages that require a valid account."
+                                               redirect = ''
                                
                                form.save()
-                               messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
-                               return HttpResponseRedirect('')
+                               
+                               if redirect != '':
+                                       message += " Here you go!"
+                               
+                               messages.add_message(request, messages.SUCCESS, message, fail_silently=True)
+                               return HttpResponseRedirect(redirect)
                else:
                        form = self.account_form(request.user)
                
@@ -381,17 +404,23 @@ class AccountMultiView(LoginMultiView):
        def account_required(self, view):
                def inner(request, *args, **kwargs):
                        if not self.has_valid_account(request.user):
-                               if not request.method == "POST":
-                                       messages.add_message(request, messages.ERROR, "You need to add some account information before you can access this page.", fail_silently=True)
-                               return self.account_view(request, *args, **kwargs)
+                               messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
+                               if self.manage_account_page:
+                                       self.set_requirement_redirect(request, redirect=request.path)
+                                       redirect = self.reverse('account', node=request.node)
+                               else:
+                                       redirect = node.get_absolute_url()
+                               return HttpResponseRedirect(redirect)
                        return view(request, *args, **kwargs)
                
                inner = self.login_required(inner)
                return inner
        
        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(self.reverse('account', node=request.node))
+               if self.manage_account_page:
+                       messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
+                       return HttpResponseRedirect(self.reverse('account', node=request.node))
+               return super(AccountMultiView, self).post_register_confirm_redirect(request)
        
        def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
                """
@@ -416,7 +445,11 @@ class AccountMultiView(LoginMultiView):
                        user.email = email
                        user.save()
                        messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
-                       return HttpReponseRedirect(self.reverse('account', node=request.node))
+                       if self.manage_account_page:
+                               redirect = self.reverse('account', node=request.node)
+                       else:
+                               redirect = request.node.get_absolute_url()
+                       return HttpResponseRedirect(redirect)
                
                raise Http404
        
index 10c51b4..07a5e0a 100644 (file)
@@ -114,8 +114,8 @@ class View(Entity):
                
                try:
                        subpath = reverse(view_name, urlconf=self, args=args or [], kwargs=kwargs or {})
-               except NoReverseMatch:
-                       raise ViewCanNotProvideSubpath
+               except NoReverseMatch, e:
+                       raise ViewCanNotProvideSubpath(e.message)
                
                if node is not None:
                        return node.construct_url(subpath)
@@ -219,7 +219,7 @@ class TargetURLModel(models.Model):
                
                try:
                        self.get_target_url()
-               except NoReverseMatch, e:
+               except (NoReverseMatch, ViewCanNotProvideSubpath), e:
                        raise ValidationError(e.message)
                
                super(TargetURLModel, self).clean()