1# Copyright 2013 The Distro Tracker Developers
2# See the COPYRIGHT file at the top-level directory of this distribution and
3# at https://deb.li/DTAuthors
4#
5# This file is part of Distro Tracker. It is subject to the license terms
6# in the LICENSE file found in the top-level directory of this
7# distribution and at https://deb.li/DTLicense. No part of Distro Tracker,
8# including this file, may be copied, modified, propagated, or distributed
9# except according to the terms contained in the LICENSE file.
10"""Django views for django_email_accounts."""
12from django.conf import settings
13from django.contrib import messages
14from django.contrib.auth import authenticate, login, logout
15from django.contrib.auth.decorators import login_required
16from django.contrib.auth.forms import PasswordChangeForm
17from django.core.exceptions import PermissionDenied
18from django.core.mail import send_mail
19from django.http import Http404
20from django.shortcuts import get_object_or_404, redirect, render
21from django.template.loader import render_to_string
22from django.urls import reverse_lazy
23from django.utils.decorators import method_decorator
24from django.utils.http import url_has_allowed_host_and_scheme, urlencode
25from django.views.generic import TemplateView
26from django.views.generic.base import View
27from django.views.generic.edit import CreateView, FormView, UpdateView
29from django_email_accounts import run_hook
30from django_email_accounts.forms import (
31 AddEmailToAccountForm,
32 AuthenticationForm,
33 ChangePersonalInfoForm,
34 ForgotPasswordForm,
35 ResetPasswordForm,
36 UserCreationForm
37)
38from django_email_accounts.models import (
39 AddEmailConfirmation,
40 MergeAccountConfirmation,
41 ResetPasswordConfirmation,
42 User,
43 UserEmail,
44 UserRegistrationConfirmation
45)
48class LoginView(FormView):
49 form_class = AuthenticationForm
50 success_url = reverse_lazy('accounts-profile')
51 template_name = 'accounts/login.html'
52 redirect_parameter = 'next'
54 def get(self, request, *args, **kwargs):
55 """
56 Handles GET requests and instantiates a blank version of the form
57 when a user is not authenticated.
59 Override default FormView behavior to redirect to profile
60 if user is already authenticated.
61 """
62 if self.request.user.is_authenticated:
63 return redirect(self.success_url)
64 else:
65 return super().get(request, *args, **kwargs)
67 def form_valid(self, form):
68 redirect_url = self.request.GET.get(
69 self.redirect_parameter, self.success_url)
70 if not url_has_allowed_host_and_scheme( 70 ↛ 74line 70 didn't jump to line 74, because the condition on line 70 was never true
71 redirect_url,
72 allowed_hosts=set(self.request.get_host())
73 ):
74 redirect_url = self.success_url
76 login(self.request, form.get_user())
78 return redirect(redirect_url)
81class LogoutView(View):
82 success_url = '/'
83 redirect_parameter = 'next'
85 def get(self, request):
86 user = request.user
87 logout(request)
89 next_url = request.GET.get(self.redirect_parameter, self.success_url)
90 if not url_has_allowed_host_and_scheme(
91 next_url,
92 allowed_hosts=set(self.request.get_host())
93 ):
94 next_url = self.success_url
96 redirect_url = run_hook('post-logout-redirect', request, user, next_url)
97 if redirect_url:
98 return redirect(redirect_url)
99 else:
100 return redirect(next_url if next_url else '/')
103class RegisterUser(CreateView):
104 """
105 Provides a view that displays a registration form on a GET request and
106 registers the user on a POST.
108 ``template_name`` and ``success_url`` properties can be overridden when
109 instantiating the view in order to customize the page displayed on a GET
110 request and the URL to which the user should be redirected after a
111 successful POST, respectively.
113 Additionally, by overriding the ``confirmation_email_template`` and
114 ``confirmation_email_subject`` it is possible to customize the subject and
115 content of a confirmation email sent to the user being registered.
117 Instead of providing a ``confirmation_email_template`` you may also override
118 the :meth:`get_confirmation_email_content` to provide a custom rendered
119 text content.
121 The sender of the email can be changed by modifying the
122 ``confirmation_email_from_address`` setting.
123 """
124 template_name = 'accounts/register.html'
125 model = User
126 success_url = reverse_lazy('accounts-register-success')
128 confirmation_email_template = 'accounts/registration-confirmation-email.txt'
129 confirmation_email_subject = 'Registration Confirmation'
130 confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL
132 def get_confirmation_email_content(self, confirmation):
133 return render_to_string(self.confirmation_email_template, {
134 'confirmation': confirmation,
135 })
137 def get_form_class(self):
138 return UserCreationForm
140 def form_valid(self, form):
141 response = super(RegisterUser, self).form_valid(form)
142 self.send_confirmation_mail(form.instance)
144 return response
146 def send_confirmation_mail(self, user):
147 """
148 Sends a confirmation email to the user. The user is inactive until the
149 email is confirmed by clicking a URL found in the email.
150 """
151 confirmation = UserRegistrationConfirmation.objects.create_confirmation(
152 user=user)
154 send_mail(
155 self.confirmation_email_subject,
156 self.get_confirmation_email_content(confirmation),
157 from_email=self.confirmation_email_from_address,
158 recipient_list=[user.main_email])
161class LoginRequiredMixin(object):
162 """
163 A view mixin which makes sure that the user is logged in before accessing
164 the view.
165 """
166 @method_decorator(login_required)
167 def dispatch(self, *args, **kwargs):
168 return super(LoginRequiredMixin, self).dispatch(*args, **kwargs)
171class MessageMixin(object):
172 """
173 A View mixin which adds a success info message to the list of messages
174 managed by the :mod:`django.contrib.message` framework in case a form has
175 been successfully processed.
177 The message which is added is retrieved by calling the :meth:`get_message`
178 method. Alternatively, a :attr:`message` attribute can be set if no
179 calculations are necessary.
180 """
181 def form_valid(self, *args, **kwargs):
182 message = self.get_message()
183 if message:
184 messages.info(self.request, message)
185 return super(MessageMixin, self).form_valid(*args, **kwargs)
187 def get_message(self):
188 if self.message:
189 return self.message
192class SetPasswordMixin(object):
193 def form_valid(self, form):
194 user = self.confirmation.user
195 user.is_active = True
196 password = form.cleaned_data['password1']
197 user.set_password(password)
198 user.save()
200 # The confirmation key is no longer needed
201 self.confirmation.delete()
203 # Log the user in
204 user = authenticate(username=user.main_email, password=password)
205 login(self.request, user)
207 return super(SetPasswordMixin, self).form_valid(form)
209 def get_confirmation_instance(self, confirmation_key):
210 self.confirmation = get_object_or_404(
211 self.confirmation_class,
212 confirmation_key=confirmation_key)
213 return self.confirmation
215 def post(self, request, confirmation_key):
216 self.get_confirmation_instance(confirmation_key)
217 return super(SetPasswordMixin, self).post(request, confirmation_key)
219 def get(self, request, confirmation_key):
220 self.get_confirmation_instance(confirmation_key)
221 return super(SetPasswordMixin, self).get(request, confirmation_key)
224class RegistrationConfirmation(SetPasswordMixin, MessageMixin, FormView):
225 form_class = ResetPasswordForm
226 template_name = 'accounts/registration-confirmation.html'
227 success_url = reverse_lazy('accounts-profile')
228 message = 'You have successfully registered'
229 confirmation_class = UserRegistrationConfirmation
232class ResetPasswordView(SetPasswordMixin, MessageMixin, FormView):
233 form_class = ResetPasswordForm
234 template_name = 'accounts/registration-reset-password.html'
235 success_url = reverse_lazy('accounts-profile')
236 message = 'You have successfully reset your password'
237 confirmation_class = ResetPasswordConfirmation
240class ForgotPasswordView(FormView):
241 form_class = ForgotPasswordForm
242 success_url = reverse_lazy('accounts-password-reset-success')
243 template_name = 'accounts/forgot-password.html'
245 confirmation_email_template = \
246 'accounts/password-reset-confirmation-email.txt'
247 confirmation_email_subject = 'Password Reset Confirmation'
248 confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL
250 def get_confirmation_email_content(self, confirmation):
251 return render_to_string(self.confirmation_email_template, {
252 'confirmation': confirmation,
253 })
255 def form_valid(self, form):
256 # Create a ResetPasswordConfirmation instance
257 email = form.cleaned_data['email']
258 user = User.objects.get(emails__email=email)
259 confirmation = \
260 ResetPasswordConfirmation.objects.create_confirmation(user=user)
262 # Send a confirmation email
263 send_mail(
264 self.confirmation_email_subject,
265 self.get_confirmation_email_content(confirmation),
266 from_email=self.confirmation_email_from_address,
267 recipient_list=[email])
269 return super(ForgotPasswordView, self).form_valid(form)
272class ChangePersonalInfoView(LoginRequiredMixin, MessageMixin, UpdateView):
273 template_name = 'accounts/change-personal-info.html'
274 form_class = ChangePersonalInfoForm
275 model = User
276 success_url = reverse_lazy('accounts-profile-modify')
277 message = 'Successfully changed your information'
279 def get_object(self, queryset=None):
280 return self.request.user
283class PasswordChangeView(LoginRequiredMixin, MessageMixin, FormView):
284 template_name = 'accounts/password-update.html'
285 form_class = PasswordChangeForm
286 success_url = reverse_lazy('accounts-profile-password-change')
287 message = 'Successfully updated your password'
289 def get_form_kwargs(self):
290 kwargs = super(PasswordChangeView, self).get_form_kwargs()
291 kwargs['user'] = self.request.user
292 return kwargs
294 def form_valid(self, form, *args, **kwargs):
295 form.save()
296 return super(PasswordChangeView, self).form_valid(form, *args, **kwargs)
299class AccountProfile(LoginRequiredMixin, View):
300 template_name = 'accounts/profile.html'
302 def get(self, request):
303 return render(request, self.template_name, {
304 'user': request.user,
305 })
308class ManageAccountEmailsView(LoginRequiredMixin, MessageMixin, FormView):
309 """
310 Render a page letting users add or remove emails to their accounts.
312 Apart from the ``success_url``, a ``merge_accounts_url`` can be provided,
313 if the name of the view is to differ from ``accounts-merge-confirmation``
314 """
315 form_class = AddEmailToAccountForm
316 template_name = 'accounts/profile-manage-emails.html'
317 success_url = reverse_lazy('accounts-manage-emails')
318 merge_accounts_url = reverse_lazy('accounts-merge-confirmation')
320 confirmation_email_template = 'accounts/add-email-confirmation-email.txt'
321 confirmation_email_subject = 'Add Email To Account'
322 confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL
324 def get_confirmation_email_content(self, confirmation):
325 return render_to_string(self.confirmation_email_template, {
326 'confirmation': confirmation,
327 })
329 def form_valid(self, form):
330 email = form.cleaned_data['email']
331 user_email, _ = UserEmail.objects.get_or_create(
332 email__iexact=email,
333 defaults={'email': email}
334 )
335 if not user_email.user:
336 # The email is not associated with an account yet.
337 # Ask for confirmation to add it to this account.
338 confirmation = AddEmailConfirmation.objects.create_confirmation(
339 user=self.request.user,
340 email=user_email)
341 self.message = (
342 'Before the email is associated with this account, '
343 'you must follow the confirmation link sent to the address'
344 )
345 # Send a confirmation email
346 send_mail(
347 self.confirmation_email_subject,
348 self.get_confirmation_email_content(confirmation),
349 from_email=self.confirmation_email_from_address,
350 recipient_list=[email])
351 elif user_email.user == self.request.user:
352 self.message = 'This email is already associated with your account.'
353 else:
354 # Offer the user to merge the two accounts
355 return redirect(self.merge_accounts_url + '?' + urlencode({
356 'email': email,
357 }))
359 return super(ManageAccountEmailsView, self).form_valid(form)
362class AccountMergeConfirmView(LoginRequiredMixin, View):
363 template_name = 'accounts/account-merge-confirm.html'
364 success_url = reverse_lazy('accounts-merge-confirmed')
366 confirmation_email_template = \
367 'accounts/merge-accounts-confirmation-email.txt'
368 confirmation_email_subject = 'Merge Accounts'
369 confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL
371 def get_confirmation_email_content(self, confirmation):
372 return render_to_string(self.confirmation_email_template, {
373 'confirmation': confirmation,
374 })
376 def get_user_email(self, query_dict):
377 if 'email' not in query_dict:
378 raise Http404
379 email = query_dict['email']
380 user_email = get_object_or_404(UserEmail, email__iexact=email)
381 return user_email
383 def get(self, request):
384 self.request = request
385 user_email = self.get_user_email(self.request.GET)
387 return render(request, self.template_name, {
388 'user_email': user_email,
389 })
391 def post(self, request):
392 self.request = request
394 user_email = self.get_user_email(self.request.POST)
395 if not user_email.user or user_email.user == self.request.user:
396 pass
398 # Send a confirmation mail
399 confirmation = MergeAccountConfirmation.objects.create_confirmation(
400 initial_user=self.request.user,
401 merge_with=user_email.user)
402 send_mail(
403 self.confirmation_email_subject,
404 self.get_confirmation_email_content(confirmation),
405 from_email=self.confirmation_email_from_address,
406 recipient_list=[user_email.email])
408 return redirect(self.success_url + '?' + urlencode({
409 'email': user_email.email,
410 }))
413class AccountMergeFinalize(View):
414 template_name = 'accounts/account-merge-finalize.html'
415 success_url = reverse_lazy('accounts-merge-finalized')
417 def get(self, request, confirmation_key):
418 confirmation = get_object_or_404(
419 MergeAccountConfirmation,
420 confirmation_key=confirmation_key)
422 return render(request, self.template_name, {
423 'confirmation': confirmation,
424 })
426 def post(self, request, confirmation_key):
427 confirmation = get_object_or_404(
428 MergeAccountConfirmation,
429 confirmation_key=confirmation_key)
431 initial_user = confirmation.initial_user
432 merge_with = confirmation.merge_with
434 # Move emails
435 for email in merge_with.emails.all():
436 initial_user.emails.add(email)
438 # Run a post merge hook
439 run_hook('post-merge', initial_user, merge_with)
441 confirmation.delete()
443 if request.user == confirmation.merge_with:
444 logout(request)
446 # The account is now obsolete and should be removed
447 merge_with.delete()
449 return redirect(self.success_url)
452class AccountMergeConfirmedView(TemplateView):
453 template_name = 'accounts/accounts-merge-confirmed.html'
455 def get_context_data(self, **kwargs):
456 if 'email' not in self.request.GET:
457 raise Http404
458 email = self.request.GET['email']
459 user_email = get_object_or_404(UserEmail, email__iexact=email)
460 context = super(AccountMergeConfirmedView,
461 self).get_context_data(**kwargs)
462 context['email'] = user_email
464 return context
467class ConfirmAddAccountEmail(View):
468 template_name = 'accounts/new-email-added.html'
470 def get(self, request, confirmation_key):
471 confirmation = get_object_or_404(
472 AddEmailConfirmation,
473 confirmation_key=confirmation_key)
474 user = confirmation.user
475 user_email = confirmation.email
476 confirmation.delete()
477 # If the email has become associated with a different user in the mean
478 # time, abort the operation.
479 if user_email.user and user_email.user != user:
480 raise PermissionDenied
481 user_email.user = user
482 user_email.save()
484 return render(request, self.template_name, {
485 'new_email': user_email,
486 })