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 

23 

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 

36 

37from django_email_accounts import views as email_accounts_views 

38from django_email_accounts.views import LoginRequiredMixin 

39 

40 

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 ) 

47 

48 

49class LoginView(email_accounts_views.LoginView): 

50 success_url = reverse_lazy('dtracker-accounts-profile') 

51 

52 

53class LogoutView(email_accounts_views.LogoutView): 

54 success_url = reverse_lazy('dtracker-index') 

55 

56 

57class RegisterUser(ConfirmationRenderMixin, email_accounts_views.RegisterUser): 

58 success_url = reverse_lazy('dtracker-accounts-register-success') 

59 

60 confirmation_email_subject = '{name} Registration Confirmation'.format( 

61 name=settings.GET_INSTANCE_NAME()) 

62 confirmation_email_from_address = settings.DISTRO_TRACKER_NOREPLY_EMAIL 

63 

64 

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

69 

70 

71class ResetPasswordView(ConfirmationRenderMixin, 

72 email_accounts_views.ResetPasswordView): 

73 success_url = reverse_lazy('dtracker-accounts-profile') 

74 

75 

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 

82 

83 

84class ChangePersonalInfoView(email_accounts_views.ChangePersonalInfoView): 

85 success_url = reverse_lazy('dtracker-accounts-profile-modify') 

86 

87 

88class PasswordChangeView(email_accounts_views.PasswordChangeView): 

89 success_url = reverse_lazy('dtracker-accounts-profile-password-change') 

90 

91 

92class AccountProfile(email_accounts_views.AccountProfile): 

93 pass 

94 

95 

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

100 

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 

104 

105 

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 

112 

113 

114class AccountMergeFinalize(email_accounts_views.AccountMergeFinalize): 

115 success_url = reverse_lazy('dtracker-accounts-merge-finalized') 

116 

117 

118class AccountMergeConfirmedView(email_accounts_views.AccountMergeConfirmedView): 

119 template_name = 'accounts/tracker-accounts-merge-confirmed.html' 

120 

121 

122class ConfirmAddAccountEmail(email_accounts_views.ConfirmAddAccountEmail): 

123 pass 

124 

125 

126class SubscriptionsView(LoginRequiredMixin, View): 

127 """ 

128 Displays a user's subscriptions. 

129 

130 This includes both direct package subscriptions and team memberships. 

131 """ 

132 template_name = 'accounts/subscriptions.html' 

133 

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

172 

173 

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

183 

184 

185class SubscribeUserToPackageView(LoginRequiredMixin, View): 

186 """ 

187 Subscribes the user to a package. 

188 

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) 

194 

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 

197 

198 # Remember selected emails via session variable 

199 request.session['selected_emails'] = emails 

200 

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

206 

207 _pkg = get_web_package(package) 

208 _err = None 

209 

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 ) 

223 

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 ) 

240 

241 

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 

251 

252 package = request.POST['package'] 

253 user = request.user 

254 

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) 

266 

267 qs.delete() 

268 

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 ) 

279 

280 

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

293 

294 # Remove all the subscriptions 

295 Subscription.objects.filter( 

296 email_settings__user_email__in=emails).delete() 

297 

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 ) 

308 

309 

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' 

317 

318 def get(self, request): 

319 if 'package' not in request.GET: 

320 raise Http404 

321 

322 if not get_web_package(request.GET['package']): 

323 raise Http404 

324 

325 return render(request, self.template_name, { 

326 'package': request.GET['package'], 

327 'emails': request.user.emails.all(), 

328 }) 

329 

330 

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) 

342 

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

348 

349 email_settings, _ = \ 

350 EmailSettings.objects.get_or_create(user_email=user_email) 

351 email_settings.default_keywords.set(self.get_keywords(keywords)) 

352 

353 return self.render_response() 

354 

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

360 

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) 

366 

367 subscription.keywords.clear() 

368 for keyword in self.get_keywords(keywords): 

369 subscription.keywords.add(keyword) 

370 

371 return self.render_response() 

372 

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 ) 

384 

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 

388 

389 self.user = request.user 

390 self.request = request 

391 email = request.POST['email'] 

392 keywords = request.POST.getlist('keyword[]') 

393 

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) 

399 

400 def get(self, request): 

401 if 'email' not in request.GET: 

402 raise Http404 

403 email = request.GET['email'] 

404 

405 try: 

406 user_email = request.user.emails.get(email=email) 

407 except UserEmail.DoesNotExist: 

408 return HttpResponseForbidden() 

409 

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 } 

431 

432 context.update({ 

433 'keywords': Keyword.objects.order_by('name').all(), 

434 'email': email, 

435 }) 

436 

437 return render(request, 'accounts/modify-subscription.html', context)