Implementation of basic conversion between Django forms and ExtJS forms.
[philo.git] / contrib / waldo / models.py
index d0669ff..93edab1 100644 (file)
@@ -2,22 +2,26 @@ from django import forms
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib import messages
 from django.contrib.auth import authenticate, login, views as auth_views
 from django.conf.urls.defaults import url, patterns, include
 from django.contrib import messages
 from django.contrib.auth import authenticate, login, views as auth_views
-from django.contrib.auth.forms import AuthenticationForm
+from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm
 from django.contrib.auth.models import User
 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 send_mail
+from django.core.urlresolvers import reverse
 from django.db import models
 from django.http import Http404, HttpResponseRedirect
 from django.db import models
 from django.http import Http404, HttpResponseRedirect
-from django.shortcuts import render_to_response
+from django.shortcuts import render_to_response, get_object_or_404
+from django.utils.http import int_to_base36, base36_to_int
 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 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.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.")
 
 
 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
-LOGIN_FORM_KEY = 'this_is_the_login_form'
-LoginForm = type('LoginForm', (AuthenticationForm,), {
-       LOGIN_FORM_KEY: forms.BooleanField(widget=forms.HiddenInput, initial=True)
-})
 
 
 def get_field_data(obj, fields):
 
 
 def get_field_data(obj, fields):
@@ -28,14 +32,37 @@ def get_field_data(obj, fields):
 
 
 class LoginMultiView(MultiView):
 
 
 class LoginMultiView(MultiView):
-       login_page = models.ForeignKey(Page, related_name='login_related')
+       """
+       Handles login, registration, and forgotten passwords. In other words, this
+       multiview provides exclusively view and methods related to usernames and
+       passwords.
+       """
+       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')
        
        @property
        def urlpatterns(self):
                urlpatterns = patterns('',
                        url(r'^login/$', self.login, name='login'),
        
        @property
        def urlpatterns(self):
                urlpatterns = patterns('',
                        url(r'^login/$', self.login, name='login'),
-                       url(r'^logout/$', self.logout, name='logout')
+                       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 get_context(self, extra_dict=None):
                return urlpatterns
        
        def get_context(self, extra_dict=None):
@@ -46,7 +73,19 @@ class LoginMultiView(MultiView):
        def display_login_page(self, request, message, node=None, extra_context=None):
                request.session.set_test_cookie()
                
        def display_login_page(self, request, message, node=None, extra_context=None):
                request.session.set_test_cookie()
                
-               redirect = request.META.get('HTTP_REFERER', None)
+               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 = ''.join(referrer[2:])
+               
+               if referrer is None:
+                       redirect = node.get_absolute_url()
+               
                path = request.get_full_path()
                if redirect != path:
                        if redirect is None:
                path = request.get_full_path()
                if redirect != path:
                        if redirect is None:
@@ -111,7 +150,10 @@ class LoginMultiView(MultiView):
                else:
                        if user.is_active:
                                login(request, user)
                else:
                        if user.is_active:
                                login(request, user)
-                               redirect = request.session.pop('redirect')
+                               try:
+                                       redirect = request.session.pop('redirect')
+                               except KeyError:
+                                       redirect = node.get_absolute_url()
                                return HttpResponseRedirect(redirect)
                        else:
                                return self.display_login_page(request, ERROR_MESSAGE, node, context)
                                return HttpResponseRedirect(redirect)
                        else:
                                return self.display_login_page(request, ERROR_MESSAGE, node, context)
@@ -123,13 +165,138 @@ class LoginMultiView(MultiView):
        def login_required(self, view):
                def inner(request, node=None, *args, **kwargs):
                        if not request.user.is_authenticated():
        def login_required(self, view):
                def inner(request, node=None, *args, **kwargs):
                        if not request.user.is_authenticated():
-                               root_url = node.get_path(Site.objects.get_current().root_node).strip('/')
                                login_url = reverse('login', urlconf=self).strip('/')
                                login_url = reverse('login', urlconf=self).strip('/')
-                               return HttpResponseRedirect('/%s/%s/' % (root_url, login_url))
+                               return HttpResponseRedirect('%s%s/' % (node.get_absolute_url(), login_url))
                        return view(request, node=node, *args, **kwargs)
                
                return inner
        
                        return view(request, node=node, *args, **kwargs)
                
                return inner
        
+       def send_confirmation_email(self, subject, email, page, extra_context):
+               message = page.render_to_string(extra_context=extra_context)
+               from_email = 'noreply@%s' % Site.objects.get_current().domain
+               send_mail(subject, message, from_email, [email])
+       
+       def password_reset(self, request, node=None, extra_context=None, token_generator=password_token_generator):
+               if request.method == 'POST':
+                       form = PasswordResetForm(request.POST)
+                       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, node.get_absolute_url().strip('/'), reverse('password_reset_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
+                                       context = {
+                                               'link': link,
+                                               '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)
+                                       messages.add_message(request, messages.SUCCESS, "An email has been sent to the address you provided with details on resetting your password.", fail_silently=True)
+                               return HttpResponseRedirect('')
+               else:
+                       form = PasswordResetForm()
+               
+               context = self.get_context({'form': form})
+               context.update(extra_context or {})
+               return self.password_reset_page.render_to_response(node, request, extra_context=context)
+       
+       def password_reset_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
+               """
+               Checks that a given hash in a password reset link is valid. If so,
+               displays the password set form.
+               """
+               assert uidb36 is not None and token is not None
+               try:
+                       uid_int = base36_to_int(uidb36)
+               except:
+                       raise Http404
+               
+               user = get_object_or_404(User, id=uid_int)
+               
+               if token_generator.check_token(user, token):
+                       if request.method == 'POST':
+                               form = SetPasswordForm(user, request.POST)
+                               
+                               if form.is_valid():
+                                       form.save()
+                                       messages.add_message(request, messages.SUCCESS, "Password reset successful.")
+                                       return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('login', urlconf=self).strip('/')))
+                       else:
+                               form = SetPasswordForm(user)
+                       
+                       context = self.get_context({'form': form})
+                       return self.password_set_page.render_to_response(node, request, extra_context=context)
+               
+               raise Http404
+       
+       def password_change(self, request, node=None, extra_context=None):
+               if request.method == 'POST':
+                       form = PasswordChangeForm(request.user, request.POST)
+                       if form.is_valid():
+                               form.save()
+                               messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
+                               return HttpResponseRedirect('')
+               else:
+                       form = PasswordChangeForm(request.user)
+               
+               context = self.get_context({'form': form})
+               context.update(extra_context or {})
+               return self.password_change_page.render_to_response(node, request, extra_context=context)
+       
+       def register(self, request, node=None, extra_context=None, token_generator=registration_token_generator):
+               if request.user.is_authenticated():
+                       return HttpResponseRedirect(node.get_absolute_url())
+               
+               if request.method == 'POST':
+                       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, node.get_absolute_url().strip('/'), reverse('register_confirm', urlconf=self, kwargs={'uidb36': int_to_base36(user.id), 'token': token}).strip('/'))
+                               context = {
+                                       'link': link
+                               }
+                               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('')
+               else:
+                       form = RegistrationForm()
+               
+               context = self.get_context({'form': form})
+               context.update(extra_context or {})
+               return self.register_page.render_to_response(node, request, extra_context=context)
+       
+       def register_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
+               """
+               Checks that a given hash in a registration link is valid and activates
+               the given account. If so, log them in and redirect to
+               self.post_register_confirm_redirect.
+               """
+               assert uidb36 is not None and token is not None
+               try:
+                       uid_int = base36_to_int(uidb36)
+               except:
+                       raise Http404
+               
+               user = get_object_or_404(User, id=uid_int)
+               if token_generator.check_token(user, token):
+                       user.is_active = True
+                       true_password = user.password
+                       try:
+                               user.set_password('temp_password')
+                               user.save()
+                               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.
+                               user.password = true_password
+                               user.save()
+                       return self.post_register_confirm_redirect(request, node)
+               
+               raise Http404
+       
+       def post_register_confirm_redirect(self, request, node):
+               return HttpResponseRedirect(node.get_absolute_url())
+       
        class Meta:
                abstract = True
 
        class Meta:
                abstract = True
 
@@ -140,7 +307,8 @@ class AccountMultiView(LoginMultiView):
        to include in the account, and fields from the account profile to use in
        the account.
        """
        to include in the account, and fields from the account profile to use in
        the account.
        """
-       manage_account_page = models.ForeignKey(Page, related_name='classifieds_manage_account_page')
+       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
        user_fields = ['first_name', 'last_name', 'email']
        required_user_fields = user_fields
        account_profile = None
@@ -150,7 +318,8 @@ class AccountMultiView(LoginMultiView):
        def urlpatterns(self):
                urlpatterns = super(AccountMultiView, self).urlpatterns
                urlpatterns += patterns('',
        def urlpatterns(self):
                urlpatterns = super(AccountMultiView, self).urlpatterns
                urlpatterns += patterns('',
-                       url(r'^account/$', self.login_required(self.account_view), name='account')
+                       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')
                )
                return urlpatterns
        
                )
                return urlpatterns
        
@@ -181,14 +350,33 @@ class AccountMultiView(LoginMultiView):
                
                return form_instances
        
                
                return form_instances
        
-       def account_view(self, request, node=None, extra_context=None):
+       def account_view(self, request, node=None, extra_context=None, token_generator=email_token_generator):
                if request.method == 'POST':
                        form_instances = self.get_account_form_instances(request.user, request.POST)
                if request.method == 'POST':
                        form_instances = self.get_account_form_instances(request.user, request.POST)
+                       current_email = request.user.email
                        
                        for form in form_instances:
                                if not form.is_valid():
                                        break
                        else:
                        
                        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
+                                       
+                                       for form in form_instances:
+                                               form.cleaned_data.pop('email', None)
+                                       
+                                       current_site = Site.objects.get_current()
+                                       token = token_generator.make_token(request.user, email)
+                                       link = 'http://%s/%s/%s/' % (current_site.domain, 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
+                                       }
+                                       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()
                                messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
                                for form in form_instances:
                                        form.save()
                                messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
@@ -219,12 +407,43 @@ class AccountMultiView(LoginMultiView):
        def account_required(self, view):
                def inner(request, *args, **kwargs):
                        if not self.has_valid_account(request.user):
        def account_required(self, view):
                def inner(request, *args, **kwargs):
                        if not self.has_valid_account(request.user):
-                               messages.add_message(request, messages.ERROR, "You need to add some account information before you can post listings.")
+                               messages.add_message(request, messages.ERROR, "You need to add some account information before you can post listings.", fail_silently=True)
                                return self.account_view(request, *args, **kwargs)
                        return view(request, *args, **kwargs)
                
                inner = self.login_required(inner)
                return inner
        
                                return self.account_view(request, *args, **kwargs)
                        return view(request, *args, **kwargs)
                
                inner = self.login_required(inner)
                return inner
        
+       def post_register_confirm_redirect(self, request, node):
+               messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
+               return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
+       
+       def email_change_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
+               """
+               Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
+               """
+               assert uidb36 is not None and token is not None and email is not None
+               
+               try:
+                       uid_int = base36_to_int(uidb36)
+               except:
+                       raise Http404
+               
+               user = get_object_or_404(User, id=uid_int)
+               
+               email = email.replace('+', '@')
+               
+               if email == user.email:
+                       # Then short-circuit.
+                       raise Http404
+               
+               if token_generator.check_token(user, email, token):
+                       user.email = email
+                       user.save()
+                       messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
+                       return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('account', urlconf=self).strip('/')))
+               
+               raise Http404
+       
        class Meta:
                abstract = True
\ No newline at end of file
        class Meta:
                abstract = True
\ No newline at end of file