Added basic docs for entities and attributes.
[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.auth.tokens import default_token_generator as password_token_generator
8 from django.contrib.sites.models import Site
9 from django.core.mail import EmailMultiAlternatives, send_mail
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.template.defaultfilters import striptags
14 from django.utils.http import int_to_base36, base36_to_int
15 from django.utils.translation import ugettext as _
16 from django.views.decorators.cache import never_cache
17 from django.views.decorators.csrf import csrf_protect
18 from philo.models import MultiView, Page
19 from philo.contrib.waldo.forms import WaldoAuthenticationForm, RegistrationForm, UserAccountForm
20 from philo.contrib.waldo.tokens import registration_token_generator, email_token_generator
21 import urlparse
22
23
24 class LoginMultiView(MultiView):
25         """
26         Handles exclusively methods and views related to logging users in and out.
27         """
28         login_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_login_related')
29         login_form = WaldoAuthenticationForm
30         
31         @property
32         def urlpatterns(self):
33                 return patterns('',
34                         url(r'^login$', self.login, name='login'),
35                         url(r'^logout$', self.logout, name='logout'),
36                 )
37         
38         def set_requirement_redirect(self, request, redirect=None):
39                 "Figure out where someone should end up after landing on a `requirement` page like the login page."
40                 if redirect is not None:
41                         pass
42                 elif 'requirement_redirect' in request.session:
43                         return
44                 else:
45                         referrer = request.META.get('HTTP_REFERER', None)
46                 
47                         if referrer is not None:
48                                 referrer = urlparse.urlparse(referrer)
49                                 host = referrer[1]
50                                 if host != request.get_host():
51                                         referrer = None
52                                 else:
53                                         redirect = '%s?%s' % (referrer[2], referrer[4])
54                 
55                         path = request.get_full_path()
56                         if referrer is None or redirect == path:
57                                 # Default to the index page if we can't find a referrer or
58                                 # if we'd otherwise redirect to where we already are.
59                                 redirect = request.node.get_absolute_url()
60                 
61                 request.session['requirement_redirect'] = redirect
62         
63         def get_requirement_redirect(self, request, default=None):
64                 redirect = request.session.pop('requirement_redirect', None)
65                 # Security checks a la django.contrib.auth.views.login
66                 if not redirect or ' ' in redirect:
67                         redirect = default
68                 else:
69                         netloc = urlparse.urlparse(redirect)[1]
70                         if netloc and netloc != request.get_host():
71                                 redirect = default
72                 if redirect is None:
73                         redirect = request.node.get_absolute_url()
74                 return redirect
75         
76         @never_cache
77         def login(self, request, extra_context=None):
78                 """
79                 Displays the login form for the given HttpRequest.
80                 """
81                 self.set_requirement_redirect(request)
82                 
83                 # Redirect already-authenticated users to the index page.
84                 if request.user.is_authenticated():
85                         messages.add_message(request, messages.INFO, "You are already authenticated. Please log out if you wish to log in as a different user.")
86                         return HttpResponseRedirect(self.get_requirement_redirect(request))
87                 
88                 if request.method == 'POST':
89                         form = self.login_form(request=request, data=request.POST)
90                         if form.is_valid():
91                                 redirect = self.get_requirement_redirect(request)
92                                 login(request, form.get_user())
93                                 
94                                 if request.session.test_cookie_worked():
95                                         request.session.delete_test_cookie()
96                                 
97                                 return HttpResponseRedirect(redirect)
98                 else:
99                         form = self.login_form()
100                 
101                 request.session.set_test_cookie()
102                 
103                 context = self.get_context()
104                 context.update(extra_context or {})
105                 context.update({
106                         'form': form
107                 })
108                 return self.login_page.render_to_response(request, extra_context=context)
109         
110         @never_cache
111         def logout(self, request, extra_context=None):
112                 return auth_views.logout(request, request.META.get('HTTP_REFERER', request.node.get_absolute_url()))
113         
114         def login_required(self, view):
115                 def inner(request, *args, **kwargs):
116                         if not request.user.is_authenticated():
117                                 self.set_requirement_redirect(request, redirect=request.path)
118                                 if request.POST:
119                                         messages.add_message(request, messages.ERROR, "Please log in again, because your session has expired.")
120                                 return HttpResponseRedirect(self.reverse('login', node=request.node))
121                         return view(request, *args, **kwargs)
122                 
123                 return inner
124         
125         class Meta:
126                 abstract = True
127
128
129 class PasswordMultiView(LoginMultiView):
130         "Adds on views for password-related functions."
131         password_reset_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_related', blank=True, null=True)
132         password_reset_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_reset_confirmation_email_related', blank=True, null=True)
133         password_set_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_set_related', blank=True, null=True)
134         password_change_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_password_change_related', blank=True, null=True)
135         
136         password_change_form = PasswordChangeForm
137         password_set_form = SetPasswordForm
138         password_reset_form = PasswordResetForm
139         
140         @property
141         def urlpatterns(self):
142                 urlpatterns = super(PasswordMultiView, self).urlpatterns
143                 
144                 if self.password_reset_page and self.password_reset_confirmation_email and self.password_set_page:
145                         urlpatterns += patterns('',
146                                 url(r'^password/reset$', csrf_protect(self.password_reset), name='password_reset'),
147                                 url(r'^password/reset/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.password_reset_confirm, name='password_reset_confirm'),
148                         )
149                 
150                 if self.password_change_page:
151                         urlpatterns += patterns('',
152                                 url(r'^password/change$', csrf_protect(self.login_required(self.password_change)), name='password_change'),
153                         )
154                 return urlpatterns
155         
156         def make_confirmation_link(self, confirmation_view, token_generator, user, node, token_args=None, reverse_kwargs=None, secure=False):
157                 token = token_generator.make_token(user, *(token_args or []))
158                 kwargs = {
159                         'uidb36': int_to_base36(user.id),
160                         'token': token
161                 }
162                 kwargs.update(reverse_kwargs or {})
163                 return node.construct_url(subpath=self.reverse(confirmation_view, kwargs=kwargs), with_domain=True, secure=secure)
164         
165         def send_confirmation_email(self, subject, email, page, extra_context):
166                 text_content = page.render_to_string(extra_context=extra_context)
167                 from_email = 'noreply@%s' % Site.objects.get_current().domain
168                 
169                 if page.template.mimetype == 'text/html':
170                         msg = EmailMultiAlternatives(subject, striptags(text_content), from_email, [email])
171                         msg.attach_alternative(text_content, 'text/html')
172                         msg.send()
173                 else:
174                         send_mail(subject, text_content, from_email, [email])
175         
176         def password_reset(self, request, extra_context=None, token_generator=password_token_generator):
177                 if request.user.is_authenticated():
178                         return HttpResponseRedirect(request.node.get_absolute_url())
179                 
180                 if request.method == 'POST':
181                         form = self.password_reset_form(request.POST)
182                         if form.is_valid():
183                                 current_site = Site.objects.get_current()
184                                 for user in form.users_cache:
185                                         context = {
186                                                 'link': self.make_confirmation_link('password_reset_confirm', token_generator, user, request.node, secure=request.is_secure()),
187                                                 'user': user,
188                                                 'site': current_site,
189                                                 'request': request,
190                                                 
191                                                 # Deprecated... leave in for backwards-compatibility
192                                                 'username': user.username
193                                         }
194                                         self.send_confirmation_email('Confirm password reset for account at %s' % current_site.domain, user.email, self.password_reset_confirmation_email, context)
195                                         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)
196                                 return HttpResponseRedirect('')
197                 else:
198                         form = self.password_reset_form()
199                 
200                 context = self.get_context()
201                 context.update(extra_context or {})
202                 context.update({
203                         'form': form
204                 })
205                 return self.password_reset_page.render_to_response(request, extra_context=context)
206         
207         def password_reset_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=password_token_generator):
208                 """
209                 Checks that a given hash in a password reset link is valid. If so,
210                 displays the password set form.
211                 """
212                 assert uidb36 is not None and token is not None
213                 try:
214                         uid_int = base36_to_int(uidb36)
215                 except:
216                         raise Http404
217                 
218                 user = get_object_or_404(User, id=uid_int)
219                 
220                 if token_generator.check_token(user, token):
221                         if request.method == 'POST':
222                                 form = self.password_set_form(user, request.POST)
223                                 
224                                 if form.is_valid():
225                                         form.save()
226                                         messages.add_message(request, messages.SUCCESS, "Password reset successful.")
227                                         return HttpResponseRedirect(self.reverse('login', node=request.node))
228                         else:
229                                 form = self.password_set_form(user)
230                         
231                         context = self.get_context()
232                         context.update(extra_context or {})
233                         context.update({
234                                 'form': form
235                         })
236                         return self.password_set_page.render_to_response(request, extra_context=context)
237                 
238                 raise Http404
239         
240         def password_change(self, request, extra_context=None):
241                 if request.method == 'POST':
242                         form = self.password_change_form(request.user, request.POST)
243                         if form.is_valid():
244                                 form.save()
245                                 messages.add_message(request, messages.SUCCESS, 'Password changed successfully.', fail_silently=True)
246                                 return HttpResponseRedirect('')
247                 else:
248                         form = self.password_change_form(request.user)
249                 
250                 context = self.get_context()
251                 context.update(extra_context or {})
252                 context.update({
253                         'form': form
254                 })
255                 return self.password_change_page.render_to_response(request, extra_context=context)
256         
257         class Meta:
258                 abstract = True
259
260
261 class RegistrationMultiView(PasswordMultiView):
262         """Adds on the pages necessary for letting new users register."""
263         register_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_related', blank=True, null=True)
264         register_confirmation_email = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_register_confirmation_email_related', blank=True, null=True)
265         registration_form = RegistrationForm
266         
267         @property
268         def urlpatterns(self):
269                 urlpatterns = super(RegistrationMultiView, self).urlpatterns
270                 if self.register_page and self.register_confirmation_email:
271                         urlpatterns += patterns('',
272                                 url(r'^register$', csrf_protect(self.register), name='register'),
273                                 url(r'^register/(?P<uidb36>\w+)/(?P<token>[^/]+)$', self.register_confirm, name='register_confirm')
274                         )
275                 return urlpatterns
276         
277         def register(self, request, extra_context=None, token_generator=registration_token_generator):
278                 if request.user.is_authenticated():
279                         return HttpResponseRedirect(request.node.get_absolute_url())
280                 
281                 if request.method == 'POST':
282                         form = self.registration_form(request.POST)
283                         if form.is_valid():
284                                 user = form.save()
285                                 current_site = Site.objects.get_current()
286                                 context = {
287                                         'link': self.make_confirmation_link('register_confirm', token_generator, user, request.node, secure=request.is_secure()),
288                                         'user': user,
289                                         'site': current_site,
290                                         'request': request
291                                 }
292                                 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
293                                 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email, fail_silently=True)
294                                 return HttpResponseRedirect(request.node.get_absolute_url())
295                 else:
296                         form = self.registration_form()
297                 
298                 context = self.get_context()
299                 context.update(extra_context or {})
300                 context.update({
301                         'form': form
302                 })
303                 return self.register_page.render_to_response(request, extra_context=context)
304         
305         def register_confirm(self, request, extra_context=None, uidb36=None, token=None, token_generator=registration_token_generator):
306                 """
307                 Checks that a given hash in a registration link is valid and activates
308                 the given account. If so, log them in and redirect to
309                 self.post_register_confirm_redirect.
310                 """
311                 assert uidb36 is not None and token is not None
312                 try:
313                         uid_int = base36_to_int(uidb36)
314                 except:
315                         raise Http404
316                 
317                 user = get_object_or_404(User, id=uid_int)
318                 if token_generator.check_token(user, token):
319                         user.is_active = True
320                         true_password = user.password
321                         temp_password = token_generator.make_token(user)
322                         try:
323                                 user.set_password(temp_password)
324                                 user.save()
325                                 authenticated_user = authenticate(username=user.username, password=temp_password)
326                                 login(request, authenticated_user)
327                         finally:
328                                 # if anything goes wrong, do our best make sure that the true password is restored.
329                                 user.password = true_password
330                                 user.save()
331                         return self.post_register_confirm_redirect(request)
332                 
333                 raise Http404
334         
335         def post_register_confirm_redirect(self, request):
336                 return HttpResponseRedirect(request.node.get_absolute_url())
337         
338         class Meta:
339                 abstract = True
340
341
342 class AccountMultiView(RegistrationMultiView):
343         """
344         By default, the `account` consists of the first_name, last_name, and email fields
345         of the User model. Using a different account model is as simple as writing a form that
346         accepts a User instance as the first argument.
347         """
348         manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_related', blank=True, null=True)
349         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.")
350         
351         account_form = UserAccountForm
352         
353         @property
354         def urlpatterns(self):
355                 urlpatterns = super(AccountMultiView, self).urlpatterns
356                 if self.manage_account_page:
357                         urlpatterns += patterns('',
358                                 url(r'^account$', self.login_required(self.account_view), name='account'),
359                         )
360                 if self.email_change_confirmation_email:
361                         urlpatterns += patterns('',
362                                 url(r'^account/email/(?P<uidb36>\w+)/(?P<email>[\w.]+[+][\w.]+)/(?P<token>[^/]+)$', self.email_change_confirm, name='email_change_confirm')
363                         )
364                 return urlpatterns
365         
366         def account_view(self, request, extra_context=None, token_generator=email_token_generator, *args, **kwargs):
367                 if request.method == 'POST':
368                         form = self.account_form(request.user, request.POST, request.FILES)
369                         
370                         if form.is_valid():
371                                 message = "Account information saved."
372                                 redirect = self.get_requirement_redirect(request, default='')
373                                 if 'email' in form.changed_data and self.email_change_confirmation_email:
374                                         # ModelForms modify their instances in-place during
375                                         # validation, so reset the instance's email to its
376                                         # previous value here, then remove the new value
377                                         # from cleaned_data. We only do this if an email
378                                         # change confirmation email is available.
379                                         request.user.email = form.initial['email']
380                                         
381                                         email = form.cleaned_data.pop('email')
382                                         
383                                         current_site = Site.objects.get_current()
384                                         
385                                         context = {
386                                                 'link': self.make_confirmation_link('email_change_confirm', token_generator, request.user, request.node, token_args=[email], reverse_kwargs={'email': email.replace('@', '+')}, secure=request.is_secure()),
387                                                 'user': request.user,
388                                                 'site': current_site,
389                                                 'request': request
390                                         }
391                                         self.send_confirmation_email('Confirm account email change at %s' % current_site.domain, email, self.email_change_confirmation_email, context)
392                                         
393                                         message = "An email has be sent to %s to confirm the email%s." % (email, bool(request.user.email) and " change" or "")
394                                         if not request.user.email:
395                                                 message += " You will need to confirm the email before accessing pages that require a valid account."
396                                                 redirect = ''
397                                 
398                                 form.save()
399                                 
400                                 if redirect != '':
401                                         message += " Here you go!"
402                                 
403                                 messages.add_message(request, messages.SUCCESS, message, fail_silently=True)
404                                 return HttpResponseRedirect(redirect)
405                 else:
406                         form = self.account_form(request.user)
407                 
408                 context = self.get_context()
409                 context.update(extra_context or {})
410                 context.update({
411                         'form': form
412                 })
413                 return self.manage_account_page.render_to_response(request, extra_context=context)
414         
415         def has_valid_account(self, user):
416                 form = self.account_form(user, {})
417                 form.data = form.initial
418                 return form.is_valid()
419         
420         def account_required(self, view):
421                 def inner(request, *args, **kwargs):
422                         if not self.has_valid_account(request.user):
423                                 messages.add_message(request, messages.ERROR, "You need to add some account information before you can access that page.", fail_silently=True)
424                                 if self.manage_account_page:
425                                         self.set_requirement_redirect(request, redirect=request.path)
426                                         redirect = self.reverse('account', node=request.node)
427                                 else:
428                                         redirect = node.get_absolute_url()
429                                 return HttpResponseRedirect(redirect)
430                         return view(request, *args, **kwargs)
431                 
432                 inner = self.login_required(inner)
433                 return inner
434         
435         def post_register_confirm_redirect(self, request):
436                 if self.manage_account_page:
437                         messages.add_message(request, messages.INFO, 'Welcome! Please fill in some more information.', fail_silently=True)
438                         return HttpResponseRedirect(self.reverse('account', node=request.node))
439                 return super(AccountMultiView, self).post_register_confirm_redirect(request)
440         
441         def email_change_confirm(self, request, extra_context=None, uidb36=None, token=None, email=None, token_generator=email_token_generator):
442                 """
443                 Checks that a given hash in an email change link is valid. If so, changes the email and redirects to the account page.
444                 """
445                 assert uidb36 is not None and token is not None and email is not None
446                 
447                 try:
448                         uid_int = base36_to_int(uidb36)
449                 except:
450                         raise Http404
451                 
452                 user = get_object_or_404(User, id=uid_int)
453                 
454                 email = '@'.join(email.rsplit('+', 1))
455                 
456                 if email == user.email:
457                         # Then short-circuit.
458                         raise Http404
459                 
460                 if token_generator.check_token(user, email, token):
461                         user.email = email
462                         user.save()
463                         messages.add_message(request, messages.SUCCESS, 'Email changed successfully.')
464                         if self.manage_account_page:
465                                 redirect = self.reverse('account', node=request.node)
466                         else:
467                                 redirect = request.node.get_absolute_url()
468                         return HttpResponseRedirect(redirect)
469                 
470                 raise Http404
471         
472         class Meta:
473                 abstract = True