1# -*- coding: utf-8 -*- 

2 

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.""" 

13 

14from urllib.parse import quote, quote_plus 

15 

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 

21 

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 BuildLogCheckStats, 

37 LintianStats, 

38 PackageExcuses, 

39 UbuntuPackage 

40) 

41 

42 

43class LintianLink(LinksPanel.ItemProvider): 

44 """ 

45 If there are any known lintian issues for the package, provides a link to 

46 the lintian page. 

47 """ 

48 def get_panel_items(self): 

49 try: 

50 lintian_stats = self.package.lintian_stats 

51 except LintianStats.DoesNotExist: 

52 return [] 

53 

54 if sum(lintian_stats.stats.values()): 

55 url = lintian_stats.get_lintian_url() 

56 return [ 

57 TemplatePanelItem('debian/lintian-link.html', { 

58 'lintian_stats': lintian_stats.stats, 

59 'lintian_url': url, 

60 }) 

61 ] 

62 

63 return [] 

64 

65 

66class BuildLogCheckLinks(LinksPanel.ItemProvider): 

67 def get_experimental_context(self): 

68 has_experimental = False 

69 experimental_repo = get_or_none(Repository, suite='experimental') 

70 if experimental_repo: 70 ↛ 71line 70 didn't jump to line 71, because the condition on line 70 was never true

71 has_experimental = experimental_repo.has_source_package_name( 

72 self.package.name) 

73 return {'has_experimental': has_experimental} 

74 

75 def get_logcheck_context(self): 

76 try: 

77 self.package.build_logcheck_stats 

78 has_checks = True 

79 except BuildLogCheckStats.DoesNotExist: 

80 has_checks = False 

81 logcheck_url = \ 

82 "https://qa.debian.org/bls/packages/{hash}/{pkg}.html".format( 

83 hash=quote(self.package.name[0], safe=""), 

84 pkg=quote(self.package.name, safe="")) 

85 return {'has_checks': has_checks, 'logcheck_url': logcheck_url} 

86 

87 def get_reproducible_context(self): 

88 try: 

89 infos = self.package.data.get(key='reproducibility') 

90 has_reproducibility = True 

91 reproducibility_status = infos.value['reproducibility'] 

92 except PackageData.DoesNotExist: 

93 has_reproducibility = False 

94 reproducibility_status = None 

95 reproducibility_url = \ 

96 "https://tests.reproducible-builds.org/debian/rb-pkg/{}.html" 

97 reproducibility_url = reproducibility_url.format( 

98 quote(self.package.name, safe="")) 

99 return {'has_reproducibility': has_reproducibility, 

100 'reproducibility_url': reproducibility_url, 

101 'reproducibility_status': reproducibility_status, 

102 } 

103 

104 def get_debcheck_context(self): 

105 # display debcheck link if there is at least one kind of problem 

106 has_debcheck = False 

107 for k in ['dependency_satisfaction', 

108 'builddependency_satisfaction']: 

109 try: 

110 self.package.data.get(key=k) 

111 has_debcheck = True 

112 break 

113 except PackageData.DoesNotExist: 

114 pass 

115 

116 debcheck_url = \ 

117 "https://qa.debian.org/dose/debcheck/src" \ 

118 "/{}.html".format(quote(self.package.name, safe="")) 

119 return {'has_debcheck': has_debcheck, 'debcheck_url': debcheck_url} 

120 

121 def get_crossqa_context(self): 

122 try: 

123 has_crossqa = False 

124 arches = self.package.data.get( 

125 key='general').value.get('architectures') 

126 # might be wrong due to https://bugs.debian.org/920024 

127 if arches is not None and arches != ['all']: 127 ↛ 128line 127 didn't jump to line 128, because the condition on line 127 was never true

128 has_crossqa = True 

129 except PackageData.DoesNotExist: 

130 has_crossqa = False 

131 return {'has_crossqa': has_crossqa} 

132 

133 def get_panel_items(self): 

134 if not isinstance(self.package, SourcePackageName): 

135 # Only source packages can have build log check info 

136 return 

137 

138 query_string = urlencode({'p': self.package.name}) 

139 

140 return [ 

141 TemplatePanelItem('debian/logcheck-links.html', { 

142 'package_name': quote(self.package.name), 

143 'package_query_string': query_string, 

144 **self.get_logcheck_context(), 

145 **self.get_reproducible_context(), 

146 **self.get_experimental_context(), 

147 **self.get_debcheck_context(), 

148 **self.get_crossqa_context(), 

149 }) 

150 ] 

151 

152 

153class PopconLink(LinksPanel.ItemProvider): 

154 POPCON_URL = 'https://qa.debian.org/popcon.php?package={package}' 

155 

156 def get_panel_items(self): 

157 if not isinstance(self.package, SourcePackageName): 

158 return 

159 

160 return [ 

161 LinksPanel.SimpleLinkItem( 

162 'popcon', 

163 self.POPCON_URL.format( 

164 package=quote_plus(self.package.name))) 

165 ] 

166 

167 

168class SourceCodeSearchLinks(LinksPanel.ItemProvider): 

169 """ 

170 Add links to sources.debian.org source code browser and the 

171 codesearch.debian.net code search (if the package is found in unstable). 

172 """ 

173 #: A list of repositories that cause the sources.debian.org link to be 

174 #: displayed if the package is found in one of them. 

175 ALLOWED_REPOSITORIES = ( 

176 'unstable', 

177 'experimental', 

178 'testing', 

179 'stable', 

180 'oldstable', 

181 ) 

182 SOURCES_URL_TEMPLATE = 'https://sources.debian.org/src/{package}/{suite}/' 

183 SEARCH_FORM_TEMPLATE = ( 

184 '<form class="code-search-form"' 

185 ' action="' + reverse('dtracker-code-search') + '"' 

186 ' method="get" target="_blank">' 

187 '<input type="hidden" name="package" value="{package}">' 

188 '<input type="search" name="query" placeholder="search source code">' 

189 '</form>') 

190 

191 def get_panel_items(self): 

192 if not isinstance(self.package, SourcePackageName): 

193 # Only source packages can have these links 

194 return 

195 

196 repositories = [repo.suite for repo in self.package.repositories] + \ 

197 [repo.codename for repo in self.package.repositories] 

198 links = [] 

199 for allowed_repo in self.ALLOWED_REPOSITORIES: 

200 if allowed_repo in repositories: 

201 links.append(LinksPanel.SimpleLinkItem( 

202 'browse source code', 

203 self.SOURCES_URL_TEMPLATE.format( 

204 package=quote(self.package.name, safe=""), 

205 suite=quote(allowed_repo, safe="")))) 

206 break 

207 

208 if 'unstable' in repositories: 

209 # Add a search form 

210 links.append(HtmlPanelItem(self.SEARCH_FORM_TEMPLATE.format( 

211 package=self.package.name))) 

212 

213 return links 

214 

215 

216class DebtagsLink(LinksPanel.ItemProvider): 

217 """ 

218 Add a link to debtags editor. 

219 """ 

220 SOURCES_URL_TEMPLATE = \ 

221 'https://debtags.debian.org/rep/todo/maint/{maint}#{package}' 

222 

223 def get_panel_items(self): 

224 if not isinstance(self.package, SourcePackageName): 

225 return 

226 try: 

227 infos = self.package.data.get(key='general') 

228 except PackageData.DoesNotExist: 

229 return 

230 maintainer = infos.value['maintainer']['email'] 

231 return [ 

232 LinksPanel.SimpleLinkItem( 

233 'edit tags', 

234 self.SOURCES_URL_TEMPLATE.format( 

235 package=quote(self.package.name, safe=""), 

236 maint=quote(maintainer, safe="")) 

237 ) 

238 ] 

239 

240 

241class RepologyLink(LinksPanel.ItemProvider): 

242 """ 

243 Add a link to the Repology service. 

244 """ 

245 ALLOWED_REPOSITORIES = ( 

246 'unstable', 

247 'experimental', 

248 'testing', 

249 'stable-backports', 

250 'stable', 

251 'oldstable', 

252 ) 

253 

254 def get_panel_items(self): 

255 if not isinstance(self.package, SourcePackageName): 

256 # Only source packages can have these links 

257 return 

258 

259 suite = None 

260 repos = [repo.suite for repo in self.package.repositories] 

261 for repo in self.ALLOWED_REPOSITORIES: 

262 if repo in repos: 

263 suite = repo.replace('-', '_') 

264 break 

265 if suite is None: 

266 return 

267 return [ 

268 LinksPanel.SimpleLinkItem( 

269 'other distros', 

270 RepologyPackagesUrl( 

271 'debian_{suite}'.format(suite=suite), 

272 self.package.name 

273 ), 

274 'provided by Repology' 

275 ) 

276 ] 

277 

278 

279class SecurityTrackerLink(LinksPanel.ItemProvider): 

280 """ 

281 Add a link to the security tracker. 

282 """ 

283 URL_TEMPLATE = \ 

284 'https://security-tracker.debian.org/tracker/source-package/{package}' 

285 

286 def get_panel_items(self): 

287 if self.package.data.filter(key='debian-security').count() == 0: 287 ↛ 289line 287 didn't jump to line 289, because the condition on line 287 was never false

288 return 

289 return [ 

290 LinksPanel.SimpleLinkItem( 

291 'security tracker', 

292 self.URL_TEMPLATE.format(package=self.package.name) 

293 ) 

294 ] 

295 

296 

297class ScreenshotsLink(LinksPanel.ItemProvider): 

298 """ 

299 Add a link to screenshots.debian.net 

300 """ 

301 SOURCES_URL_TEMPLATE = \ 

302 'https://screenshots.debian.net/package/{package}' 

303 

304 def get_panel_items(self): 

305 if not isinstance(self.package, SourcePackageName): 

306 return 

307 try: 

308 infos = self.package.data.get(key='screenshots') 

309 except PackageData.DoesNotExist: 

310 return 

311 if infos.value['screenshots'] == 'true': 311 ↛ 320line 311 didn't jump to line 320, because the condition on line 311 was never false

312 return [ 

313 LinksPanel.SimpleLinkItem( 

314 'screenshots', 

315 self.SOURCES_URL_TEMPLATE.format( 

316 package=quote(self.package.name, safe="")) 

317 ) 

318 ] 

319 else: 

320 return 

321 

322 

323class TransitionsPanel(BasePanel): 

324 template_name = 'debian/transitions-panel.html' 

325 panel_importance = 2 

326 position = 'center' 

327 title = 'testing migrations' 

328 

329 @cached_property 

330 def context(self): 

331 try: 

332 excuses = self.package.excuses.excuses 

333 except PackageExcuses.DoesNotExist: 

334 excuses = None 

335 if excuses: 335 ↛ 336line 335 didn't jump to line 336, because the condition on line 335 was never true

336 excuses = [mark_safe(excuse) for excuse in excuses] 

337 return { 

338 'transitions': self.package.package_transitions.all(), 

339 'excuses': excuses, 

340 'package_name': self.package.name, 

341 } 

342 

343 @property 

344 def has_content(self): 

345 return bool(self.context['transitions']) or \ 

346 bool(self.context['excuses']) 

347 

348 

349class UbuntuPanel(BasePanel): 

350 template_name = 'debian/ubuntu-panel.html' 

351 position = 'right' 

352 title = 'ubuntu' 

353 

354 @cached_property 

355 def context(self): 

356 try: 

357 ubuntu_package = self.package.ubuntu_package 

358 except UbuntuPackage.DoesNotExist: 

359 return 

360 

361 return { 

362 'ubuntu_package': ubuntu_package, 

363 } 

364 

365 @property 

366 def has_content(self): 

367 return bool(self.context) 

368 

369 

370class BackToOldPTS(BasePanel): 

371 """ 

372 Display a message to users of the old PTS to encourage them to file bugs 

373 about issues that they discover and also to offer them a link back to the 

374 old PTS in case they need it. 

375 """ 

376 template_name = 'debian/back-to-old-pts.html' 

377 position = 'center' 

378 title = 'About the new package tracker' 

379 panel_importance = 100 

380 

381 @cached_property 

382 def context(self): 

383 return { 

384 'package': self.package.name 

385 } 

386 

387 @property 

388 def has_content(self): 

389 return "packages.qa.debian.org" in \ 

390 force_str(self.request.META.get('HTTP_REFERER', ''), 

391 encoding='latin1', errors='replace') 

392 

393 

394class Dl10nLinks(LinksPanel.ItemProvider): 

395 def get_panel_items(self): 

396 if not isinstance(self.package, SourcePackageName): 

397 return 

398 

399 try: 

400 dl10n_stats = self.package.data.get(key='dl10n').value 

401 except PackageData.DoesNotExist: 

402 return 

403 

404 return [ 

405 TemplatePanelItem('debian/dl10n-links.html', { 

406 'dl10n_stats': dl10n_stats, 

407 }) 

408 ] 

409 

410 

411class DebianPatchesLink(LinksPanel.ItemProvider): 

412 def get_panel_items(self): 

413 try: 

414 data = self.package.data.get(key='debian-patches').value 

415 except PackageData.DoesNotExist: 

416 return 

417 

418 count = data.get('patches', 0) 

419 if count == 0 or count is None: 

420 return 

421 

422 link_title = f'{count} patch' 

423 if count > 1: 423 ↛ 425line 423 didn't jump to line 425, because the condition on line 423 was never false

424 link_title += 'es' 

425 link_title += ' in debian/patches' 

426 

427 return [ 

428 LinksPanel.SimpleLinkItem('debian patches', data.get('url'), 

429 title=link_title), 

430 ]