Coverage for distro_tracker/vendor/debian/models.py: 79%

132 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2025-09-06 20:40 +0000

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

2 

3# Copyright 2013 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 

13""" 

14Debian-specific models. 

15""" 

16 

17from django.core.exceptions import ObjectDoesNotExist 

18from django.db import models 

19from django.utils.http import urlencode 

20 

21from distro_tracker.core.models import ( 

22 BinaryPackageBugStats, 

23 BugDisplayManager, 

24 PackageBugStats, 

25 PackageName, 

26 SourcePackageName, 

27) 

28from distro_tracker.core.utils import SpaceDelimitedTextField, get_or_none 

29from distro_tracker.core.utils.packages import package_hashdir 

30 

31 

32class DebianContributor(models.Model): 

33 """ 

34 Model containing additional Debian-specific information about contributors. 

35 """ 

36 email = models.OneToOneField('django_email_accounts.UserEmail', 

37 on_delete=models.CASCADE) 

38 agree_with_low_threshold_nmu = models.BooleanField(default=False) 

39 is_debian_maintainer = models.BooleanField(default=False) 

40 allowed_packages = SpaceDelimitedTextField(blank=True) 

41 

42 def __str__(self): 

43 return 'Debian contributor <{email}>'.format(email=self.email) 

44 

45 

46class LintianStats(models.Model): 

47 """ 

48 Model for lintian stats of packages. 

49 """ 

50 package = models.OneToOneField(PackageName, related_name='lintian_stats', 

51 on_delete=models.CASCADE) 

52 stats = models.JSONField(default=dict) 

53 

54 def __str__(self): 

55 return 'Lintian stats for package {package}'.format( 

56 package=self.package) 

57 

58 def get_lintian_url(self): 

59 """ 

60 Returns the lintian URL for the package matching the 

61 :class:`LintianStats 

62 <distro_tracker.vendor.debian.models.LintianStats>`. 

63 """ 

64 package = get_or_none(SourcePackageName, pk=self.package.pk) 

65 if not package: 65 ↛ 66line 65 didn't jump to line 66, because the condition on line 65 was never true

66 return '' 

67 

68 return ( 

69 'https://udd.debian.org/lintian/?packages={pkg}'.format( 

70 pkg=self.package) 

71 ) 

72 

73 

74class PackageTransition(models.Model): 

75 package = models.ForeignKey(PackageName, related_name='package_transitions', 

76 on_delete=models.CASCADE) 

77 transition_name = models.CharField(max_length=120) 

78 status = models.CharField(max_length=50, blank=True, null=True) 

79 reject = models.BooleanField(default=False) 

80 

81 def __str__(self): 

82 return "Transition {name} ({status}) for package {pkg}".format( 

83 name=self.transition_name, status=self.status, pkg=self.package) 

84 

85 

86class PackageExcuses(models.Model): 

87 package = models.OneToOneField(PackageName, related_name='excuses', 

88 on_delete=models.CASCADE) 

89 excuses = models.JSONField(default=dict) 

90 

91 def __str__(self): 

92 return "Excuses for the package {pkg}".format(pkg=self.package) 

93 

94 

95class UbuntuPackage(models.Model): 

96 package = models.OneToOneField( 

97 PackageName, 

98 related_name='ubuntu_package', 

99 on_delete=models.CASCADE) 

100 version = models.TextField(max_length=100) 

101 bugs = models.JSONField(null=True) 

102 patch_diff = models.JSONField(null=True) 

103 

104 def __str__(self): 

105 return "Ubuntu package info for {pkg}".format(pkg=self.package) 

106 

107 

108class DebianBugDisplayManager(BugDisplayManager): 

109 table_field_template_name = 'debian/package-table-fields/bugs.html' 

110 panel_template_name = 'debian/bugs.html' 

111 # Map category names to their bug panel display names and descriptions 

112 category_descriptions = { 

113 'rc': { 

114 'display_name': 'RC', 

115 'description': 'Release Critical', 

116 }, 

117 'normal': { 

118 'display_name': 'I&N', 

119 'description': 'Important and Normal', 

120 }, 

121 'wishlist': { 

122 'display_name': 'M&W', 

123 'description': 'Minor and Wishlist', 

124 }, 

125 'fixed': { 

126 'display_name': 'F&P', 

127 'description': 'Fixed and Pending', 

128 }, 

129 'patch': { 

130 'display_name': 'patch', 

131 'description': 'Patch', 

132 }, 

133 'help': { 

134 'display_name': 'help', 

135 'description': 'Help needed', 

136 }, 

137 'newcomer': { 

138 'display_name': 'NC', 

139 'description': 'newcomer', 

140 'link': 'https://wiki.debian.org/BTS/NewcomerTag', 

141 } 

142 } 

143 

144 def get_bug_tracker_url(self, package_name, package_type, category_name): 

145 """ 

146 Returns a URL to the BTS for the given package for the given bug 

147 category name. 

148 

149 The following categories are recognized for Debian's implementation: 

150 

151 - ``all`` - all bugs for the package 

152 - ``all-merged`` - all bugs, including the merged ones 

153 - ``rc`` - release critical bugs 

154 - ``rc-merged`` - release critical bugs, including the merged ones 

155 - ``normal`` - bugs tagged as normal and important 

156 - ``normal`` - bugs tagged as normal and important, including the merged 

157 ones 

158 - ``wishlist`` - bugs tagged as wishlist and minor 

159 - ``wishlist-merged`` - bugs tagged as wishlist and minor, including the 

160 merged ones 

161 - ``fixed`` - bugs tagged as fixed and pending 

162 - ``fixed-merged`` - bugs tagged as fixed and pending, including the 

163 merged ones 

164 

165 :param package_name: The name of the package for which the BTS link 

166 should be provided. 

167 :param package_type: The type of the package for which the BTS link 

168 should be provided. For Debian this is one of: ``source``, 

169 ``pseudo``, ``binary``. 

170 :param category_name: The name of the bug category for which the BTS 

171 link should be provided. It is one of the categories listed above. 

172 

173 :rtype: :class:`string` or ``None`` if there is no BTS bug for the given 

174 category. 

175 """ 

176 URL_PARAMETERS = { 

177 'all': ( 

178 ('repeatmerged', 'no'), 

179 ), 

180 'rc': ( 

181 ('archive', 'no'), 

182 ('pend-exc', 'pending-fixed'), 

183 ('pend-exc', 'fixed'), 

184 ('pend-exc', 'done'), 

185 ('sev-inc', 'critical'), 

186 ('sev-inc', 'grave'), 

187 ('sev-inc', 'serious'), 

188 ('repeatmerged', 'no'), 

189 ), 

190 'normal': ( 

191 ('archive', 'no'), 

192 ('pend-exc', 'pending-fixed'), 

193 ('pend-exc', 'fixed'), 

194 ('pend-exc', 'done'), 

195 ('sev-inc', 'important'), 

196 ('sev-inc', 'normal'), 

197 ('repeatmerged', 'no'), 

198 ), 

199 'wishlist': ( 

200 ('archive', 'no'), 

201 ('pend-exc', 'pending-fixed'), 

202 ('pend-exc', 'fixed'), 

203 ('pend-exc', 'done'), 

204 ('sev-inc', 'minor'), 

205 ('sev-inc', 'wishlist'), 

206 ('repeatmerged', 'no'), 

207 ), 

208 'fixed': ( 

209 ('archive', 'no'), 

210 ('pend-inc', 'pending-fixed'), 

211 ('pend-inc', 'fixed'), 

212 ('repeatmerged', 'no'), 

213 ), 

214 'patch': ( 

215 ('include', 'tags:patch'), 

216 ('exclude', 'tags:pending'), 

217 ('pend-exc', 'done'), 

218 ('repeatmerged', 'no'), 

219 ), 

220 'help': ( 

221 ('tag', 'help'), 

222 ('pend-exc', 'pending-fixed'), 

223 ('pend-exc', 'fixed'), 

224 ('pend-exc', 'done'), 

225 ), 

226 'newcomer': ( 

227 ('tag', 'newcomer'), 

228 ('pend-exc', 'pending-fixed'), 

229 ('pend-exc', 'fixed'), 

230 ('pend-exc', 'done'), 

231 ), 

232 'all-merged': ( 

233 ('repeatmerged', 'yes'), 

234 ), 

235 'rc-merged': ( 

236 ('archive', 'no'), 

237 ('pend-exc', 'pending-fixed'), 

238 ('pend-exc', 'fixed'), 

239 ('pend-exc', 'done'), 

240 ('sev-inc', 'critical'), 

241 ('sev-inc', 'grave'), 

242 ('sev-inc', 'serious'), 

243 ('repeatmerged', 'yes'), 

244 ), 

245 'normal-merged': ( 

246 ('archive', 'no'), 

247 ('pend-exc', 'pending-fixed'), 

248 ('pend-exc', 'fixed'), 

249 ('pend-exc', 'done'), 

250 ('sev-inc', 'important'), 

251 ('sev-inc', 'normal'), 

252 ('repeatmerged', 'yes'), 

253 ), 

254 'wishlist-merged': ( 

255 ('archive', 'no'), 

256 ('pend-exc', 'pending-fixed'), 

257 ('pend-exc', 'fixed'), 

258 ('pend-exc', 'done'), 

259 ('sev-inc', 'minor'), 

260 ('sev-inc', 'wishlist'), 

261 ('repeatmerged', 'yes'), 

262 ), 

263 'fixed-merged': ( 

264 ('archive', 'no'), 

265 ('pend-inc', 'pending-fixed'), 

266 ('pend-inc', 'fixed'), 

267 ('repeatmerged', 'yes'), 

268 ), 

269 'patch-merged': ( 

270 ('include', 'tags:patch'), 

271 ('exclude', 'tags:pending'), 

272 ('pend-exc', 'done'), 

273 ('repeatmerged', 'yes'), 

274 ), 

275 } 

276 if category_name not in URL_PARAMETERS: 276 ↛ 277line 276 didn't jump to line 277, because the condition on line 276 was never true

277 return 

278 

279 domain = 'https://bugs.debian.org/' 

280 query_parameters = URL_PARAMETERS[category_name] 

281 

282 if package_type == 'source': 282 ↛ 284line 282 didn't jump to line 284, because the condition on line 282 was never false

283 query_parameters += (('src', package_name),) 

284 elif package_type == 'binary': 

285 if category_name == 'all': 

286 # All bugs for a binary package don't follow the same pattern as 

287 # the rest of the URLs. 

288 return domain + package_name 

289 query_parameters += (('which', 'pkg'),) 

290 query_parameters += (('data', package_name),) 

291 

292 return ( 

293 domain + 

294 'cgi-bin/pkgreport.cgi?' + 

295 urlencode(query_parameters) 

296 ) 

297 

298 def get_bugs_categories_list(self, stats, package): 

299 # Some bug categories should not be included in the count. 

300 exclude_from_count = ('patch', 'help', 'newcomer') 

301 

302 categories = [] 

303 total, total_merged = 0, 0 

304 # From all known bug stats, extract only the ones relevant for the panel 

305 for category in stats: 

306 category_name = category['category_name'] 

307 if category_name not in self.category_descriptions.keys(): 307 ↛ 308line 307 didn't jump to line 308, because the condition on line 307 was never true

308 continue 

309 # Add main bug count 

310 category_stats = { 

311 'category_name': category['category_name'], 

312 'bug_count': category['bug_count'], 

313 } 

314 # Add merged bug count 

315 if 'merged_count' in category: 315 ↛ 321line 315 didn't jump to line 321, because the condition on line 315 was never false

316 if category['merged_count'] != category['bug_count']: 

317 category_stats['merged'] = { 

318 'bug_count': category['merged_count'], 

319 } 

320 # Add descriptions 

321 category_stats.update(self.category_descriptions[category_name]) 

322 categories.append(category_stats) 

323 

324 # Keep a running total of all and all-merged bugs 

325 if category_name not in exclude_from_count: 

326 total += category['bug_count'] 

327 total_merged += category.get('merged_count', 0) 

328 

329 # Add another "category" with the bug totals. 

330 all_category = { 

331 'category_name': 'all', 

332 'display_name': 'all', 

333 'bug_count': total, 

334 } 

335 if total != total_merged: 

336 all_category['merged'] = { 

337 'bug_count': total_merged, 

338 } 

339 # The totals are the first displayed row. 

340 categories.insert(0, all_category) 

341 

342 # Add URLs for all categories 

343 for category in categories: 

344 # URL for the non-merged category 

345 url = self.get_bug_tracker_url( 

346 package.name, 'source', category['category_name']) 

347 category['url'] = url 

348 

349 # URL for the merged category 

350 if 'merged' in category: 

351 url_merged = self.get_bug_tracker_url( 

352 package.name, 'source', 

353 category['category_name'] + '-merged' 

354 ) 

355 category['merged']['url'] = url_merged 

356 

357 return categories 

358 

359 def table_field_context(self, package): 

360 """ 

361 :returns: The context data for package's bug stats with RC bugs data to 

362 be highlighted in the template, as well as providing proper links 

363 for Debian BTS. 

364 """ 

365 try: 

366 stats = package.bug_stats.stats 

367 except ObjectDoesNotExist: 

368 stats = [] 

369 

370 data = {} 

371 data['bugs'] = self.get_bugs_categories_list(stats, package) 

372 

373 total = 0 

374 for category in data['bugs']: 374 ↛ 378line 374 didn't jump to line 378, because the loop on line 374 didn't complete

375 if category['category_name'] == 'all': 375 ↛ 374line 375 didn't jump to line 374, because the condition on line 375 was never false

376 total = category['bug_count'] 

377 break 

378 data['all'] = total 

379 data['bts_url'] = self.get_bug_tracker_url( 

380 package.name, 'source', 'all') 

381 

382 # Highlights RC bugs and set text color based on the bug category 

383 data['text_color'] = 'text-default' 

384 for bug in data['bugs']: 

385 if bug['category_name'] == 'rc' and bug['bug_count'] > 0: 

386 data['text_color'] = 'text-danger' 

387 data['rc_bugs'] = bug['bug_count'] 

388 elif bug['category_name'] == 'normal' and bug['bug_count'] > 0: 

389 if data['text_color'] != 'text-danger': 

390 data['text_color'] = 'text-warning' 

391 elif bug['category_name'] == 'patch' and bug['bug_count'] > 0: 

392 if (data['text_color'] != 'text-warning' and 

393 data['text_color'] != 'text-danger'): 

394 data['text_color'] = 'text-info' 

395 return data 

396 

397 def panel_context(self, package): 

398 """ 

399 Returns bug statistics which are to be displayed in the bugs panel 

400 (:class:`BugsPanel <distro_tracker.core.panels.BugsPanel>`). 

401 

402 Debian wants to include the merged bug count for each bug category 

403 (but only if the count is different than non-merged bug count) so this 

404 function is used in conjunction with a custom bug panel template which 

405 displays this bug count in parentheses next to the non-merged count. 

406 

407 Each bug category count (merged and non-merged) is linked to a URL in 

408 the BTS which displays more information about the bugs found in that 

409 category. 

410 

411 A verbose name is included for each of the categories. 

412 

413 The function includes a URL to a bug history graph which is displayed in 

414 the rendered template. 

415 """ 

416 bug_stats = get_or_none(PackageBugStats, package=package) 

417 

418 if bug_stats: 418 ↛ 419line 418 didn't jump to line 419, because the condition on line 418 was never true

419 stats = bug_stats.stats 

420 else: 

421 stats = [] 

422 

423 categories = self.get_bugs_categories_list(stats, package) 

424 

425 # Debian also includes a custom graph of bug history 

426 graph_url = ( 

427 'https://qa.debian.org/data/bts/graphs/' 

428 '{package_hash}/{package_name}.png' 

429 ) 

430 

431 # Final context variables which are available in the template 

432 return { 

433 'categories': categories, 

434 'graph_url': graph_url.format( 

435 package_hash=package_hashdir(package.name), 

436 package_name=package.name), 

437 } 

438 

439 def get_binary_bug_stats(self, binary_name): 

440 """ 

441 Returns the bug statistics for the given binary package. 

442 

443 Debian's implementation filters out some of the stored bug category 

444 stats. It also provides a different, more verbose, display name for each 

445 of them. The included categories and their names are: 

446 

447 - rc - critical, grave serious 

448 - normal - important and normal 

449 - wishlist - wishlist and minor 

450 - fixed - pending and fixed 

451 """ 

452 stats = get_or_none(BinaryPackageBugStats, package__name=binary_name) 

453 if stats is None: 

454 return 

455 category_descriptions = { 

456 'rc': { 

457 'display_name': 'critical, grave and serious', 

458 }, 

459 'normal': { 

460 'display_name': 'important and normal', 

461 }, 

462 'wishlist': { 

463 'display_name': 'wishlist and minor', 

464 }, 

465 'fixed': { 

466 'display_name': 'pending and fixed', 

467 }, 

468 } 

469 

470 def extend_category(category, extra_parameters): 

471 category.update(extra_parameters) 

472 return category 

473 

474 # Filter the bug stats to only include some categories and add a custom 

475 # display name for each of them. 

476 return [ 

477 extend_category(category, 

478 category_descriptions[category['category_name']]) 

479 for category in stats.stats 

480 if category['category_name'] in category_descriptions.keys() 

481 ]