WIP: Implementation of pended user creation using tokens. Needs some testing.
[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
13 from django.utils.http import int_to_base36
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/$', 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/$', 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         @csrf_protect
158         def password_reset(self, request, node=None, extra_context=None):
159                 pass
160         
161         @csrf_protect
162         def register(self, request, node=None, extra_context=None, token_generator=default_token_generator):
163                 if request.method == POST:
164                         form = RegistrationForm(request.POST)
165                         if form.is_valid():
166                                 user = form.save()
167                                 current_site = Site.objects.get_current()
168                                 token = default_token_generator.make_token(user)
169                                 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('/'))
170                                 context = {
171                                         'link': link
172                                 }
173                                 self.send_confirmation_email('Confirm account creation at %s' % current_site.name, user.email, self.register_confirmation_email, context)
174                                 messages.add_message(request, messages.SUCCESS, 'An email has been sent to %s with details on activating your account.' % user.email)
175                                 return HttpResponseRedirect('')
176                 else:
177                         form = RegistrationForm()
178                 
179                 context = self.get_context({'form': form})
180                 context.update(extra_context or {})
181                 return self.register_page.render_to_response(request, node, context)
182         
183         class Meta:
184                 abstract = True
185
186
187 class AccountMultiView(LoginMultiView):
188         """
189         Subclasses may define an account_profile model, fields from the User model
190         to include in the account, and fields from the account profile to use in
191         the account.
192         """
193         manage_account_page = models.ForeignKey(Page, related_name='%(app_label)s_%(class)s_manage_account_page')
194         user_fields = ['first_name', 'last_name', 'email']
195         required_user_fields = user_fields
196         account_profile = None
197         account_profile_fields = None
198         
199         @property
200         def urlpatterns(self):
201                 urlpatterns = super(AccountMultiView, self).urlpatterns
202                 urlpatterns += patterns('',
203                         url(r'^account/$', self.login_required(self.account_view), name='account')
204                 )
205                 return urlpatterns
206         
207         def get_account_forms(self):
208                 user_form = forms.models.modelform_factory(User, fields=self.user_fields)
209                 
210                 if self.account_profile is None:
211                         profile_form = None
212                 else:
213                         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'])
214                 
215                 for field_name, field in user_form.base_fields.items():
216                         if field_name in self.required_user_fields:
217                                 field.required = True
218                 return user_form, profile_form
219         
220         def get_account_form_instances(self, user, data=None):
221                 form_instances = []
222                 user_form, profile_form = self.get_account_forms()
223                 if data is None:
224                         form_instances.append(user_form(instance=user))
225                         if profile_form:
226                                 form_instances.append(profile_form(instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
227                 else:
228                         form_instances.append(user_form(data, instance=user))
229                         if profile_form:
230                                 form_instances.append(profile_form(data, instance=self.account_profile._default_manager.get_or_create(user=user)[0]))
231                 
232                 return form_instances
233         
234         def account_view(self, request, node=None, extra_context=None):
235                 if request.method == 'POST':
236                         form_instances = self.get_account_form_instances(request.user, request.POST)
237                         
238                         for form in form_instances:
239                                 if not form.is_valid():
240                                         break
241                         else:
242                                 for form in form_instances:
243                                         form.save()
244                                 messages.add_message(request, messages.SUCCESS, "Account information saved.", fail_silently=True)
245                                 return HttpResponseRedirect('')
246                 else:
247                         form_instances = self.get_account_form_instances(request.user)
248                 
249                 context = self.get_context({
250                         'forms': form_instances
251                 })
252                 context.update(extra_context or {})
253                 return self.manage_account_page.render_to_response(node, request, extra_context=context)
254         
255         def has_valid_account(self, user):
256                 user_form, profile_form = self.get_account_forms()
257                 forms = []
258                 forms.append(user_form(data=get_field_data(user, self.user_fields)))
259                 
260                 if profile_form is not None:
261                         profile = self.account_profile._default_manager.get_or_create(user=user)[0]
262                         forms.append(profile_form(data=get_field_data(profile, self.account_profile_fields)))
263                 
264                 for form in forms:
265                         if not form.is_valid():
266                                 return False
267                 return True
268         
269         def account_required(self, view):
270                 def inner(request, *args, **kwargs):
271                         if not self.has_valid_account(request.user):
272                                 messages.add_message(request, messages.ERROR, "You need to add some account information before you can post listings.")
273                                 return self.account_view(request, *args, **kwargs)
274                         return view(request, *args, **kwargs)
275                 
276                 inner = self.login_required(inner)
277                 return inner
278         
279         class Meta:
280                 abstract = True