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"""Views for the :mod:`distro_tracker.core` app.""" 

11import importlib 

12from urllib.parse import quote 

13 

14from django.conf import settings 

15from django.contrib import messages 

16from django.core.exceptions import PermissionDenied 

17from django.core.mail import send_mail 

18from django.db.models import Q 

19from django.http import Http404 

20from django.shortcuts import get_object_or_404, redirect, render 

21from django.urls import reverse, reverse_lazy 

22from django.utils.decorators import method_decorator 

23from django.views.decorators.cache import cache_control 

24from django.views.generic import DeleteView, ListView, TemplateView, View 

25from django.views.generic.detail import DetailView 

26from django.views.generic.edit import FormView, UpdateView 

27 

28from distro_tracker import vendor 

29from distro_tracker.accounts.models import UserEmail 

30from distro_tracker.accounts.views import LoginRequiredMixin 

31from distro_tracker.core.forms import AddTeamMemberForm, CreateTeamForm 

32from distro_tracker.core.models import ( 

33 ActionItem, 

34 BinaryPackageName, 

35 Keyword, 

36 MembershipConfirmation, 

37 News, 

38 NewsRenderer, 

39 PackageName, 

40 PseudoPackageName, 

41 SourcePackageName, 

42 Team, 

43 TeamMembership, 

44 get_web_package 

45) 

46from distro_tracker.core.package_tables import create_table 

47from distro_tracker.core.panels import get_panels_for_package 

48from distro_tracker.core.utils import ( 

49 distro_tracker_render_to_string, 

50 get_or_none, 

51 render_to_json_response 

52) 

53from distro_tracker.core.utils.http import ( 

54 safe_redirect 

55) 

56 

57 

58def package_page(request, package_name): 

59 """ 

60 Renders the package page. 

61 """ 

62 package = get_web_package(package_name) 

63 if not package: 

64 raise Http404 

65 if package.get_absolute_url() not in (quote(request.path), request.path): 

66 return redirect(package) 

67 

68 is_subscribed = False 

69 if request.user.is_authenticated: 

70 # Check if the user is subscribed to the package 

71 is_subscribed = request.user.is_subscribed_to(package) 

72 

73 return render(request, 'core/package.html', { 

74 'package': package, 

75 'panels': get_panels_for_package(package, request), 

76 'is_subscribed': is_subscribed, 

77 }) 

78 

79 

80def package_page_redirect(request, package_name): 

81 """ 

82 Catch-all view which tries to redirect the user to a package page 

83 """ 

84 return redirect('dtracker-package-page', package_name=package_name) 

85 

86 

87def legacy_package_url_redirect(request, package_hash, package_name): 

88 """ 

89 Redirects access to URLs in the form of the "old" PTS package URLs to the 

90 new package URLs. 

91 

92 .. note:: 

93 The "old" package URL is: /<hash>/<package_name>.html 

94 """ 

95 return redirect('dtracker-package-page', package_name=package_name, 

96 permanent=True) 

97 

98 

99class PackageSearchView(View): 

100 """ 

101 A view which responds to package search queries. 

102 """ 

103 def get(self, request): 

104 if 'package_name' not in self.request.GET: 

105 raise Http404 

106 package_name = self.request.GET.get('package_name').lower().strip() 

107 

108 package = get_web_package(package_name) 

109 if package is not None: 

110 return redirect(package) 

111 else: 

112 return render(request, 'core/package_search.html', { 

113 'package_name': package_name 

114 }) 

115 

116 

117class OpenSearchDescription(View): 

118 """ 

119 Return the open search description XML document allowing 

120 browsers to launch searches on the website. 

121 """ 

122 

123 def get(self, request): 

124 return render(request, 'core/opensearch-description.xml', { 

125 'search_uri': request.build_absolute_uri( 

126 reverse('dtracker-package-search')), 

127 'autocomplete_uri': request.build_absolute_uri( 

128 reverse('dtracker-api-package-autocomplete')), 

129 'favicon_uri': request.build_absolute_uri( 

130 reverse('dtracker-favicon')), 

131 }, content_type='application/opensearchdescription+xml') 

132 

133 

134class PackageAutocompleteView(View): 

135 """ 

136 A view which responds to package auto-complete queries. 

137 

138 Renders a JSON list of package names matching the given query, meaning 

139 their name starts with the given query parameter. 

140 """ 

141 @method_decorator(cache_control(must_revalidate=True, max_age=3600)) 

142 def get(self, request): 

143 if 'q' not in request.GET: 

144 raise Http404 

145 query_string = request.GET['q'] 

146 package_type = request.GET.get('package_type', None) 

147 MANAGERS = { 

148 'pseudo': PseudoPackageName.objects, 

149 'source': SourcePackageName.objects, 

150 'binary': BinaryPackageName.objects.exclude(source=True), 

151 } 

152 # When no package type is given include both pseudo and source packages 

153 filtered = MANAGERS.get( 

154 package_type, 

155 PackageName.objects.filter(Q(source=True) | Q(pseudo=True)) 

156 ) 

157 filtered = filtered.filter(name__icontains=query_string) 

158 # Extract only the name of the package. 

159 filtered = filtered.values('name') 

160 # Limit the number of packages returned from the autocomplete 

161 AUTOCOMPLETE_ITEMS_LIMIT = 100 

162 filtered = filtered[:AUTOCOMPLETE_ITEMS_LIMIT] 

163 return render_to_json_response([query_string, 

164 [package['name'] 

165 for package in filtered]]) 

166 

167 

168def news_page(request, news_id, slug=''): 

169 """ 

170 Displays a news item's full content. 

171 """ 

172 news = get_object_or_404(News, pk=news_id) 

173 

174 renderer_class = \ 

175 NewsRenderer.get_renderer_for_content_type(news.content_type) 

176 if renderer_class is None: 176 ↛ 177line 176 didn't jump to line 177

177 renderer_class = \ 

178 NewsRenderer.get_renderer_for_content_type('text/plain') 

179 

180 renderer = renderer_class(news) 

181 return render(request, 'core/news.html', { 

182 'news_renderer': renderer, 

183 'news': news, 

184 }) 

185 

186 

187class PackageNews(ListView): 

188 """ 

189 A view which lists all the news of a package. 

190 """ 

191 _DEFAULT_NEWS_LIMIT = 30 

192 NEWS_LIMIT = getattr( 

193 settings, 

194 'DISTRO_TRACKER_NEWS_PANEL_LIMIT', 

195 _DEFAULT_NEWS_LIMIT) 

196 

197 paginate_by = NEWS_LIMIT 

198 template_name = 'core/package_news.html' 

199 context_object_name = 'news' 

200 

201 def get(self, request, package_name): 

202 self.package = get_object_or_404(PackageName, name=package_name) 

203 return super(PackageNews, self).get(request, package_name) 

204 

205 def get_queryset(self): 

206 news = self.package.news_set.prefetch_related('signed_by') 

207 return news.order_by('-datetime_created') 

208 

209 def get_context_data(self, *args, **kwargs): 

210 context = super(PackageNews, self).get_context_data(*args, **kwargs) 

211 context['package'] = self.package 

212 return context 

213 

214 

215class ActionItemJsonView(View): 

216 """ 

217 View renders a :class:`distro_tracker.core.models.ActionItem` in a JSON 

218 response. 

219 """ 

220 @method_decorator(cache_control(must_revalidate=True, max_age=3600)) 

221 def get(self, request, item_pk): 

222 item = get_object_or_404(ActionItem, pk=item_pk) 

223 return render_to_json_response(item.to_dict()) 

224 

225 

226class ActionItemView(View): 

227 """ 

228 View renders a :class:`distro_tracker.core.models.ActionItem` in an HTML 

229 response. 

230 """ 

231 def get(self, request, item_pk): 

232 item = get_object_or_404(ActionItem, pk=item_pk) 

233 return render(request, 'core/action-item.html', { 

234 'item': item, 

235 }) 

236 

237 

238def legacy_rss_redirect(request, package_hash, package_name): 

239 """ 

240 Redirects old package RSS news feed URLs to the new ones. 

241 """ 

242 return redirect( 

243 'dtracker-package-rss-news-feed', 

244 package_name=package_name, 

245 permanent=True) 

246 

247 

248class KeywordsView(View): 

249 def get(self, request): 

250 return render_to_json_response([ 

251 keyword.name for keyword in Keyword.objects.order_by('name').all() 

252 ]) 

253 

254 

255class CreateTeamView(LoginRequiredMixin, FormView): 

256 model = Team 

257 template_name = 'core/team-create.html' 

258 form_class = CreateTeamForm 

259 

260 def form_valid(self, form): 

261 instance = form.save(commit=False) 

262 user = self.request.user 

263 instance.owner = user 

264 instance.save() 

265 instance.add_members(user.emails.filter(email=user.main_email)) 

266 

267 return redirect(instance) 

268 

269 

270class TeamDetailsView(DetailView): 

271 model = Team 

272 template_name = 'core/team.html' 

273 table_limit = 20 

274 

275 def _create_tables(self): 

276 result, implemented = vendor.call( 

277 'get_tables_for_team_page', self.object, self.table_limit) 

278 if implemented: 278 ↛ 279line 278 didn't jump to line 279, because the condition on line 278 was never true

279 return result 

280 

281 return [ 

282 create_table( 

283 slug='general', scope=self.object, limit=self.table_limit), 

284 create_table( 

285 slug='general', scope=self.object, 

286 limit=self.table_limit, tag='tag:bugs' 

287 ), 

288 ] 

289 

290 def get_context_data(self, **kwargs): 

291 context = super(TeamDetailsView, self).get_context_data(**kwargs) 

292 context['tables'] = self._create_tables() 

293 if self.request.user.is_authenticated: 

294 context['user_member_of_team'] = self.object.user_is_member( 

295 self.request.user) 

296 

297 return context 

298 

299 

300class DeleteTeamView(DeleteView): 

301 model = Team 

302 success_url = reverse_lazy('dtracker-team-deleted') 

303 template_name = 'core/team-confirm-delete.html' 

304 

305 def get_object(self, *args, **kwargs): 

306 """ 

307 Makes sure that the team instance to be deleted is owned by the 

308 logged in user. 

309 """ 

310 instance = super(DeleteTeamView, self).get_object(*args, **kwargs) 

311 if instance.owner != self.request.user: 

312 raise PermissionDenied 

313 return instance 

314 

315 

316class UpdateTeamView(UpdateView): 

317 model = Team 

318 form_class = CreateTeamForm 

319 template_name = 'core/team-update.html' 

320 

321 def get_object(self, *args, **kwargs): 

322 """ 

323 Makes sure that the team instance to be updated is owned by the 

324 logged in user. 

325 """ 

326 instance = super(UpdateTeamView, self).get_object(*args, **kwargs) 

327 if instance.owner != self.request.user: 

328 raise PermissionDenied 

329 

330 # Set current maintainer email to the email field in the form 

331 if instance.maintainer_email is not None: 

332 self.initial.update( 

333 {'maintainer_email': instance.maintainer_email.email}) 

334 return instance 

335 

336 

337class AddPackageToTeamView(LoginRequiredMixin, View): 

338 def post(self, request, slug): 

339 """ 

340 Adds the package given in the POST parameters to the team. 

341 

342 If the currently logged in user is not a team member, a 

343 403 Forbidden response is given. 

344 

345 Once the package is added, the user is redirected back to the team's 

346 page. 

347 """ 

348 team = get_object_or_404(Team, slug=slug) 

349 if not team.user_is_member(request.user): 

350 # Only team members are allowed to modify the packages followed by 

351 # the team. 

352 raise PermissionDenied 

353 

354 if 'package' in request.POST: 354 ↛ 360line 354 didn't jump to line 360, because the condition on line 354 was never false

355 package_name = request.POST['package'] 

356 package = get_or_none(PackageName, name=package_name) 

357 if package: 

358 team.packages.add(package) 

359 

360 return redirect('dtracker-team-manage', slug=team.slug) 

361 

362 

363class RemovePackageFromTeamView(LoginRequiredMixin, View): 

364 def get_team(self, slug): 

365 team = get_object_or_404(Team, slug=slug) 

366 if not team.user_is_member(self.request.user): 

367 # Only team members are allowed to modify the packages followed by 

368 # the team. 

369 raise PermissionDenied 

370 

371 return team 

372 

373 def post(self, request, slug): 

374 """ 

375 Removes the package given in the POST parameters from the team. 

376 

377 If the currently logged in user is not a team member, a 

378 403 Forbidden response is given. 

379 

380 Once the package is removed, the user is redirected back to the team's 

381 page. 

382 """ 

383 self.request = request 

384 team = self.get_team(slug) 

385 

386 if 'package' in request.POST: 386 ↛ 392line 386 didn't jump to line 392, because the condition on line 386 was never false

387 package_name = request.POST['package'] 

388 package = get_or_none(PackageName, name=package_name) 

389 if package: 

390 team.packages.remove(package) 

391 

392 return redirect('dtracker-team-manage', slug=team.slug) 

393 

394 

395class JoinTeamView(LoginRequiredMixin, View): 

396 """ 

397 Lets logged in users join a public team. 

398 

399 After a user has been added to the team, redirect them back to the 

400 team page. 

401 """ 

402 template_name = 'core/team-join-choose-email.html' 

403 

404 def get(self, request, slug): 

405 team = get_object_or_404(Team, slug=slug) 

406 

407 return render(request, self.template_name, { 

408 'team': team, 

409 }) 

410 

411 def post(self, request, slug): 

412 team = get_object_or_404(Team, slug=slug) 

413 if not team.public: 

414 # Only public teams can be joined directly by users 

415 raise PermissionDenied 

416 

417 if 'email' in request.POST: 

418 emails = request.POST.getlist('email') 

419 # Make sure the user owns the emails 

420 user_emails = [e.email for e in request.user.emails.all()] 

421 for email in emails: 

422 if email not in user_emails: 

423 raise PermissionDenied 

424 # Add the given emails to the team 

425 team.add_members(self.request.user.emails.filter(email__in=emails)) 

426 

427 return redirect(team) 

428 

429 

430class LeaveTeamView(LoginRequiredMixin, View): 

431 """ 

432 Lets logged in users leave teams they are a part of. 

433 """ 

434 def get(self, request, slug): 

435 team = get_object_or_404(Team, slug=slug) 

436 return redirect(team) 

437 

438 def post(self, request, slug): 

439 team = get_object_or_404(Team, slug=slug) 

440 if not team.user_is_member(request.user): 

441 # Leaving a team when you're not already a part of it makes no 

442 # sense 

443 raise PermissionDenied 

444 

445 # Remove all the user's emails from the team 

446 team.remove_members( 

447 UserEmail.objects.filter(pk__in=request.user.emails.all())) 

448 

449 return redirect(team) 

450 

451 

452class ManageTeam(LoginRequiredMixin, ListView): 

453 """ 

454 Provides the team owner a method to manually add/remove members of the 

455 team. 

456 """ 

457 template_name = 'core/team-manage.html' 

458 paginate_by = 20 

459 context_object_name = 'members_list' 

460 

461 def get_queryset(self): 

462 return self.team.members.all().order_by('email') 

463 

464 def get_context_data(self, *args, **kwargs): 

465 context = super(ManageTeam, self).get_context_data(*args, **kwargs) 

466 context['team'] = self.team 

467 context['form'] = AddTeamMemberForm() 

468 return context 

469 

470 def get(self, request, slug): 

471 self.team = get_object_or_404(Team, slug=slug) 

472 if not self.team.user_is_member(self.request.user): 

473 # Only team members are allowed to access the page 

474 raise PermissionDenied 

475 return super(ManageTeam, self).get(request, slug) 

476 

477 

478class RemoveTeamMember(LoginRequiredMixin, View): 

479 def post(self, request, slug): 

480 self.team = get_object_or_404(Team, slug=slug) 

481 if self.team.owner != request.user: 

482 raise PermissionDenied 

483 

484 if 'email' in request.POST: 

485 emails = request.POST.getlist('email') 

486 self.team.remove_members(UserEmail.objects.filter(email__in=emails)) 

487 

488 return redirect('dtracker-team-manage', slug=self.team.slug) 

489 

490 

491class AddTeamMember(LoginRequiredMixin, View): 

492 def post(self, request, slug): 

493 self.team = get_object_or_404(Team, slug=slug) 

494 if self.team.owner != request.user: 

495 raise PermissionDenied 

496 

497 response = redirect('dtracker-team-manage', slug=self.team.slug) 

498 form = AddTeamMemberForm(request.POST) 

499 if form.is_valid(): 

500 email = form.cleaned_data['email'] 

501 # Emails that do not exist should be created 

502 user, _ = UserEmail.objects.get_or_create(email=email) 

503 if self.team.members.filter(email=user).exists(): 

504 messages.error( 

505 request, 

506 ("The email address %s is already a member " 

507 "of the team" % email) 

508 ) 

509 return response 

510 

511 # The membership is muted by default until the user confirms it 

512 membership = self.team.add_members([user], muted=True)[0] 

513 confirmation = MembershipConfirmation.objects.create_confirmation( 

514 membership=membership) 

515 send_mail( 

516 'Team Membership Confirmation', 

517 distro_tracker_render_to_string( 

518 'core/email-team-membership-confirmation.txt', 

519 { 

520 'confirmation': confirmation, 

521 'team': self.team, 

522 }), 

523 from_email=settings.DISTRO_TRACKER_CONTACT_EMAIL, 

524 recipient_list=[email]) 

525 

526 return response 

527 

528 

529class ConfirmMembershipView(View): 

530 def get(self, request, confirmation_key): 

531 confirmation = get_object_or_404( 

532 MembershipConfirmation, confirmation_key=confirmation_key) 

533 membership = confirmation.membership 

534 membership.muted = False 

535 membership.save() 

536 # The confirmation is no longer necessary 

537 confirmation.delete() 

538 

539 return redirect(membership.team) 

540 

541 

542class TeamListView(ListView): 

543 queryset = Team.objects.filter(public=True).order_by('name') 

544 paginate_by = 20 

545 template_name = 'core/team-list.html' 

546 context_object_name = 'team_list' 

547 

548 

549class SetMuteTeamView(LoginRequiredMixin, View): 

550 """ 

551 The view lets users mute or unmute a team membership or a particular 

552 package in the membership. 

553 """ 

554 action = 'mute' 

555 

556 def post(self, request, slug): 

557 team = get_object_or_404(Team, slug=slug) 

558 if 'email' not in request.POST: 

559 raise Http404 

560 user = request.user 

561 try: 

562 email = user.emails.get(email=request.POST['email']) 

563 except UserEmail.DoesNotExist: 

564 raise PermissionDenied 

565 

566 try: 

567 membership = team.team_membership_set.get(user_email=email) 

568 except TeamMembership.DoesNotExist: 

569 raise Http404 

570 

571 if self.action == 'mute': 

572 mute = True 

573 elif self.action == 'unmute': 573 ↛ 576line 573 didn't jump to line 576, because the condition on line 573 was never false

574 mute = False 

575 else: 

576 raise Http404 

577 

578 if 'package' in request.POST: 

579 package = get_object_or_404(PackageName, 

580 name=request.POST['package']) 

581 membership.set_mute_package(package, mute) 

582 else: 

583 membership.muted = mute 

584 membership.save() 

585 

586 _next = request.POST.get('next', None) 

587 return safe_redirect(_next, team) 

588 

589 

590class SetMembershipKeywords(LoginRequiredMixin, View): 

591 """ 

592 The view lets users set either default membership keywords or 

593 package-specific keywords. 

594 """ 

595 def render_response(self): 

596 if self.request.headers.get('accept') == 'application/json': 

597 return render_to_json_response({ 

598 'status': 'ok', 

599 }) 

600 _next = self.request.POST.get('next', None) 

601 return safe_redirect(_next, self.team) 

602 

603 def post(self, request, slug): 

604 self.request = request 

605 self.team = get_object_or_404(Team, slug=slug) 

606 user = request.user 

607 mandatory_parameters = ('email', 'keyword[]') 

608 if any(param not in request.POST for param in mandatory_parameters): 

609 raise Http404 

610 try: 

611 email = user.emails.get(email=request.POST['email']) 

612 except UserEmail.DoesNotExist: 

613 raise PermissionDenied 

614 

615 try: 

616 membership = self.team.team_membership_set.get(user_email=email) 

617 except TeamMembership.DoesNotExist: 

618 raise Http404 

619 

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

621 if 'package' in request.POST: 

622 package = get_object_or_404(PackageName, 

623 name=request.POST['package']) 

624 membership.set_keywords(package, keywords) 

625 else: 

626 membership.set_membership_keywords(keywords) 

627 

628 return self.render_response() 

629 

630 

631class EditMembershipView(LoginRequiredMixin, ListView): 

632 template_name = 'core/edit-team-membership.html' 

633 paginate_by = 20 

634 context_object_name = 'package_list' 

635 

636 def get(self, request, slug): 

637 self.team = get_object_or_404(Team, slug=slug) 

638 if 'email' not in request.GET: 

639 raise Http404 

640 user = request.user 

641 try: 

642 email = user.emails.get(email=request.GET['email']) 

643 except UserEmail.DoesNotExist: 

644 raise PermissionDenied 

645 

646 try: 

647 self.membership = \ 

648 self.team.team_membership_set.get(user_email=email) 

649 except TeamMembership.DoesNotExist: 

650 raise Http404 

651 

652 return super(EditMembershipView, self).get(request, slug) 

653 

654 def get_queryset(self): 

655 return self.team.packages.all().order_by('name') 

656 

657 def get_context_data(self, *args, **kwargs): 

658 # Annotate the packages with a boolean indicating whether the package 

659 # is muted by the user and a list of keywords specific for the package 

660 # membership 

661 for pkg in self.object_list: 

662 pkg.is_muted = self.membership.is_muted(pkg) 

663 pkg.keywords = sorted( 

664 self.membership.get_keywords(pkg), 

665 key=lambda x: x.name) 

666 context = super(EditMembershipView, self).get_context_data(*args, 

667 **kwargs) 

668 context['membership'] = self.membership 

669 return context 

670 

671 

672class TeamAutocompleteView(View): 

673 """ 

674 A view which responds to team auto-complete queries. 

675 

676 Renders a JSON list of team names matching the given query, meaning 

677 their name contains the given query parameter. 

678 """ 

679 @method_decorator(cache_control(must_revalidate=True, max_age=3600)) 

680 def get(self, request): 

681 if 'q' not in request.GET: 681 ↛ 682line 681 didn't jump to line 682, because the condition on line 681 was never true

682 raise Http404 

683 query_string = request.GET['q'] 

684 filtered = Team.objects.filter( 

685 Q(name__icontains=query_string) | Q(slug__icontains=query_string)) 

686 # Extract only the name and slug of the team. 

687 filtered = filtered.values('name', 'slug') 

688 # Limit the number of teams returned from the autocomplete 

689 AUTOCOMPLETE_ITEMS_LIMIT = 100 

690 filtered = filtered[:AUTOCOMPLETE_ITEMS_LIMIT] 

691 return render_to_json_response({ 

692 'query_string': query_string, 

693 'teams': list(filtered) 

694 }) 

695 

696 

697class TeamSearchView(View): 

698 """ 

699 A view which responds to team search queries. 

700 """ 

701 def get(self, request): 

702 if 'query' not in self.request.GET: 702 ↛ 703line 702 didn't jump to line 703, because the condition on line 702 was never true

703 raise Http404 

704 

705 query = self.request.GET.get('query') 

706 team = self.find_team(query) 

707 if team is not None: 

708 return redirect(team) 

709 else: 

710 messages.error( 

711 request, 

712 ("No team could be identified with the query string %s" % query) 

713 ) 

714 return redirect(reverse('dtracker-team-list')) 

715 

716 def find_team(self, query): 

717 if Team.objects.filter(slug=query).exists(): 

718 return Team.objects.filter(slug=query).first() 

719 elif Team.objects.filter(name=query).exists(): 

720 return Team.objects.filter(name=query).first() 

721 elif Team.objects.filter( 

722 Q(name__icontains=query) | Q(slug__icontains=query) 

723 ).count() == 1: 

724 return Team.objects.filter( 

725 Q(name__icontains=query) | Q(slug__icontains=query)).first() 

726 

727 return None 

728 

729 

730class TeamPackagesTableView(View): 

731 """ 

732 View renders a :class:`distro_tracker.core.package_tables.BasePackageTable` 

733 in an HTML response. 

734 """ 

735 template_name = 'core/team-packages-table.html' 

736 

737 def get(self, request, slug, table_slug): 

738 team = get_object_or_404(Team, slug=slug) 

739 

740 tag = request.GET.get('tag', None) 

741 limit = request.GET.get('limit', None) 

742 self.table = create_table( 

743 slug=table_slug, scope=team, limit=limit, tag=tag) 

744 return render(request, self.template_name, { 

745 'table': self.table, 

746 'team': team 

747 }) 

748 

749 

750class IndexView(TemplateView): 

751 template_name = 'core/index.html' 

752 

753 def get_context_data(self, **kwargs): 

754 context = super(IndexView, self).get_context_data(**kwargs) 

755 links = [] 

756 for app in settings.INSTALLED_APPS: 

757 try: 

758 urlmodule = importlib.import_module(app + '.tracker_urls') 

759 if hasattr(urlmodule, 'frontpagelinks'): 

760 links += [(reverse(name), text) 

761 for name, text in urlmodule.frontpagelinks] 

762 except ImportError: 

763 pass 

764 context['application_links'] = links 

765 return context