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.""" 

11 

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 

28 

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) 

46 

47 

48class LoginView(FormView): 

49 form_class = AuthenticationForm 

50 success_url = reverse_lazy('accounts-profile') 

51 template_name = 'accounts/login.html' 

52 redirect_parameter = 'next' 

53 

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. 

58 

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) 

66 

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 

75 

76 login(self.request, form.get_user()) 

77 

78 return redirect(redirect_url) 

79 

80 

81class LogoutView(View): 

82 success_url = '/' 

83 redirect_parameter = 'next' 

84 

85 def get(self, request): 

86 user = request.user 

87 logout(request) 

88 

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 

95 

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 '/') 

101 

102 

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. 

107 

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. 

112 

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. 

116 

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. 

120 

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') 

127 

128 confirmation_email_template = 'accounts/registration-confirmation-email.txt' 

129 confirmation_email_subject = 'Registration Confirmation' 

130 confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL 

131 

132 def get_confirmation_email_content(self, confirmation): 

133 return render_to_string(self.confirmation_email_template, { 

134 'confirmation': confirmation, 

135 }) 

136 

137 def get_form_class(self): 

138 return UserCreationForm 

139 

140 def form_valid(self, form): 

141 response = super(RegisterUser, self).form_valid(form) 

142 self.send_confirmation_mail(form.instance) 

143 

144 return response 

145 

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) 

153 

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]) 

159 

160 

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) 

169 

170 

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. 

176 

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) 

186 

187 def get_message(self): 

188 if self.message: 

189 return self.message 

190 

191 

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() 

199 

200 # The confirmation key is no longer needed 

201 self.confirmation.delete() 

202 

203 # Log the user in 

204 user = authenticate(username=user.main_email, password=password) 

205 login(self.request, user) 

206 

207 return super(SetPasswordMixin, self).form_valid(form) 

208 

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 

214 

215 def post(self, request, confirmation_key): 

216 self.get_confirmation_instance(confirmation_key) 

217 return super(SetPasswordMixin, self).post(request, confirmation_key) 

218 

219 def get(self, request, confirmation_key): 

220 self.get_confirmation_instance(confirmation_key) 

221 return super(SetPasswordMixin, self).get(request, confirmation_key) 

222 

223 

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 

230 

231 

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 

238 

239 

240class ForgotPasswordView(FormView): 

241 form_class = ForgotPasswordForm 

242 success_url = reverse_lazy('accounts-password-reset-success') 

243 template_name = 'accounts/forgot-password.html' 

244 

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 

249 

250 def get_confirmation_email_content(self, confirmation): 

251 return render_to_string(self.confirmation_email_template, { 

252 'confirmation': confirmation, 

253 }) 

254 

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) 

261 

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]) 

268 

269 return super(ForgotPasswordView, self).form_valid(form) 

270 

271 

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' 

278 

279 def get_object(self, queryset=None): 

280 return self.request.user 

281 

282 

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' 

288 

289 def get_form_kwargs(self): 

290 kwargs = super(PasswordChangeView, self).get_form_kwargs() 

291 kwargs['user'] = self.request.user 

292 return kwargs 

293 

294 def form_valid(self, form, *args, **kwargs): 

295 form.save() 

296 return super(PasswordChangeView, self).form_valid(form, *args, **kwargs) 

297 

298 

299class AccountProfile(LoginRequiredMixin, View): 

300 template_name = 'accounts/profile.html' 

301 

302 def get(self, request): 

303 return render(request, self.template_name, { 

304 'user': request.user, 

305 }) 

306 

307 

308class ManageAccountEmailsView(LoginRequiredMixin, MessageMixin, FormView): 

309 """ 

310 Render a page letting users add or remove emails to their accounts. 

311 

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') 

319 

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 

323 

324 def get_confirmation_email_content(self, confirmation): 

325 return render_to_string(self.confirmation_email_template, { 

326 'confirmation': confirmation, 

327 }) 

328 

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 })) 

358 

359 return super(ManageAccountEmailsView, self).form_valid(form) 

360 

361 

362class AccountMergeConfirmView(LoginRequiredMixin, View): 

363 template_name = 'accounts/account-merge-confirm.html' 

364 success_url = reverse_lazy('accounts-merge-confirmed') 

365 

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 

370 

371 def get_confirmation_email_content(self, confirmation): 

372 return render_to_string(self.confirmation_email_template, { 

373 'confirmation': confirmation, 

374 }) 

375 

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 

382 

383 def get(self, request): 

384 self.request = request 

385 user_email = self.get_user_email(self.request.GET) 

386 

387 return render(request, self.template_name, { 

388 'user_email': user_email, 

389 }) 

390 

391 def post(self, request): 

392 self.request = request 

393 

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 

397 

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]) 

407 

408 return redirect(self.success_url + '?' + urlencode({ 

409 'email': user_email.email, 

410 })) 

411 

412 

413class AccountMergeFinalize(View): 

414 template_name = 'accounts/account-merge-finalize.html' 

415 success_url = reverse_lazy('accounts-merge-finalized') 

416 

417 def get(self, request, confirmation_key): 

418 confirmation = get_object_or_404( 

419 MergeAccountConfirmation, 

420 confirmation_key=confirmation_key) 

421 

422 return render(request, self.template_name, { 

423 'confirmation': confirmation, 

424 }) 

425 

426 def post(self, request, confirmation_key): 

427 confirmation = get_object_or_404( 

428 MergeAccountConfirmation, 

429 confirmation_key=confirmation_key) 

430 

431 initial_user = confirmation.initial_user 

432 merge_with = confirmation.merge_with 

433 

434 # Move emails 

435 for email in merge_with.emails.all(): 

436 initial_user.emails.add(email) 

437 

438 # Run a post merge hook 

439 run_hook('post-merge', initial_user, merge_with) 

440 

441 confirmation.delete() 

442 

443 if request.user == confirmation.merge_with: 

444 logout(request) 

445 

446 # The account is now obsolete and should be removed 

447 merge_with.delete() 

448 

449 return redirect(self.success_url) 

450 

451 

452class AccountMergeConfirmedView(TemplateView): 

453 template_name = 'accounts/accounts-merge-confirmed.html' 

454 

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 

463 

464 return context 

465 

466 

467class ConfirmAddAccountEmail(View): 

468 template_name = 'accounts/new-email-added.html' 

469 

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() 

483 

484 return render(request, self.template_name, { 

485 'new_email': user_email, 

486 })