1# Copyright 2013-2015 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"""Views for the :mod:`distro_tracker.accounts` app."""
11from django.conf import settings
12from django.core.exceptions import ValidationError
13from django.db.models import Prefetch
14from django.http import (
15 Http404,
16 HttpResponseBadRequest,
17 HttpResponseForbidden
18)
19from django.shortcuts import get_object_or_404, render, resolve_url
20from django.urls import reverse_lazy
21from django.utils.html import format_html
22from django.views.generic.base import View
24from distro_tracker.accounts.models import UserEmail
25from distro_tracker.core.models import (
26 EmailSettings,
27 Keyword,
28 Subscription,
29 get_web_package
30)
31from distro_tracker.core.utils import (
32 distro_tracker_render_to_string,
33 render_to_json_response
34)
35from distro_tracker.core.utils.http import safe_redirect
37from django_email_accounts import views as email_accounts_views
38from django_email_accounts.views import LoginRequiredMixin
41class ConfirmationRenderMixin(object):
42 def get_confirmation_email_content(self, confirmation):
43 return distro_tracker_render_to_string(
44 self.confirmation_email_template,
45 {'confirmation': confirmation}
46 )
49class LoginView(email_accounts_views.LoginView):
50 success_url = reverse_lazy('dtracker-accounts-profile')
53class LogoutView(email_accounts_views.LogoutView):
54 success_url = reverse_lazy('dtracker-index')
57class RegisterUser(ConfirmationRenderMixin, email_accounts_views.RegisterUser):
58 success_url = reverse_lazy('dtracker-accounts-register-success')
60 confirmation_email_subject = '{name} Registration Confirmation'.format(
61 name=settings.GET_INSTANCE_NAME())
62 confirmation_email_from_address = settings.DISTRO_TRACKER_NOREPLY_EMAIL
65class RegistrationConfirmation(email_accounts_views.RegistrationConfirmation):
66 success_url = reverse_lazy('dtracker-accounts-profile')
67 message = 'You have successfully registered to the {name}'.format(
68 name=settings.GET_INSTANCE_NAME())
71class ResetPasswordView(ConfirmationRenderMixin,
72 email_accounts_views.ResetPasswordView):
73 success_url = reverse_lazy('dtracker-accounts-profile')
76class ForgotPasswordView(ConfirmationRenderMixin,
77 email_accounts_views.ForgotPasswordView):
78 success_url = reverse_lazy('dtracker-accounts-password-reset-success')
79 confirmation_email_subject = '{name} Password Reset Confirmation'.format(
80 name=settings.GET_INSTANCE_NAME())
81 confirmation_email_from_address = settings.DISTRO_TRACKER_NOREPLY_EMAIL
84class ChangePersonalInfoView(email_accounts_views.ChangePersonalInfoView):
85 success_url = reverse_lazy('dtracker-accounts-profile-modify')
88class PasswordChangeView(email_accounts_views.PasswordChangeView):
89 success_url = reverse_lazy('dtracker-accounts-profile-password-change')
92class AccountProfile(email_accounts_views.AccountProfile):
93 pass
96class ManageAccountEmailsView(ConfirmationRenderMixin,
97 email_accounts_views.ManageAccountEmailsView):
98 success_url = reverse_lazy('dtracker-accounts-manage-emails')
99 merge_accounts_url = reverse_lazy('dtracker-accounts-merge-confirmation')
101 confirmation_email_subject = 'Add Email To {name} Account'.format(
102 name=settings.GET_INSTANCE_NAME())
103 confirmation_email_from_address = settings.DISTRO_TRACKER_NOREPLY_EMAIL
106class AccountMergeConfirmView(ConfirmationRenderMixin,
107 email_accounts_views.AccountMergeConfirmView):
108 success_url = reverse_lazy('dtracker-accounts-merge-confirmed')
109 confirmation_email_subject = 'Merge {name} Accounts'.format(
110 name=settings.GET_INSTANCE_NAME())
111 confirmation_email_from_address = settings.DISTRO_TRACKER_NOREPLY_EMAIL
114class AccountMergeFinalize(email_accounts_views.AccountMergeFinalize):
115 success_url = reverse_lazy('dtracker-accounts-merge-finalized')
118class AccountMergeConfirmedView(email_accounts_views.AccountMergeConfirmedView):
119 template_name = 'accounts/tracker-accounts-merge-confirmed.html'
122class ConfirmAddAccountEmail(email_accounts_views.ConfirmAddAccountEmail):
123 pass
126class SubscriptionsView(LoginRequiredMixin, View):
127 """
128 Displays a user's subscriptions.
130 This includes both direct package subscriptions and team memberships.
131 """
132 template_name = 'accounts/subscriptions.html'
134 def get(self, request):
135 user = request.user
136 keyword_qs = Keyword.objects.order_by('name')
137 # Ensure we have EmailSettings for all emails
138 for user_email in UserEmail.objects.filter(user=user):
139 EmailSettings.objects.get_or_create(user_email=user_email)
140 user_emails = UserEmail.objects.filter(user=user).order_by(
141 'email'
142 ).prefetch_related(
143 Prefetch(
144 'emailsettings__subscription_set___keywords',
145 queryset=keyword_qs
146 ),
147 Prefetch(
148 'emailsettings__default_keywords',
149 queryset=keyword_qs
150 )
151 )
152 # Map users emails to the subscriptions of that email
153 subscriptions = [
154 {
155 'email': user_email,
156 'subscriptions': sorted([
157 subscription for subscription
158 in user_email.emailsettings.subscription_set.all()
159 ], key=lambda sub: sub.package.name),
160 'team_memberships': sorted([
161 membership for membership in user_email.membership_set.all()
162 ], key=lambda m: m.team.name)
163 }
164 for user_email in user_emails
165 ]
166 # Initializing session variable if not set.
167 request.session.setdefault('selected_emails', [str(user_emails[0])])
168 return render(request, self.template_name, {
169 'subscriptions': subscriptions,
170 'selected_emails': request.session['selected_emails']
171 })
174class UserEmailsView(LoginRequiredMixin, View):
175 """
176 Returns a JSON encoded list of the currently logged in user's emails.
177 """
178 def get(self, request):
179 user = request.user
180 return render_to_json_response([
181 email.email for email in user.emails.all()
182 ])
185class SubscribeUserToPackageView(LoginRequiredMixin, View):
186 """
187 Subscribes the user to a package.
189 The user whose email address is provided must currently be logged in.
190 """
191 def post(self, request):
192 package = request.POST.get('package', None)
193 emails = request.POST.getlist('email', None)
195 if not package or not emails: 195 ↛ 196line 195 didn't jump to line 196, because the condition on line 195 was never true
196 raise Http404
198 # Remember selected emails via session variable
199 request.session['selected_emails'] = emails
201 # Check whether the logged in user is associated with the given emails
202 users_emails = [e.email for e in request.user.emails.all()]
203 for email in emails:
204 if email not in users_emails:
205 return HttpResponseForbidden()
207 _pkg = get_web_package(package)
208 _err = None
210 if _pkg:
211 try:
212 for email in emails:
213 Subscription.objects.create_for(
214 package_name=package,
215 email=email)
216 except ValidationError as e:
217 _err = e.message
218 else:
219 _err = format_html(
220 "Package {pkg} does not exist.",
221 pkg=package,
222 )
224 if request.headers.get('accept') == 'application/json':
225 json_result = {'status': 'ok'}
226 if _err is not None:
227 json_result = {
228 'status': 'failed',
229 'error': _err,
230 }
231 return render_to_json_response(json_result)
232 else:
233 if _err: 233 ↛ 235line 233 didn't jump to line 235, because the condition on line 233 was never false
234 return HttpResponseBadRequest(_err)
235 _next = request.POST.get('next', None)
236 return safe_redirect(
237 _next,
238 resolve_url('dtracker-package-page', package_name=package),
239 )
242class UnsubscribeUserView(LoginRequiredMixin, View):
243 """
244 Unsubscribes the currently logged in user from the given package.
245 An email can be optionally provided in which case only the given email is
246 unsubscribed from the package, if the logged in user owns it.
247 """
248 def post(self, request):
249 if 'package' not in request.POST:
250 raise Http404
252 package = request.POST['package']
253 user = request.user
255 if 'email' not in request.POST:
256 # Unsubscribe all the user's emails from the package
257 user_emails = UserEmail.objects.filter(user=user)
258 qs = Subscription.objects.filter(
259 email_settings__user_email__in=user_emails,
260 package__name=package)
261 else:
262 # Unsubscribe only the given email from the package
263 qs = Subscription.objects.filter(
264 email_settings__user_email__email=request.POST['email'],
265 package__name=package)
267 qs.delete()
269 if request.headers.get('accept') == 'application/json': 269 ↛ 274line 269 didn't jump to line 274, because the condition on line 269 was never false
270 return render_to_json_response({
271 'status': 'ok',
272 })
273 else:
274 _next = request.POST.get('next', None)
275 return safe_redirect(
276 _next,
277 resolve_url('dtracker-package-page', package_name=package),
278 )
281class UnsubscribeAllView(LoginRequiredMixin, View):
282 """
283 The view unsubscribes the currently logged in user from all packages.
284 If an optional ``email`` POST parameter is provided, only removes all
285 subscriptions for the given emails.
286 """
287 def post(self, request):
288 user = request.user
289 if 'email' not in request.POST: 289 ↛ 290line 289 didn't jump to line 290, because the condition on line 289 was never true
290 emails = user.emails.all()
291 else:
292 emails = user.emails.filter(email__in=request.POST.getlist('email'))
294 # Remove all the subscriptions
295 Subscription.objects.filter(
296 email_settings__user_email__in=emails).delete()
298 if request.headers.get('accept') == 'application/json': 298 ↛ 303line 298 didn't jump to line 303, because the condition on line 298 was never false
299 return render_to_json_response({
300 'status': 'ok',
301 })
302 else:
303 _next = request.POST.get('next', None)
304 return safe_redirect(
305 _next,
306 resolve_url('dtracker-index'),
307 )
310class ChooseSubscriptionEmailView(LoginRequiredMixin, View):
311 """
312 Lets the user choose which email to subscribe to a package with.
313 This is an alternative view when JS is disabled and the appropriate choice
314 cannot be offered in a popup.
315 """
316 template_name = 'accounts/choose-email.html'
318 def get(self, request):
319 if 'package' not in request.GET:
320 raise Http404
322 if not get_web_package(request.GET['package']):
323 raise Http404
325 return render(request, self.template_name, {
326 'package': request.GET['package'],
327 'emails': request.user.emails.all(),
328 })
331class ModifyKeywordsView(LoginRequiredMixin, View):
332 """
333 Lets the logged-in user modify their default keywords or
334 subscription-specific keywords.
335 """
336 def get_keywords(self, keywords):
337 """
338 :returns: :class:`Keyword <distro_tracker.core.models.Keyword>`
339 instances for the given keyword names.
340 """
341 return Keyword.objects.filter(name__in=keywords)
343 def modify_default_keywords(self, email, keywords):
344 try:
345 user_email = UserEmail.objects.get(user=self.user, email=email)
346 except (UserEmail.DoesNotExist):
347 return HttpResponseForbidden()
349 email_settings, _ = \
350 EmailSettings.objects.get_or_create(user_email=user_email)
351 email_settings.default_keywords.set(self.get_keywords(keywords))
353 return self.render_response()
355 def modify_subscription_keywords(self, email, package, keywords):
356 try:
357 user_email = UserEmail.objects.get(user=self.user, email=email)
358 except (UserEmail.DoesNotExist):
359 return HttpResponseForbidden()
361 email_settings, _ = \
362 EmailSettings.objects.get_or_create(user_email=user_email)
363 subscription = get_object_or_404(
364 Subscription, email_settings__user_email=user_email,
365 package__name=package)
367 subscription.keywords.clear()
368 for keyword in self.get_keywords(keywords):
369 subscription.keywords.add(keyword)
371 return self.render_response()
373 def render_response(self):
374 if self.request.headers.get('accept') == 'application/json': 374 ↛ 379line 374 didn't jump to line 379, because the condition on line 374 was never false
375 return render_to_json_response({
376 'status': 'ok',
377 })
378 else:
379 _next = self.request.POST.get('next', None)
380 return safe_redirect(
381 _next,
382 resolve_url('dtracker-index'),
383 )
385 def post(self, request):
386 if 'email' not in request.POST or 'keyword[]' not in request.POST: 386 ↛ 387line 386 didn't jump to line 387, because the condition on line 386 was never true
387 raise Http404
389 self.user = request.user
390 self.request = request
391 email = request.POST['email']
392 keywords = request.POST.getlist('keyword[]')
394 if 'package' in request.POST:
395 return self.modify_subscription_keywords(
396 email, request.POST['package'], keywords)
397 else:
398 return self.modify_default_keywords(email, keywords)
400 def get(self, request):
401 if 'email' not in request.GET:
402 raise Http404
403 email = request.GET['email']
405 try:
406 user_email = request.user.emails.get(email=email)
407 except UserEmail.DoesNotExist:
408 return HttpResponseForbidden()
410 if 'package' in request.GET:
411 package = request.GET['package']
412 subscription = get_object_or_404(
413 Subscription, email_settings__user_email=user_email,
414 package__name=package)
415 context = {
416 'post': {
417 'email': email,
418 'package': package,
419 },
420 'package': package,
421 'user_keywords': subscription.keywords.all(),
422 }
423 else:
424 context = {
425 'post': {
426 'email': email,
427 },
428 'user_keywords':
429 user_email.emailsettings.default_keywords.all(),
430 }
432 context.update({
433 'keywords': Keyword.objects.order_by('name').all(),
434 'email': email,
435 })
437 return render(request, 'accounts/modify-subscription.html', context)