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"""Implements the core panels shown on package pages.""" 

11import importlib 

12import logging 

13from collections import defaultdict 

14 

15from debian.debian_support import AptPkgVersion 

16 

17from django.conf import settings 

18from django.utils.functional import cached_property 

19from django.utils.safestring import mark_safe 

20 

21from distro_tracker import vendor 

22from distro_tracker.core.models import ( 

23 ActionItem, 

24 BugDisplayManagerMixin, 

25 MailingList, 

26 News, 

27 PackageData, 

28 PseudoPackageName, 

29 SourcePackageName 

30) 

31from distro_tracker.core.templatetags.distro_tracker_extras import octicon 

32from distro_tracker.core.utils import ( 

33 add_developer_extras, 

34 get_vcs_name 

35) 

36from distro_tracker.core.utils.plugins import PluginRegistry 

37 

38logger = logging.getLogger(__name__) 

39 

40 

41class BasePanel(metaclass=PluginRegistry): 

42 

43 """ 

44 A base class representing panels which are displayed on a package page. 

45 

46 To include a panel on the package page, users only need to create a 

47 subclass and implement the necessary properties and methods. 

48 

49 .. note:: 

50 To make sure the subclass is loaded, make sure to put it in a 

51 ``tracker_panels`` module at the top level of a Django app. 

52 """ 

53 #: A list of available positions 

54 # NOTE: This is a good candidate for Python3.4's Enum. 

55 POSITIONS = ( 

56 'left', 

57 'center', 

58 'right', 

59 ) 

60 

61 def __init__(self, package, request): 

62 self.package = package 

63 self.request = request 

64 

65 @property 

66 def context(self): 

67 """ 

68 Should return a dictionary representing context variables necessary for 

69 the panel. 

70 When the panel's template is rendered, it will have access to the values 

71 in this dictionary. 

72 """ 

73 return {} 

74 

75 @property 

76 def position(self): 

77 """ 

78 The property should be one of the available :attr:`POSITIONS` signalling 

79 where the panel should be positioned in the page. 

80 """ 

81 return 'center' 

82 

83 @property 

84 def title(self): 

85 """ 

86 The title of the panel. 

87 """ 

88 return '' 

89 

90 @property 

91 def template_name(self): 

92 """ 

93 If the panel has a corresponding template which is used to render its 

94 HTML output, this property should contain the name of this template. 

95 """ 

96 return None 

97 

98 @property 

99 def html_output(self): 

100 """ 

101 If the panel does not want to use a template, it can return rendered 

102 HTML in this property. The HTML needs to be marked safe or else it will 

103 be escaped in the final output. 

104 """ 

105 return None 

106 

107 @property 

108 def panel_importance(self): 

109 """ 

110 Returns and integer giving the importance of a package. 

111 The panels in a single column are always positioned in decreasing 

112 importance order. 

113 """ 

114 return 0 

115 

116 @property 

117 def has_content(self): 

118 """ 

119 Returns a bool indicating whether the panel actually has any content to 

120 display for the package. 

121 """ 

122 return True 

123 

124 

125def get_panels_for_package(package, request): 

126 """ 

127 A convenience method which accesses the :class:`BasePanel`'s list of 

128 children and instantiates them for the given package. 

129 

130 :returns: A dict mapping the page position to a list of Panels which should 

131 be rendered in that position. 

132 :rtype: dict 

133 """ 

134 # First import panels from installed apps. 

135 for app in settings.INSTALLED_APPS: 

136 try: 

137 module_name = app + '.' + 'tracker_panels' 

138 importlib.import_module(module_name) 

139 except ImportError: 

140 # The app does not implement package panels. 

141 pass 

142 

143 panels = defaultdict(lambda: []) 

144 for panel_class in BasePanel.plugins: 

145 if panel_class is not BasePanel: 145 ↛ 144line 145 didn't jump to line 144, because the condition on line 145 was never false

146 panel = panel_class(package, request) 

147 if panel.has_content: 

148 panels[panel.position].append(panel) 

149 

150 # Each columns' panels are sorted in the order of decreasing importance 

151 return dict({ 

152 key: list(sorted(value, key=lambda x: -x.panel_importance)) 

153 for key, value in panels.items() 

154 }) 

155 

156 

157class GeneralInformationPanel(BasePanel): 

158 

159 """ 

160 This panel displays general information regarding a package. 

161 

162 - name 

163 - component 

164 - version (in the default repository) 

165 - maintainer 

166 - uploaders 

167 - architectures 

168 - standards version 

169 - VCS 

170 

171 Several vendor-specific functions can be implemented which augment this 

172 panel: 

173 

174 - :func:`get_developer_information_url 

175 <distro_tracker.vendor.skeleton.rules.get_developer_information_url>` 

176 - :func:`get_maintainer_extra 

177 <distro_tracker.vendor.skeleton.rules.get_maintainer_extra>` 

178 - :func:`get_uploader_extra 

179 <distro_tracker.vendor.skeleton.rules.get_uploader_extra>` 

180 """ 

181 position = 'left' 

182 title = 'general' 

183 template_name = 'core/panels/general.html' 

184 

185 def _get_archive_url_info(self, email): 

186 ml = MailingList.objects.get_by_email(email) 

187 if ml: 187 ↛ 188line 187 didn't jump to line 188, because the condition on line 187 was never true

188 return ml.archive_url_for_email(email) 

189 

190 def _add_archive_urls(self, general): 

191 maintainer_email = general['maintainer']['email'] 

192 general['maintainer']['archive_url'] = ( 

193 self._get_archive_url_info(maintainer_email) 

194 ) 

195 

196 uploaders = general.get('uploaders', None) 

197 if not uploaders: 197 ↛ 200line 197 didn't jump to line 200, because the condition on line 197 was never false

198 return 

199 

200 for uploader in uploaders: 

201 uploader['archive_url'] = ( 

202 self._get_archive_url_info(uploader['email']) 

203 ) 

204 

205 @cached_property 

206 def context(self): 

207 try: 

208 info = PackageData.objects.get(package=self.package, key='general') 

209 except PackageData.DoesNotExist: 

210 # There is no general info for the package 

211 return 

212 

213 general = info.value 

214 # Add source package URL 

215 url, implemented = vendor.call('get_package_information_site_url', **{ 

216 'package_name': general['name'], 

217 'source_package': True, 

218 }) 

219 if implemented and url: 219 ↛ 220line 219 didn't jump to line 220, because the condition on line 219 was never true

220 general['url'] = url 

221 # Map the VCS type to its name. 

222 if 'vcs' in general: 

223 shorthand = general['vcs'].get('type', 'unknown') 

224 general['vcs']['full_name'] = get_vcs_name(shorthand) 

225 # Add vcs extra links (including Vcs-Browser) 

226 try: 

227 vcs_extra_links = PackageData.objects.get( 

228 package=self.package, key='vcs_extra_links').value 

229 except PackageData.DoesNotExist: 

230 vcs_extra_links = {} 

231 if 'browser' in general['vcs']: 231 ↛ 232line 231 didn't jump to line 232, because the condition on line 231 was never true

232 vcs_extra_links['Browse'] = general['vcs']['browser'] 

233 vcs_extra_links.pop('checksum', None) 

234 general['vcs']['extra_links'] = [ 

235 (key, vcs_extra_links[key]) 

236 for key in sorted(vcs_extra_links.keys()) 

237 ] 

238 # Add mailing list archive URLs 

239 self._add_archive_urls(general) 

240 # Add developer information links and any other vendor-specific extras 

241 general = add_developer_extras(general) 

242 

243 return general 

244 

245 @property 

246 def has_content(self): 

247 return bool(self.context) 

248 

249 

250class VersionsInformationPanel(BasePanel): 

251 

252 """ 

253 This panel displays the versions of the package in each of the repositories 

254 it is found in. 

255 

256 Several vendor-specific functions can be implemented which augment this 

257 panel: 

258 

259 - :func:`get_package_information_site_url 

260 <distro_tracker.vendor.skeleton.rules.get_package_information_site_url>` 

261 - :func:`get_external_version_information_urls 

262 <distro_tracker.vendor.skeleton.rules.get_external_version_information_urls>` 

263 """ 

264 position = 'left' 

265 title = 'versions' 

266 template_name = 'core/panels/versions.html' 

267 

268 @cached_property 

269 def context(self): 

270 try: 

271 info = PackageData.objects.get( 

272 package=self.package, key='versions') 

273 except PackageData.DoesNotExist: 

274 info = None 

275 

276 context = {} 

277 

278 if info: 

279 version_info = info.value 

280 package_name = info.package.name 

281 for item in version_info.get('version_list', ()): 281 ↛ 282line 281 didn't jump to line 282, because the loop on line 281 never started

282 url, implemented = vendor.call( 

283 'get_package_information_site_url', 

284 ** 

285 {'package_name': package_name, 

286 'repository': item.get( 

287 'repository'), 

288 'source_package': True, 

289 'version': 

290 item.get('version'), }) 

291 if implemented and url: 

292 item['url'] = url 

293 

294 context['version_info'] = version_info 

295 

296 # Add in any external version resource links 

297 external_resources, implemented = ( 

298 vendor.call('get_external_version_information_urls', 

299 self.package.name) 

300 ) 

301 if implemented and external_resources: 

302 context['external_resources'] = external_resources 

303 

304 # Add any vendor-provided versions 

305 vendor_versions, implemented = vendor.call( 

306 'get_extra_versions', self.package) 

307 if implemented and vendor_versions: 

308 context['vendor_versions'] = vendor_versions 

309 

310 return context 

311 

312 @property 

313 def has_content(self): 

314 return (bool(self.context.get('version_info', None)) or 

315 bool(self.context.get('vendor_versions', None))) 

316 

317 

318class VersionedLinks(BasePanel): 

319 

320 """ 

321 A panel displaying links specific for source package versions. 

322 

323 The panel exposes an endpoint which allows for extending the displayed 

324 content. This is achieved by implementing a 

325 :class:`VersionedLinks.LinkProvider` subclass. 

326 """ 

327 position = 'left' 

328 title = 'versioned links' 

329 template_name = 'core/panels/versioned-links.html' 

330 

331 class LinkProvider(metaclass=PluginRegistry): 

332 

333 """ 

334 A base class for classes which should provide a list of version 

335 specific links. 

336 

337 Subclasses need to define the :attr:`icons` property and implement the 

338 :meth:`get_link_for_icon` method. 

339 """ 

340 #: A list of strings representing icons for links that the class 

341 #: provides. 

342 #: Each string is an HTML representation of the icon. 

343 #: If the string should be considered safe and rendered in the 

344 #: resulting template without HTML encoding it, it should be marked 

345 #: with :func:`django.utils.safestring.mark_safe`. 

346 #: It requires each icon to be a string to discourage using complex 

347 #: markup for icons. Using a template is possible by making 

348 #: :attr:`icons` a property and rendering the template as string before 

349 #: returning it in the list. 

350 icons = [] 

351 

352 def get_link_for_icon(self, package, icon_index): 

353 """ 

354 Return a URL for the given package version which should be used for 

355 the icon at the given index in the :attr:`icons` property. 

356 If no link can be given for the icon, ``None`` should be returned 

357 instead. 

358 

359 :type package: :class:`SourcePackage 

360 <distro_tracker.core.models.SourcePackage>` 

361 :type icon_index: int 

362 

363 :rtype: :class:`string` or ``None`` 

364 """ 

365 return None 

366 

367 def get_links(self, package): 

368 """ 

369 For each of the icons returned by the :attr:`icons` property, 

370 returns a URL specific for the given package. 

371 

372 The order of the URLs must match the order of the icons (matching 

373 links and icons need to have the same index). Consequently, the 

374 length of the returned list is the same as the length of the 

375 :attr:`icons` property. 

376 

377 If no link can be given for some icon, ``None`` should be put 

378 instead. 

379 

380 This method has a default implementation which calls the 

381 :meth:`get_link_for_icon` for each icon defined in the :attr:`icons` 

382 property. This should be enough for all intents and purposes and 

383 the method should not need to be overridden by subclasses. 

384 

385 :param package: The source package instance for which links should 

386 be provided 

387 :type package: :class:`SourcePackage 

388 <distro_tracker.core.models.SourcePackage>` 

389 

390 :returns: List of URLs for the package 

391 :rtype: list 

392 """ 

393 return [ 

394 self.get_link_for_icon(package, index) 

395 for index, icon in enumerate(self.icons) 

396 ] 

397 

398 @classmethod 

399 def get_providers(cls): 

400 """ 

401 Helper classmethod returning a list of instances of all registered 

402 :class:`VersionedLinks.LinkProvider` subclasses. 

403 """ 

404 return [ 

405 klass() 

406 for klass in cls.plugins 

407 if klass is not cls 

408 ] 

409 

410 def __init__(self, *args, **kwargs): 

411 super(VersionedLinks, self).__init__(*args, **kwargs) 

412 #: All icons that the panel displays for each version. 

413 #: Icons must be the same for each version. 

414 self.ALL_ICONS = [ 

415 icon 

416 for link_provider in VersionedLinks.LinkProvider.get_providers() 

417 for icon in link_provider.icons 

418 ] 

419 

420 @cached_property 

421 def context(self): 

422 # Only process source files 

423 if not isinstance(self.package, SourcePackageName): 423 ↛ 424line 423 didn't jump to line 424, because the condition on line 423 was never true

424 return 

425 # Make sure we display the versions in a version-number increasing 

426 # order 

427 versions = sorted( 

428 self.package.source_package_versions.all(), 

429 key=lambda x: AptPkgVersion(x.version) 

430 ) 

431 

432 versioned_links = [] 

433 for package in versions: 

434 if all([src_repo_entry.repository.get_flags()['hidden'] 

435 for src_repo_entry in package.repository_entries.all()]): 

436 # All associated repositories are hidden 

437 continue 

438 links = [ 

439 link 

440 for link_provider in VersionedLinks.LinkProvider.get_providers() 

441 for link in link_provider.get_links(package) 

442 ] 

443 versioned_links.append({ 

444 'number': package.version, 

445 'links': [ 

446 { 

447 'icon_html': icon, 

448 'url': link, 

449 } 

450 for icon, link in zip(self.ALL_ICONS, links) 

451 ] 

452 }) 

453 

454 return versioned_links 

455 

456 @property 

457 def has_content(self): 

458 # Do not display the panel if there are no icons or the package has no 

459 # versions. 

460 return bool(self.ALL_ICONS) and bool(self.context) 

461 

462 

463class DscLinkProvider(VersionedLinks.LinkProvider): 

464 icons = [ 

465 octicon('desktop-download', 

466 '.dsc, use dget on this link to retrieve source package'), 

467 ] 

468 

469 def get_link_for_icon(self, package, index): 

470 if index >= len(self.icons): 

471 return None 

472 if package.main_entry: 

473 return package.main_entry.dsc_file_url 

474 

475 

476class BinariesInformationPanel(BasePanel, BugDisplayManagerMixin): 

477 

478 """ 

479 This panel displays a list of binary package names which a given source 

480 package produces. 

481 

482 If there are existing bug statistics for some of the binary packages, a 

483 list of bug counts is also displayed. 

484 

485 If implemented, the following functions can augment the information of 

486 this panel: 

487 

488 - :func:`get_package_information_site_url 

489 <distro_tracker.vendor.skeleton.rules.get_package_information_site_url>` 

490 provides the link used for each binary package name. 

491 - :func:`get_bug_display_manager_class 

492 <distro_tracker.vendor.skeleton.rules.get_bug_display_manager_class>` 

493 provides a custom class to handle which bug statistics for a given binary 

494 package must be displayed as a list of bug counts for different 

495 categories. 

496 This is useful if, for example, the vendor wants to display only a small 

497 set of categories rather than all stats found in the database. 

498 Refer to the function's documentation for the format of the return value. 

499 """ 

500 position = 'left' 

501 title = 'binaries' 

502 template_name = 'core/panels/binaries.html' 

503 

504 def _get_binary_bug_stats(self, binary_name): 

505 bug_stats = self.bug_manager.get_binary_bug_stats(binary_name) 

506 

507 if bug_stats is None: 507 ↛ 508line 507 didn't jump to line 508, because the condition on line 507 was never true

508 return 

509 # Try to get the URL to the bug tracker for the given categories 

510 for category in bug_stats: 

511 url = self.bug_manager.get_bug_tracker_url( 

512 binary_name, 'binary', category['category_name']) 

513 if not url: 513 ↛ 515line 513 didn't jump to line 515, because the condition on line 513 was never false

514 continue 

515 category['url'] = url 

516 # Include the total bug count and corresponding tracker URL 

517 all_bugs_url = self.bug_manager.get_bug_tracker_url( 

518 binary_name, 'binary', 'all') 

519 return { 

520 'total_count': sum( 

521 category['bug_count'] for category in bug_stats), 

522 'all_bugs_url': all_bugs_url, 

523 'categories': bug_stats, 

524 } 

525 

526 @cached_property 

527 def context(self): 

528 try: 

529 info = PackageData.objects.get( 

530 package=self.package, key='binaries') 

531 except PackageData.DoesNotExist: 

532 return 

533 

534 binaries = info.value 

535 for binary in binaries: 

536 # For each binary try to include known bug stats 

537 bug_stats = self._get_binary_bug_stats(binary['name']) 

538 if bug_stats is not None: 538 ↛ 543line 538 didn't jump to line 543, because the condition on line 538 was never false

539 binary['bug_stats'] = bug_stats 

540 

541 # For each binary try to include a link to an external package-info 

542 # site. 

543 if 'repository' in binary: 543 ↛ 544line 543 didn't jump to line 544, because the condition on line 543 was never true

544 url, implemented = vendor.call( 

545 'get_package_information_site_url', **{ 

546 'package_name': binary['name'], 

547 'repository': binary['repository'], 

548 'source_package': False, 

549 } 

550 ) 

551 if implemented and url: 

552 binary['url'] = url 

553 

554 return binaries 

555 

556 @property 

557 def has_content(self): 

558 return bool(self.context) 

559 

560 

561class PanelItem(object): 

562 

563 """ 

564 The base class for all items embeddable in panels. 

565 

566 Lets the users define the panel item's content in two ways: 

567 

568 - A template and a context accessible to the template as item.context 

569 variable 

570 - Define the HTML output directly. This string needs to be marked safe, 

571 otherwise it will be HTML encoded in the output. 

572 """ 

573 #: The template to render when this item should be rendered 

574 template_name = None 

575 #: Context to be available when the template is rendered 

576 context = None 

577 #: HTML output to be placed in the page when the item should be rendered 

578 html_output = None 

579 

580 

581class TemplatePanelItem(PanelItem): 

582 

583 """ 

584 A subclass of :class:`PanelItem` which gives a more convenient interface 

585 for defining items rendered by a template + context. 

586 """ 

587 

588 def __init__(self, template_name, context=None): 

589 self.template_name = template_name 

590 self.context = context 

591 

592 

593class HtmlPanelItem(PanelItem): 

594 

595 """ 

596 A subclass of :class:`PanelItem` which gives a more convenient interface 

597 for defining items which already provide HTML text. 

598 Takes care of marking the given text as safe. 

599 """ 

600 

601 def __init__(self, html): 

602 self._html = mark_safe(html) 

603 

604 @property 

605 def html_output(self): 

606 return self._html 

607 

608 

609class PanelItemProvider(metaclass=PluginRegistry): 

610 

611 """ 

612 A base class for classes which produce :class:`PanelItem` instances. 

613 

614 Each panel which wishes to allow clients to register item providers needs 

615 a separate subclass of this class. 

616 """ 

617 @classmethod 

618 def all_panel_item_providers(cls): 

619 """ 

620 Returns all subclasses of the given :class:`PanelItemProvider` 

621 subclass. 

622 

623 Makes it possible for each :class:`ListPanel` to have its own separate 

624 set of providers derived from its base ItemProvider. 

625 """ 

626 result = [] 

627 for item_provider in cls.plugins: 

628 if not issubclass(item_provider, cls): 

629 continue 

630 # Not returning items from non-installed apps 

631 if not any([str(item_provider.__module__).startswith(a) 

632 for a in settings.INSTALLED_APPS]): 

633 continue 

634 result.append(item_provider) 

635 return result 

636 

637 def __init__(self, package): 

638 self.package = package 

639 

640 def get_panel_items(self): 

641 """ 

642 The main method which needs to return a list of :class:`PanelItem` 

643 instances which the provider wants rendered in the panel. 

644 """ 

645 return [] 

646 

647 

648class ListPanelMeta(PluginRegistry): 

649 

650 """ 

651 A meta class for the :class:`ListPanel`. Makes sure that each subclass of 

652 :class:`ListPanel` has a new :class:`PanelItemProvider` subclass. 

653 """ 

654 def __init__(cls, name, bases, attrs): # noqa 

655 super(ListPanelMeta, cls).__init__(name, bases, attrs) 

656 if name != 'NewBase': 656 ↛ exitline 656 didn't return from function '__init__', because the condition on line 656 was never false

657 cls.ItemProvider = type( 

658 str('{name}ItemProvider'.format(name=name)), 

659 (PanelItemProvider,), 

660 {} 

661 ) 

662 

663 

664class ListPanel(BasePanel, metaclass=ListPanelMeta): 

665 

666 """ 

667 The base class for panels which would like to present an extensible list of 

668 items. 

669 

670 The subclasses only need to add the :attr:`position <BasePanel.position>` 

671 and :attr:`title <BasePanel.title>` attributes, the rendering is handled 

672 automatically, based on the registered list of item providers for the 

673 panel. 

674 

675 Clients can add items to the panel by implementing a subclass of the 

676 :class:`ListPanel.ItemProvider` class. 

677 

678 It is possible to change the :attr:`template_name <BasePanel.template_name>` 

679 too, but making sure all the same context variable names are used in the 

680 custom template. 

681 """ 

682 template_name = 'core/panels/list-panel.html' 

683 

684 def get_items(self): 

685 """ 

686 Returns a list of :class:`PanelItem` instances for the current panel 

687 instance. This means the items are prepared for the package given to 

688 the panel instance. 

689 """ 

690 panel_providers = self.ItemProvider.all_panel_item_providers() 

691 items = [] 

692 for panel_provider_class in panel_providers: 

693 panel_provider = panel_provider_class(self.package) 

694 try: 

695 new_panel_items = panel_provider.get_panel_items() 

696 except Exception: 

697 logger.exception('Panel provider %s: error generating items.', 

698 panel_provider.__class__) 

699 continue 

700 if new_panel_items is not None: 

701 items.extend(new_panel_items) 

702 return items 

703 

704 @cached_property 

705 def context(self): 

706 return { 

707 'items': self.get_items() 

708 } 

709 

710 @property 

711 def has_content(self): 

712 return bool(self.context['items']) 

713 

714 

715# This should be a sort of "abstract" panel which should never be rendered on 

716# its own, so it is removed from the list of registered panels. 

717ListPanel.unregister_plugin() 

718 

719 

720class LinksPanel(ListPanel): 

721 

722 """ 

723 This panel displays a list of important links for a given source package. 

724 

725 Clients can add items to the panel by implementing a subclass of the 

726 :class:`LinksPanel.ItemProvider` class. 

727 """ 

728 position = 'right' 

729 title = 'links' 

730 

731 class SimpleLinkItem(HtmlPanelItem): 

732 

733 """ 

734 A convenience :class:`PanelItem` which renders a simple link in the 

735 panel, by having the text, url and, optionally, the tooltip text 

736 given in the constructor. 

737 """ 

738 TEMPLATE = '<a href="{url}">{text}</a>' 

739 TEMPLATE_TOOLTIP = '<a href="{url}" title="{title}">{text}</a>' 

740 

741 def __init__(self, text, url, title=None): 

742 if title: 

743 template = self.TEMPLATE_TOOLTIP 

744 else: 

745 template = self.TEMPLATE 

746 html = template.format(text=text, url=url, title=title) 

747 super(LinksPanel.SimpleLinkItem, self).__init__(html) 

748 

749 

750class GeneralInfoLinkPanelItems(LinksPanel.ItemProvider): 

751 

752 """ 

753 Provides the :class:`LinksPanel` with links derived from general package 

754 information. 

755 

756 For now, this is only the homepage of the package, if available. 

757 """ 

758 

759 def get_panel_items(self): 

760 items = [] 

761 if hasattr(self.package, 'main_version') and self.package.main_version \ 

762 and self.package.main_version.homepage: 

763 items.append( 

764 LinksPanel.SimpleLinkItem( 

765 'homepage', 

766 self.package.main_version.homepage, 

767 'upstream web homepage' 

768 ), 

769 ) 

770 return items 

771 

772 

773class NewsPanel(BasePanel): 

774 _DEFAULT_NEWS_LIMIT = 30 

775 panel_importance = 1 

776 NEWS_LIMIT = getattr( 

777 settings, 

778 'DISTRO_TRACKER_NEWS_PANEL_LIMIT', 

779 _DEFAULT_NEWS_LIMIT) 

780 

781 template_name = 'core/panels/news.html' 

782 title = 'news' 

783 

784 @cached_property 

785 def context(self): 

786 news = News.objects.prefetch_related('signed_by') 

787 news = news.filter(package=self.package).order_by('-datetime_created') 

788 news = list(news[:self.NEWS_LIMIT]) 

789 more_pages = len(news) == self.NEWS_LIMIT 

790 return { 

791 'news': news, 

792 'has_more': more_pages 

793 } 

794 

795 @property 

796 def has_content(self): 

797 return bool(self.context['news']) 

798 

799 

800class BugsPanel(BasePanel, BugDisplayManagerMixin): 

801 

802 """ 

803 The panel displays bug statistics for the package. 

804 

805 This panel is highly customizable to make sure that Distro Tracker can be 

806 integrated with any bug tracker. 

807 

808 The default for the package is to display the bug count for all bug 

809 categories found in the 

810 :class:`PackageBugStats <distro_tracker.core.models.PackageBugStats>` 

811 instance which corresponds to the package. The sum of all bugs from 

812 all categories is also displayed as the first row of the panel. 

813 Such behavior is defined by :class:`BugDisplayManager 

814 <distro_tracker.core.models.BugDisplayManager>` class. 

815 

816 A vendor may provide a custom way of displaying bugs data in 

817 bugs panel by implementing :func:`get_bug_display_manager_class 

818 <distro_tracker.vendor.skeleton.rules.get_bug_display_manager_class>` 

819 function. This is useful if, for example, the vendor does 

820 not want to display the count of all bug categories. 

821 Refer to the function's documentation for the format of the return value. 

822 

823 This customization should be used only by vendors whose bug statistics have 

824 a significantly different format than the expected ``category: count`` 

825 format. 

826 """ 

827 position = 'right' 

828 title = 'bugs' 

829 panel_importance = 1 

830 

831 @property 

832 def template_name(self): 

833 return self.bug_manager.panel_template_name 

834 

835 @cached_property 

836 def context(self): 

837 return self.bug_manager.panel_context(self.package) 

838 

839 @property 

840 def has_content(self): 

841 return bool(self.context) 

842 

843 

844class ActionNeededPanel(BasePanel): 

845 

846 """ 

847 The panel displays a list of 

848 :class:`ActionItem <distro_tracker.core.models.ActionItem>` 

849 model instances which are associated with the package. 

850 

851 This means that all other modules can create action items which are 

852 displayed for a package in this panel by creating instances of that class. 

853 """ 

854 title = 'action needed' 

855 template_name = 'core/panels/action-needed.html' 

856 panel_importance = 5 

857 position = 'center' 

858 

859 @cached_property 

860 def context(self): 

861 action_items = ActionItem.objects.filter(package=self.package) 

862 action_items = action_items.order_by( 

863 '-severity', '-last_updated_timestamp') 

864 

865 return { 

866 'items': action_items, 

867 } 

868 

869 @property 

870 def has_content(self): 

871 return bool(self.context['items']) 

872 

873 

874class DeadPackageWarningPanel(BasePanel): 

875 """ 

876 The panel displays a warning when the package has been dropped 

877 from development repositories, and another one when the package no longer 

878 exists in any repository. 

879 """ 

880 title = 'package is gone' 

881 template_name = 'core/panels/package-is-gone.html' 

882 panel_importance = 9 

883 position = 'center' 

884 

885 @property 

886 def has_content(self): 

887 if isinstance(self.package, SourcePackageName): 

888 for repo in self.package.repositories: 

889 if repo.is_development_repository(): 

890 return False 

891 return True 

892 elif isinstance(self.package, PseudoPackageName): 

893 return False 

894 else: 

895 return True 

896 

897 @cached_property 

898 def context(self): 

899 if isinstance(self.package, SourcePackageName): 

900 disappeared = len(self.package.repositories) == 0 

901 else: 

902 disappeared = True 

903 return { 

904 'disappeared': disappeared, 

905 'removals_url': getattr(settings, 'DISTRO_TRACKER_REMOVALS_URL', 

906 ''), 

907 }