views.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. # Copyright 2013 The Distro Tracker Developers
  2. # See the COPYRIGHT file at the top-level directory of this distribution and
  3. # at http://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 http://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. from __future__ import unicode_literals
  11. from django.conf import settings
  12. from django.utils.http import is_safe_url
  13. from django.contrib import messages
  14. from django.contrib.auth import authenticate
  15. from django.contrib.auth import login
  16. from django.contrib.auth import logout
  17. from django.utils.decorators import method_decorator
  18. from django.shortcuts import render
  19. from django.shortcuts import get_object_or_404
  20. from django.shortcuts import redirect
  21. from django.contrib.auth.decorators import login_required
  22. from django.views.generic.base import View
  23. from django.views.generic.edit import CreateView
  24. from django.views.generic.edit import UpdateView
  25. from django.views.generic.edit import FormView
  26. from django.views.generic import TemplateView
  27. from django.utils.http import urlencode
  28. from django.core.urlresolvers import reverse_lazy
  29. from django.template.loader import render_to_string
  30. from django_email_accounts.models import User
  31. from django_email_accounts.models import UserEmail
  32. from django.core.mail import send_mail
  33. from django.core.exceptions import PermissionDenied
  34. from django.contrib.auth.forms import PasswordChangeForm
  35. from django.http import Http404
  36. from django_email_accounts.forms import (
  37. AddEmailToAccountForm,
  38. UserCreationForm,
  39. ResetPasswordForm,
  40. ForgotPasswordForm,
  41. ChangePersonalInfoForm,
  42. AuthenticationForm,
  43. )
  44. from django_email_accounts.models import (
  45. MergeAccountConfirmation,
  46. UserRegistrationConfirmation,
  47. AddEmailConfirmation,
  48. ResetPasswordConfirmation,
  49. )
  50. from django_email_accounts import run_hook
  51. class LoginView(FormView):
  52. form_class = AuthenticationForm
  53. success_url = reverse_lazy('accounts-profile')
  54. template_name = 'accounts/login.html'
  55. def form_valid(self, form):
  56. if not is_safe_url(url=self.success_url, host=self.request.get_host()):
  57. self.success_url = '/'
  58. login(self.request, form.get_user())
  59. return redirect(self.success_url)
  60. class LogoutView(View):
  61. success_url = '/'
  62. redirect_parameter = 'next'
  63. def get(self, request):
  64. user = request.user
  65. logout(request)
  66. next_url = request.GET.get(self.redirect_parameter, self.success_url)
  67. redirect_url = run_hook('post-logout-redirect', request, user, next_url)
  68. if redirect_url:
  69. return redirect(redirect_url)
  70. else:
  71. return redirect(next_url if next_url else '/')
  72. class RegisterUser(CreateView):
  73. """
  74. Provides a view that displays a registration form on a GET request and
  75. registers the user on a POST.
  76. ``template_name`` and ``success_url`` properties can be overridden when
  77. instantiating the view in order to customize the page displayed on a GET
  78. request and the URL to which the user should be redirected after a
  79. successful POST, respectively.
  80. Additionally, by overriding the ``confirmation_email_template`` and
  81. ``confirmation_email_subject`` it is possible to customize the subject and
  82. content of a confirmation email sent to the user being registered.
  83. Instead of providing a ``confirmation_email_template`` you may also override
  84. the :meth:`get_confirmation_email_content` to provide a custom rendered
  85. text content.
  86. The sender of the email can be changed by modifying the
  87. ``confirmation_email_from_address`` setting.
  88. """
  89. template_name = 'accounts/register.html'
  90. model = User
  91. success_url = reverse_lazy('accounts-register-success')
  92. confirmation_email_template = 'accounts/registration-confirmation-email.txt'
  93. confirmation_email_subject = 'Registration Confirmation'
  94. confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL
  95. def get_confirmation_email_content(self, confirmation):
  96. return render_to_string(self.confirmation_email_template, {
  97. 'confirmation': confirmation,
  98. })
  99. def get_form_class(self):
  100. return UserCreationForm
  101. def form_valid(self, form):
  102. response = super(RegisterUser, self).form_valid(form)
  103. self.send_confirmation_mail(form.instance)
  104. return response
  105. def send_confirmation_mail(self, user):
  106. """
  107. Sends a confirmation email to the user. The user is inactive until the
  108. email is confirmed by clicking a URL found in the email.
  109. """
  110. confirmation = UserRegistrationConfirmation.objects.create_confirmation(
  111. user=user)
  112. send_mail(
  113. self.confirmation_email_subject,
  114. self.get_confirmation_email_content(confirmation),
  115. from_email=self.confirmation_email_from_address,
  116. recipient_list=[user.main_email])
  117. class LoginRequiredMixin(object):
  118. """
  119. A view mixin which makes sure that the user is logged in before accessing
  120. the view.
  121. """
  122. @method_decorator(login_required)
  123. def dispatch(self, *args, **kwargs):
  124. return super(LoginRequiredMixin, self).dispatch(*args, **kwargs)
  125. class MessageMixin(object):
  126. """
  127. A View mixin which adds a success info message to the list of messages
  128. managed by the :mod:`django.contrib.message` framework in case a form has
  129. been successfully processed.
  130. The message which is added is retrieved by calling the :meth:`get_message`
  131. method. Alternatively, a :attr:`message` attribute can be set if no
  132. calculations are necessary.
  133. """
  134. def form_valid(self, *args, **kwargs):
  135. message = self.get_message()
  136. if message:
  137. messages.info(self.request, message)
  138. return super(MessageMixin, self).form_valid(*args, **kwargs)
  139. def get_message(self):
  140. if self.message:
  141. return self.message
  142. class SetPasswordMixin(object):
  143. def form_valid(self, form):
  144. user = self.confirmation.user
  145. user.is_active = True
  146. password = form.cleaned_data['password1']
  147. user.set_password(password)
  148. user.save()
  149. # The confirmation key is no longer needed
  150. self.confirmation.delete()
  151. # Log the user in
  152. user = authenticate(username=user.main_email, password=password)
  153. login(self.request, user)
  154. return super(SetPasswordMixin, self).form_valid(form)
  155. def get_confirmation_instance(self, confirmation_key):
  156. self.confirmation = get_object_or_404(
  157. self.confirmation_class,
  158. confirmation_key=confirmation_key)
  159. return self.confirmation
  160. def post(self, request, confirmation_key):
  161. self.get_confirmation_instance(confirmation_key)
  162. return super(SetPasswordMixin, self).post(request, confirmation_key)
  163. def get(self, request, confirmation_key):
  164. self.get_confirmation_instance(confirmation_key)
  165. return super(SetPasswordMixin, self).get(request, confirmation_key)
  166. class RegistrationConfirmation(SetPasswordMixin, MessageMixin, FormView):
  167. form_class = ResetPasswordForm
  168. template_name = 'accounts/registration-confirmation.html'
  169. success_url = reverse_lazy('accounts-profile')
  170. message = 'You have successfully registered'
  171. confirmation_class = UserRegistrationConfirmation
  172. class ResetPasswordView(SetPasswordMixin, MessageMixin, FormView):
  173. form_class = ResetPasswordForm
  174. template_name = 'accounts/registration-reset-password.html'
  175. success_url = reverse_lazy('accounts-profile')
  176. message = 'You have successfully reset your password'
  177. confirmation_class = ResetPasswordConfirmation
  178. class ForgotPasswordView(FormView):
  179. form_class = ForgotPasswordForm
  180. success_url = reverse_lazy('accounts-password-reset-success')
  181. template_name = 'accounts/forgot-password.html'
  182. confirmation_email_template = \
  183. 'accounts/password-reset-confirmation-email.txt'
  184. confirmation_email_subject = 'Password Reset Confirmation'
  185. confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL
  186. def get_confirmation_email_content(self, confirmation):
  187. return render_to_string(self.confirmation_email_template, {
  188. 'confirmation': confirmation,
  189. })
  190. def form_valid(self, form):
  191. # Create a ResetPasswordConfirmation instance
  192. email = form.cleaned_data['email']
  193. user = User.objects.get(emails__email=email)
  194. confirmation = \
  195. ResetPasswordConfirmation.objects.create_confirmation(user=user)
  196. # Send a confirmation email
  197. send_mail(
  198. self.confirmation_email_subject,
  199. self.get_confirmation_email_content(confirmation),
  200. from_email=self.confirmation_email_from_address,
  201. recipient_list=[email])
  202. return super(ForgotPasswordView, self).form_valid(form)
  203. class ChangePersonalInfoView(LoginRequiredMixin, MessageMixin, UpdateView):
  204. template_name = 'accounts/change-personal-info.html'
  205. form_class = ChangePersonalInfoForm
  206. model = User
  207. success_url = reverse_lazy('accounts-profile-modify')
  208. message = 'Successfully changed your information'
  209. def get_object(self, queryset=None):
  210. return self.request.user
  211. class PasswordChangeView(LoginRequiredMixin, MessageMixin, FormView):
  212. template_name = 'accounts/password-update.html'
  213. form_class = PasswordChangeForm
  214. success_url = reverse_lazy('accounts-profile-password-change')
  215. message = 'Successfully updated your password'
  216. def get_form_kwargs(self):
  217. kwargs = super(PasswordChangeView, self).get_form_kwargs()
  218. kwargs['user'] = self.request.user
  219. return kwargs
  220. def form_valid(self, form, *args, **kwargs):
  221. form.save()
  222. return super(PasswordChangeView, self).form_valid(form, *args, **kwargs)
  223. class AccountProfile(LoginRequiredMixin, View):
  224. template_name = 'accounts/profile.html'
  225. def get(self, request):
  226. return render(request, self.template_name, {
  227. 'user': request.user,
  228. })
  229. class ManageAccountEmailsView(LoginRequiredMixin, MessageMixin, FormView):
  230. """
  231. Render a page letting users add or remove emails to their accounts.
  232. Apart from the ``success_url``, a ``merge_accounts_url`` can be provided,
  233. if the name of the view is to differ from ``accounts-merge-confirmation``
  234. """
  235. form_class = AddEmailToAccountForm
  236. template_name = 'accounts/profile-manage-emails.html'
  237. success_url = reverse_lazy('accounts-manage-emails')
  238. merge_accounts_url = reverse_lazy('accounts-merge-confirmation')
  239. confirmation_email_template = 'accounts/add-email-confirmation-email.txt'
  240. confirmation_email_subject = 'Add Email To Account'
  241. confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL
  242. def get_confirmation_email_content(self, confirmation):
  243. return render_to_string(self.confirmation_email_template, {
  244. 'confirmation': confirmation,
  245. })
  246. def form_valid(self, form):
  247. email = form.cleaned_data['email']
  248. user_email, _ = UserEmail.objects.get_or_create(email=email)
  249. if not user_email.user:
  250. # The email is not associated with an account yet.
  251. # Ask for confirmation to add it to this account.
  252. confirmation = AddEmailConfirmation.objects.create_confirmation(
  253. user=self.request.user,
  254. email=user_email)
  255. self.message = (
  256. 'Before the email is associated with this account, '
  257. 'you must follow the confirmation link sent to the address'
  258. )
  259. # Send a confirmation email
  260. send_mail(
  261. self.confirmation_email_subject,
  262. self.get_confirmation_email_content(confirmation),
  263. from_email=self.confirmation_email_from_address,
  264. recipient_list=[email])
  265. elif user_email.user == self.request.user:
  266. self.message = 'This email is already associated with your account.'
  267. else:
  268. # Offer the user to merge the two accounts
  269. return redirect(self.merge_accounts_url + '?' + urlencode({
  270. 'email': email,
  271. }))
  272. return super(ManageAccountEmailsView, self).form_valid(form)
  273. class AccountMergeConfirmView(LoginRequiredMixin, View):
  274. template_name = 'accounts/account-merge-confirm.html'
  275. success_url = reverse_lazy('accounts-merge-confirmed')
  276. confirmation_email_template = \
  277. 'accounts/merge-accounts-confirmation-email.txt'
  278. confirmation_email_subject = 'Merge Accounts'
  279. confirmation_email_from_address = settings.DEFAULT_FROM_EMAIL
  280. def get_confirmation_email_content(self, confirmation):
  281. return render_to_string(self.confirmation_email_template, {
  282. 'confirmation': confirmation,
  283. })
  284. def get_user_email(self, query_dict):
  285. if 'email' not in query_dict:
  286. raise Http404
  287. email = query_dict['email']
  288. user_email = get_object_or_404(UserEmail, email=email)
  289. return user_email
  290. def get(self, request):
  291. self.request = request
  292. user_email = self.get_user_email(self.request.GET)
  293. return render(request, self.template_name, {
  294. 'user_email': user_email,
  295. })
  296. def post(self, request):
  297. self.request = request
  298. user_email = self.get_user_email(self.request.POST)
  299. if not user_email.user or user_email.user == self.request.user:
  300. pass
  301. # Send a confirmation mail
  302. confirmation = MergeAccountConfirmation.objects.create_confirmation(
  303. initial_user=self.request.user,
  304. merge_with=user_email.user)
  305. send_mail(
  306. self.confirmation_email_subject,
  307. self.get_confirmation_email_content(confirmation),
  308. from_email=self.confirmation_email_from_address,
  309. recipient_list=[user_email.email])
  310. return redirect(self.success_url + '?' + urlencode({
  311. 'email': user_email.email,
  312. }))
  313. class AccountMergeFinalize(View):
  314. template_name = 'accounts/account-merge-finalize.html'
  315. success_url = reverse_lazy('accounts-merge-finalized')
  316. def get(self, request, confirmation_key):
  317. confirmation = get_object_or_404(
  318. MergeAccountConfirmation,
  319. confirmation_key=confirmation_key)
  320. return render(request, self.template_name, {
  321. 'confirmation': confirmation,
  322. })
  323. def post(self, request, confirmation_key):
  324. confirmation = get_object_or_404(
  325. MergeAccountConfirmation,
  326. confirmation_key=confirmation_key)
  327. initial_user = confirmation.initial_user
  328. merge_with = confirmation.merge_with
  329. # Move emails
  330. for email in merge_with.emails.all():
  331. initial_user.emails.add(email)
  332. # Run a post merge hook
  333. run_hook('post-merge', initial_user, merge_with)
  334. confirmation.delete()
  335. if request.user == confirmation.merge_with:
  336. logout(request)
  337. # The account is now obsolete and should be removed
  338. merge_with.delete()
  339. return redirect(self.success_url)
  340. class AccountMergeConfirmedView(TemplateView):
  341. template_name = 'accounts/accounts-merge-confirmed.html'
  342. def get_context_data(self, **kwargs):
  343. if 'email' not in self.request.GET:
  344. raise Http404
  345. email = self.request.GET['email']
  346. user_email = get_object_or_404(UserEmail, email=email)
  347. context = super(AccountMergeConfirmedView,
  348. self).get_context_data(**kwargs)
  349. context['email'] = user_email
  350. return context
  351. class ConfirmAddAccountEmail(View):
  352. template_name = 'accounts/new-email-added.html'
  353. def get(self, request, confirmation_key):
  354. confirmation = get_object_or_404(
  355. AddEmailConfirmation,
  356. confirmation_key=confirmation_key)
  357. user = confirmation.user
  358. user_email = confirmation.email
  359. confirmation.delete()
  360. # If the email has become associated with a different user in the mean
  361. # time, abort the operation.
  362. if user_email.user and user_email.user != user:
  363. raise PermissionDenied
  364. user_email.user = user
  365. user_email.save()
  366. return render(request, self.template_name, {
  367. 'new_email': user_email,
  368. })