Token-based User registration functional
[philo.git] / contrib / waldo / models.py
1 from django import forms
2 from django.conf.urls.defaults import url, patterns, include
3 from django.contrib import messages
4 from django.contrib.auth import authenticate, login, views as auth_views
5 from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm
6 from django.contrib.auth.models import User
7 from django.contrib.sites.models import Site
8 from django.core.mail import send_mail
9 from django.core.urlresolvers import reverse
10 from django.db import models
11 from django.http import Http404, HttpResponseRedirect
12 from django.shortcuts import render_to_response, get_object_or_404
13 from django.utils.http import int_to_base36, base36_to_int
14 from django.utils.translation import ugettext_lazy, ugettext as _
15 from django.views.decorators.cache import never_cache
16 from django.views.decorators.csrf import csrf_protect
17 from philo.models import MultiView, Page
18 from philo.contrib.waldo.forms import LOGIN_FORM_KEY, LoginForm, RegistrationForm
19 from philo.contrib.waldo.tokens import default_token_generator
20
21
22 ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.")
23
24
25 def get_field_data(obj, fields):
26         if fields == None:
27                 fields = [field.name for field in obj._meta.fields if field.editable]
28         
29         return dict([(field.name, field.value_from_object(obj)) for field in obj._meta.fields if field.name in fields])
30
31
32 class LoginMultiView(MultiView):
33         """
34         Handles login, registration, and forgotten passwords. In other words, this
35         multiview provides exclusively view and methods related to usernames and
36         passwords.
37         """
38         login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
39         password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related')
40         register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related')
41         register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related')
42         
43         @property
44         def urlpatterns(self):
45                 urlpatterns = patterns('',
46                         url(r'^login/$', self.login, name='login'),
47                         url(r'^logout/$', self.logout, name='logout')
48                 )
49                 urlpatterns += patterns('',
50                         url(r'^password/reset/$', csrf_protect(self.password_reset), name='password_reset'),
51                         url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)/$',
52                                 self.password_reset_confirm, name='password_reset_confirm')
53                 )
54                 urlpatterns += patterns('',
55                         url(r'^register/$', csrf_protect(self.register), name='register'),
56                         url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)/$',
57                                 self.register_confirm, name='register_confirm')
58                 )
59                 return urlpatterns
60         
61         def get_context(self, extra_dict=None):
62                 context = {}
63                 context.update(extra_dict or {})
64                 return context
65         
66         def display_login_page(self, request, message, node=None, extra_context=None):
67                 request.session.set_test_cookie()
68                 
69                 redirect = request.META.get('HTTP_REFERER', None)
70                 path = request.get_full_path()
71                 if redirect != path:
72                         if redirect is None:
73                                 redirect = '/'.join(path.split('/')[:-2])
74                         request.session['redirect'] = redirect
75                 
76                 if request.POST:
77                         form = LoginForm(request.POST)
78                 else:
79                         form = LoginForm()
80                 context = self.get_context({
81                         'message': message,
82                         'form': form
83                 })
84                 context.update(extra_context or {})
85                 return self.login_page.render_to_response(node, request, extra_context=context)
86         
87         def login(self, request, node=None, extra_context=None):
88                 """
89                 Displays the login form for the given HttpRequest.
90                 """
91                 context = self.get_context(extra_context)
92                 
93                 from django.contrib.auth.models import User
94                 
95                 # If this isn't already the login page, display it.
96                 if not request.POST.has_key(LOGIN_FORM_KEY):
97                         if request.POST:
98                                 message = _("Please log in again, because your session has expired.")
99                         else:
100                                 message = ""
101                         return self.display_login_page(request, message, node, context)
102
103                 # Check that the user accepts cookies.
104                 if not request.session.test_cookie_worked():
105                         message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.")
106                         return self.display_login_page(request, message, node, context)
107                 else:
108                         request.session.delete_test_cookie()
109                 
110                 # Check the password.
111                 username = request.POST.get('username', None)
112                 password = request.POST.get('password', None)
113                 user = authenticate(username=username, password=password)
114                 if user is None:
115                         message = ERROR_MESSAGE
116                         if username is not None and u'@' in username:
117                                 # Mistakenly entered e-mail address instead of username? Look it up.
118                                 try:
119                                         user = User.objects.get(email=username)
120                                 except (User.DoesNotExist, User.MultipleObjectsReturned):
121                                         message = _("Usernames cannot contain the '@' character.")
122                                 else:
123                                         if user.check_password(password):
124                                                 message = _("Your e-mail address is not your username."
125                                                                         " Try '%s' instead.") % user.username
126                                         else:
127                                                 message = _("Usernames cannot contain the '@' character.")
128                         return self.display_login_page(request, message, node, context)
129
130                 # The user data is correct; log in the user in and continue.
131                 else:
132                         if user.is_active:
133                                 login(request, user)
134                                 redirect = request.session.pop('redirect')
135                                 return HttpResponseRedirect(redirect)
136                         else:
137                                 return self.display_login_page(request, ERROR_MESSAGE, node, context)
138         login = never_cache(login)
139         
140         def logout(self, request):
141                 return auth_views.logout(request, request.META['HTTP_REFERER'])
142         
143         def login_required(self, view):
144                 def inner(request, node=None, *args, **kwargs):
145                         if not request.user.is_authenticated():
146                                 login_url = reverse('login', urlconf=self).strip('/')
147                                 return HttpResponseRedirect('%s%s/' % (node.get_absolute_url(), login_url))
148                         return view(request, node=node, *args, **kwargs)
149                 
150                 return inner
151         
152         def send_confirmation_email(self, subject, email, page, extra_context):
153                 message = page.render_to_string(extra_context=extra_context)
154                 from_email = 'noreply@%s' % Site.objects.get_current().domain
155                 send_mail(subject, message, from_email, [email])
156         
157         def password_reset(self, request, node=None, extra_context=None):
158                 pass
159         
160         def password_reset_confirm(self, request, node=None, extra_context=None):
161                 pass
162         
163         def register(self, request, node=None, extra_context=None, token_generator=default_token_generator):
164                 if request.user.is_authenticated():
165                         return HttpResponseRedirect(node.get_absolute_url())
166                 
167                 if request.method == 'POST':
168                         form = RegistrationForm(request.POST)
169                         if form.is_valid():
170                                 user = form.save()
171                                 current_site = Site.objects.get_current()
172                                 token = default_token_generator.make_token(user)
173                                 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('/'))
174                                 context = {
175                                         'link': link
176                                 }
177                                 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
178                                 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email)
179                                 return HttpResponseRedirect('')
180                 else:
181                         form = RegistrationForm()
182                 
183                 context = self.get_context({'form': form})
184                 context.update(extra_context or {})
185                 return self.register_page.render_to_response(node, request, extra_context=context)
186         
187         def register_confirm(self, request, node=None, extra_context=None, uidb36=None, token=None):
188                 """
189                 Checks that a given hash in a registration link is valid and activates
190                 the given account. If so, log them in and redirect to
191                 self.post_register_confirm_redirect.
192                 """
193                 assert uidb36 is not None and token is not None
194                 try:
195                         uid_int = base36_to_int(uidb36)
196                 except:
197                         raise Http404
198                 
199                 user = get_object_or_404(User, id=uid_int)
200                 if default_token_generator.check_token(user, token):
201                         user.is_active = True
202                         user.save()
203                         messages.add_message(request, messages.SUCCESS, "Your account's been created! Go ahead and log in.")
204                         return self.post_register_confirm_redirect(request, node)
205                 
206                 raise Http404
207         
208         def post_register_confirm_redirect(self, request, node):
209                 return HttpResponseRedirect('/%s/%s/' % (node.get_absolute_url().strip('/'), reverse('login', urlconf=self).strip('/')))
210         
211         class Meta:
212                 abstract = True
213
214
215 class AccountMultiView(LoginMultiView):
216         """
217         Subclasses may define an account_profile model, fields from the User model
218         to include in the account, and fields from the account profile to use in
219         the account.
220         """
221         manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_page')
222         user_fields = ['first_name', 'last_name', 'email']
223         required_user_fields = user_fields
224         account_profile = None
225         account_profile_fields = None
226         
227         @property
228         def urlpatterns(self):
229                 urlpatterns = super(AccountMultiView, self).urlpatterns
230                 urlpatterns += patterns('',
231                         url(r'^account/$', self.login_required(self.account_view), name='account')
232                 )
233                 return urlpatterns
234         
235         def get_account_forms(self):
236                 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
237                 
238                 if self.account_profile is None:
239                         profile_form = None
240                 else:
241                         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'])
242                 
243                 for field_name, field in user_form.base_fields.items():
244                         if field_name in self.required_user_fields:
245                                 field.required = True
246                 return user_form, profile_form
247         
248         def get_account_form_instances(self, user, data=None):
249                 form_instances = []
250                 user_form, profile_form = self.get_account_forms()
251                 if data is None:
252                         form_instances.append(user_form(instance=user))
253                         if profile_form:
254                                 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
255                 else:
256                         form_instances.append(user_form(data, instance=user))
257                         if profile_form:
258                                 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
259                 
260                 return form_instances
261         
262         def account_view(self, request, node=None, extra_context=None):
263                 if request.method == 'POST':
264                         form_instances = self.get_account_form_instances(request.user, request.POST)
265                         
266                         for form in form_instances:
267                                 if not form.is_valid():
268                                         break
269                         else:
270                                 for form in form_instances:
271                                         form.save()
272                                 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
273                                 return HttpResponseRedirect('')
274                 else:
275                         form_instances = self.get_account_form_instances(request.user)
276                 
277                 context = self.get_context({
278                         'forms': form_instances
279                 })
280                 context.update(extra_context or {})
281                 return self.manage_account_page.render_to_response(node, request, extra_context=context)
282         
283         def has_valid_account(self, user):
284                 user_form, profile_form = self.get_account_forms()
285                 forms = []
286                 forms.append(user_form(data=get_field_data(user, self.user_fields)))
287                 
288                 if profile_form is not None:
289                         profile = self.account_profile._default_manager.get_or_create(user=user)[0]
290                         forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
291                 
292                 for form in forms:
293                         if not form.is_valid():
294                                 return False
295                 return True
296         
297         def account_required(self, view):
298                 def inner(request, *args, **kwargs):
299                         if not self.has_valid_account(request.user):
300                                 messages.add_message(request, messages.ERROR, "You need to add some account information before you can post listings.")
301                                 return self.account_view(request, *args, **kwargs)
302                         return view(request, *args, **kwargs)
303                 
304                 inner = self.login_required(inner)
305                 return inner
306         
307         class Meta:
308                 abstract = True