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
15from debian.debian_support import AptPkgVersion
17from django.conf import settings
18from django.utils.functional import cached_property
19from django.utils.safestring import mark_safe
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
38logger = logging.getLogger(__name__)
41class BasePanel(metaclass=PluginRegistry):
43 """
44 A base class representing panels which are displayed on a package page.
46 To include a panel on the package page, users only need to create a
47 subclass and implement the necessary properties and methods.
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 )
61 def __init__(self, package, request):
62 self.package = package
63 self.request = request
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 {}
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'
83 @property
84 def title(self):
85 """
86 The title of the panel.
87 """
88 return ''
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
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
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
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
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.
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
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)
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 })
157class GeneralInformationPanel(BasePanel):
159 """
160 This panel displays general information regarding a package.
162 - name
163 - component
164 - version (in the default repository)
165 - maintainer
166 - uploaders
167 - architectures
168 - standards version
169 - VCS
171 Several vendor-specific functions can be implemented which augment this
172 panel:
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'
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)
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 )
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
200 for uploader in uploaders:
201 uploader['archive_url'] = (
202 self._get_archive_url_info(uploader['email'])
203 )
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
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)
243 return general
245 @property
246 def has_content(self):
247 return bool(self.context)
250class VersionsInformationPanel(BasePanel):
252 """
253 This panel displays the versions of the package in each of the repositories
254 it is found in.
256 Several vendor-specific functions can be implemented which augment this
257 panel:
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'
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
276 context = {}
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
294 context['version_info'] = version_info
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
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
310 return context
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)))
318class VersionedLinks(BasePanel):
320 """
321 A panel displaying links specific for source package versions.
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'
331 class LinkProvider(metaclass=PluginRegistry):
333 """
334 A base class for classes which should provide a list of version
335 specific links.
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 = []
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.
359 :type package: :class:`SourcePackage
360 <distro_tracker.core.models.SourcePackage>`
361 :type icon_index: int
363 :rtype: :class:`string` or ``None``
364 """
365 return None
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.
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.
377 If no link can be given for some icon, ``None`` should be put
378 instead.
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.
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>`
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 ]
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 ]
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 ]
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 )
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 })
454 return versioned_links
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)
463class DscLinkProvider(VersionedLinks.LinkProvider):
464 icons = [
465 octicon('desktop-download',
466 '.dsc, use dget on this link to retrieve source package'),
467 ]
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
476class BinariesInformationPanel(BasePanel, BugDisplayManagerMixin):
478 """
479 This panel displays a list of binary package names which a given source
480 package produces.
482 If there are existing bug statistics for some of the binary packages, a
483 list of bug counts is also displayed.
485 If implemented, the following functions can augment the information of
486 this panel:
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'
504 def _get_binary_bug_stats(self, binary_name):
505 bug_stats = self.bug_manager.get_binary_bug_stats(binary_name)
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 }
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
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
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
554 return binaries
556 @property
557 def has_content(self):
558 return bool(self.context)
561class PanelItem(object):
563 """
564 The base class for all items embeddable in panels.
566 Lets the users define the panel item's content in two ways:
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
581class TemplatePanelItem(PanelItem):
583 """
584 A subclass of :class:`PanelItem` which gives a more convenient interface
585 for defining items rendered by a template + context.
586 """
588 def __init__(self, template_name, context=None):
589 self.template_name = template_name
590 self.context = context
593class HtmlPanelItem(PanelItem):
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 """
601 def __init__(self, html):
602 self._html = mark_safe(html)
604 @property
605 def html_output(self):
606 return self._html
609class PanelItemProvider(metaclass=PluginRegistry):
611 """
612 A base class for classes which produce :class:`PanelItem` instances.
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.
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
637 def __init__(self, package):
638 self.package = package
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 []
648class ListPanelMeta(PluginRegistry):
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 )
664class ListPanel(BasePanel, metaclass=ListPanelMeta):
666 """
667 The base class for panels which would like to present an extensible list of
668 items.
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.
675 Clients can add items to the panel by implementing a subclass of the
676 :class:`ListPanel.ItemProvider` class.
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'
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
704 @cached_property
705 def context(self):
706 return {
707 'items': self.get_items()
708 }
710 @property
711 def has_content(self):
712 return bool(self.context['items'])
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()
720class LinksPanel(ListPanel):
722 """
723 This panel displays a list of important links for a given source package.
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'
731 class SimpleLinkItem(HtmlPanelItem):
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>'
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)
750class GeneralInfoLinkPanelItems(LinksPanel.ItemProvider):
752 """
753 Provides the :class:`LinksPanel` with links derived from general package
754 information.
756 For now, this is only the homepage of the package, if available.
757 """
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
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)
781 template_name = 'core/panels/news.html'
782 title = 'news'
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 }
795 @property
796 def has_content(self):
797 return bool(self.context['news'])
800class BugsPanel(BasePanel, BugDisplayManagerMixin):
802 """
803 The panel displays bug statistics for the package.
805 This panel is highly customizable to make sure that Distro Tracker can be
806 integrated with any bug tracker.
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.
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.
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
831 @property
832 def template_name(self):
833 return self.bug_manager.panel_template_name
835 @cached_property
836 def context(self):
837 return self.bug_manager.panel_context(self.package)
839 @property
840 def has_content(self):
841 return bool(self.context)
844class ActionNeededPanel(BasePanel):
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.
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'
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')
865 return {
866 'items': action_items,
867 }
869 @property
870 def has_content(self):
871 return bool(self.context['items'])
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'
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
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 }