Coverage for distro_tracker/vendor/debian/tracker_panels.py: 90%
184 statements
« prev ^ index » next coverage.py v6.5.0, created at 2025-10-07 08:16 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2025-10-07 08:16 +0000
1# -*- coding: utf-8 -*-
3# Copyright 2013-2023 The Distro Tracker Developers
4# See the COPYRIGHT file at the top-level directory of this distribution and
5# at https://deb.li/DTAuthors
6#
7# This file is part of Distro Tracker. It is subject to the license terms
8# in the LICENSE file found in the top-level directory of this
9# distribution and at https://deb.li/DTLicense. No part of Distro Tracker,
10# including this file, may be copied, modified, propagated, or distributed
11# except according to the terms contained in the LICENSE file.
12"""Debian specific panels on the package page."""
14from urllib.parse import quote, quote_plus
16from django.urls import reverse
17from django.utils.encoding import force_str
18from django.utils.functional import cached_property
19from django.utils.http import urlencode
20from django.utils.safestring import mark_safe
22from distro_tracker.core.models import (
23 PackageData,
24 Repository,
25 SourcePackageName
26)
27from distro_tracker.core.panels import (
28 BasePanel,
29 HtmlPanelItem,
30 LinksPanel,
31 TemplatePanelItem
32)
33from distro_tracker.core.utils import get_or_none
34from distro_tracker.core.utils.urls import RepologyPackagesUrl
35from distro_tracker.vendor.debian.models import (
36 LintianStats,
37 PackageExcuses,
38 UbuntuPackage
39)
42class LintianLink(LinksPanel.ItemProvider):
43 """
44 If there are any known lintian issues for the package, provides a link to
45 the lintian page.
46 """
47 def get_panel_items(self):
48 try:
49 lintian_stats = self.package.lintian_stats
50 except LintianStats.DoesNotExist:
51 return []
53 if sum(lintian_stats.stats.values()):
54 url = lintian_stats.get_lintian_url()
55 return [
56 TemplatePanelItem('debian/lintian-link.html', {
57 'lintian_stats': lintian_stats.stats,
58 'lintian_url': url,
59 })
60 ]
62 return []
65class BuildLogCheckLinks(LinksPanel.ItemProvider):
66 def get_experimental_context(self):
67 has_experimental = False
68 experimental_repo = get_or_none(Repository, suite='experimental')
69 if experimental_repo: 69 ↛ 70line 69 didn't jump to line 70, because the condition on line 69 was never true
70 has_experimental = experimental_repo.has_source_package_name(
71 self.package.name)
72 return {'has_experimental': has_experimental}
74 def get_reproducible_context(self):
75 try:
76 infos = self.package.data.get(key='reproducibility')
77 has_reproducibility = True
78 reproducibility_status = infos.value['reproducibility']
79 except PackageData.DoesNotExist:
80 has_reproducibility = False
81 reproducibility_status = None
82 reproducibility_url = \
83 "https://tests.reproducible-builds.org/debian/rb-pkg/{}.html"
84 reproducibility_url = reproducibility_url.format(
85 quote(self.package.name, safe=""))
86 return {'has_reproducibility': has_reproducibility,
87 'reproducibility_url': reproducibility_url,
88 'reproducibility_status': reproducibility_status,
89 }
91 def get_debcheck_context(self):
92 # display debcheck link if there is at least one kind of problem
93 has_debcheck = False
94 for k in ['dependency_satisfaction',
95 'builddependency_satisfaction']:
96 try:
97 self.package.data.get(key=k)
98 has_debcheck = True
99 break
100 except PackageData.DoesNotExist:
101 pass
103 debcheck_url = \
104 "https://qa.debian.org/dose/debcheck/src" \
105 "/{}.html".format(quote(self.package.name, safe=""))
106 return {'has_debcheck': has_debcheck, 'debcheck_url': debcheck_url}
108 def get_crossqa_context(self):
109 try:
110 has_crossqa = False
111 arches = self.package.data.get(
112 key='general').value.get('architectures')
113 # might be wrong due to https://bugs.debian.org/920024
114 if arches is not None and arches != ['all']: 114 ↛ 115line 114 didn't jump to line 115, because the condition on line 114 was never true
115 has_crossqa = True
116 except PackageData.DoesNotExist:
117 has_crossqa = False
118 return {'has_crossqa': has_crossqa}
120 def get_panel_items(self):
121 if not isinstance(self.package, SourcePackageName):
122 # Only source packages can have build log check info
123 return
125 query_string = urlencode({'p': self.package.name})
127 return [
128 TemplatePanelItem('debian/logcheck-links.html', {
129 'package_name': quote(self.package.name),
130 'package_query_string': query_string,
131 **self.get_reproducible_context(),
132 **self.get_experimental_context(),
133 **self.get_debcheck_context(),
134 **self.get_crossqa_context(),
135 })
136 ]
139class PopconLink(LinksPanel.ItemProvider):
140 POPCON_URL = 'https://qa.debian.org/popcon.php?package={package}'
142 def get_panel_items(self):
143 if not isinstance(self.package, SourcePackageName):
144 return
146 return [
147 LinksPanel.SimpleLinkItem(
148 'popcon',
149 self.POPCON_URL.format(
150 package=quote_plus(self.package.name)))
151 ]
154class SourceCodeSearchLinks(LinksPanel.ItemProvider):
155 """
156 Add links to sources.debian.org source code browser and the
157 codesearch.debian.net code search (if the package is found in unstable).
158 """
159 #: A list of repositories that cause the sources.debian.org link to be
160 #: displayed if the package is found in one of them.
161 ALLOWED_REPOSITORIES = (
162 'unstable',
163 'experimental',
164 'testing',
165 'stable',
166 'oldstable',
167 )
168 SOURCES_URL_TEMPLATE = 'https://sources.debian.org/src/{package}/{suite}/'
169 SEARCH_FORM_TEMPLATE = (
170 '<form class="code-search-form"'
171 ' action="' + reverse('dtracker-code-search') + '"'
172 ' method="get" target="_blank">'
173 '<input type="hidden" name="package" value="{package}">'
174 '<input type="search" name="query" placeholder="search source code">'
175 '</form>')
177 def get_panel_items(self):
178 if not isinstance(self.package, SourcePackageName):
179 # Only source packages can have these links
180 return
182 repositories = [repo.suite for repo in self.package.repositories] + \
183 [repo.codename for repo in self.package.repositories]
184 links = []
185 for allowed_repo in self.ALLOWED_REPOSITORIES:
186 if allowed_repo in repositories:
187 links.append(LinksPanel.SimpleLinkItem(
188 'browse source code',
189 self.SOURCES_URL_TEMPLATE.format(
190 package=quote(self.package.name, safe=""),
191 suite=quote(allowed_repo, safe=""))))
192 break
194 if 'unstable' in repositories:
195 # Add a search form
196 links.append(HtmlPanelItem(self.SEARCH_FORM_TEMPLATE.format(
197 package=self.package.name)))
199 return links
202class RepologyLink(LinksPanel.ItemProvider):
203 """
204 Add a link to the Repology service.
205 """
206 ALLOWED_REPOSITORIES = (
207 'unstable',
208 'experimental',
209 'testing',
210 'stable-backports',
211 'stable',
212 'oldstable',
213 )
215 def get_panel_items(self):
216 if not isinstance(self.package, SourcePackageName):
217 # Only source packages can have these links
218 return
220 suite = None
221 repos = [repo.suite for repo in self.package.repositories]
222 for repo in self.ALLOWED_REPOSITORIES:
223 if repo in repos:
224 suite = repo.replace('-', '_')
225 break
226 if suite is None:
227 return
228 return [
229 LinksPanel.SimpleLinkItem(
230 'other distros',
231 RepologyPackagesUrl(
232 'debian_{suite}'.format(suite=suite),
233 self.package.name
234 ),
235 'provided by Repology'
236 )
237 ]
240class SecurityTrackerLink(LinksPanel.ItemProvider):
241 """
242 Add a link to the security tracker.
243 """
244 URL_TEMPLATE = \
245 'https://security-tracker.debian.org/tracker/source-package/{package}'
247 def get_panel_items(self):
248 if self.package.data.filter(key='debian-security').count() == 0: 248 ↛ 250line 248 didn't jump to line 250, because the condition on line 248 was never false
249 return
250 return [
251 LinksPanel.SimpleLinkItem(
252 'security tracker',
253 self.URL_TEMPLATE.format(package=self.package.name)
254 )
255 ]
258class ScreenshotsLink(LinksPanel.ItemProvider):
259 """
260 Add a link to screenshots.debian.net
261 """
262 SOURCES_URL_TEMPLATE = \
263 'https://screenshots.debian.net/package/{package}'
265 def get_panel_items(self):
266 if not isinstance(self.package, SourcePackageName):
267 return
268 try:
269 infos = self.package.data.get(key='screenshots')
270 except PackageData.DoesNotExist:
271 return
272 if infos.value['screenshots'] == 'true': 272 ↛ 281line 272 didn't jump to line 281, because the condition on line 272 was never false
273 return [
274 LinksPanel.SimpleLinkItem(
275 'screenshots',
276 self.SOURCES_URL_TEMPLATE.format(
277 package=quote(self.package.name, safe=""))
278 )
279 ]
280 else:
281 return
284class TransitionsPanel(BasePanel):
285 template_name = 'debian/transitions-panel.html'
286 panel_importance = 2
287 position = 'center'
288 title = 'testing migrations'
290 @cached_property
291 def context(self):
292 try:
293 excuses = self.package.excuses.excuses
294 except PackageExcuses.DoesNotExist:
295 excuses = None
296 if excuses: 296 ↛ 297line 296 didn't jump to line 297, because the condition on line 296 was never true
297 excuses = [mark_safe(excuse) for excuse in excuses]
298 return {
299 'transitions': self.package.package_transitions.all(),
300 'excuses': excuses,
301 'package_name': self.package.name,
302 }
304 @property
305 def has_content(self):
306 return bool(self.context['transitions']) or \
307 bool(self.context['excuses'])
310class UbuntuPanel(BasePanel):
311 template_name = 'debian/ubuntu-panel.html'
312 position = 'right'
313 title = 'ubuntu'
315 @cached_property
316 def context(self):
317 try:
318 ubuntu_package = self.package.ubuntu_package
319 except UbuntuPackage.DoesNotExist:
320 return
322 return {
323 'ubuntu_package': ubuntu_package,
324 }
326 @property
327 def has_content(self):
328 return bool(self.context)
331class BackToOldPTS(BasePanel):
332 """
333 Display a message to users of the old PTS to encourage them to file bugs
334 about issues that they discover and also to offer them a link back to the
335 old PTS in case they need it.
336 """
337 template_name = 'debian/back-to-old-pts.html'
338 position = 'center'
339 title = 'About the new package tracker'
340 panel_importance = 100
342 @cached_property
343 def context(self):
344 return {
345 'package': self.package.name
346 }
348 @property
349 def has_content(self):
350 return "packages.qa.debian.org" in \
351 force_str(self.request.META.get('HTTP_REFERER', ''),
352 encoding='latin1', errors='replace')
355class Dl10nLinks(LinksPanel.ItemProvider):
356 def get_panel_items(self):
357 if not isinstance(self.package, SourcePackageName):
358 return
360 try:
361 dl10n_stats = self.package.data.get(key='dl10n').value
362 except PackageData.DoesNotExist:
363 return
365 return [
366 TemplatePanelItem('debian/dl10n-links.html', {
367 'dl10n_stats': dl10n_stats,
368 })
369 ]
372class DebianPatchesLink(LinksPanel.ItemProvider):
373 def get_panel_items(self):
374 try:
375 data = self.package.data.get(key='debian-patches').value
376 except PackageData.DoesNotExist:
377 return
379 count = data.get('patches', 0)
380 if count == 0 or count is None:
381 return
383 link_title = f'{count} patch'
384 if count > 1: 384 ↛ 386line 384 didn't jump to line 386, because the condition on line 384 was never false
385 link_title += 'es'
386 link_title += ' in debian/patches'
388 return [
389 LinksPanel.SimpleLinkItem('debian patches', data.get('url'),
390 title=link_title),
391 ]