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 BuildLogCheckStats(models.Model): 

96 package = models.OneToOneField( 

97 SourcePackageName, 

98 related_name='build_logcheck_stats', 

99 on_delete=models.CASCADE) 

100 stats = models.JSONField(default=dict) 

101 

102 def __str__(self): 

103 return "Build logcheck stats for {pkg}".format(pkg=self.package) 

104 

105 

106class UbuntuPackage(models.Model): 

107 package = models.OneToOneField( 

108 PackageName, 

109 related_name='ubuntu_package', 

110 on_delete=models.CASCADE) 

111 version = models.TextField(max_length=100) 

112 bugs = models.JSONField(null=True) 

113 patch_diff = models.JSONField(null=True) 

114 

115 def __str__(self): 

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

117 

118 

119class DebianBugDisplayManager(BugDisplayManager): 

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

121 panel_template_name = 'debian/bugs.html' 

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

123 category_descriptions = { 

124 'rc': { 

125 'display_name': 'RC', 

126 'description': 'Release Critical', 

127 }, 

128 'normal': { 

129 'display_name': 'I&N', 

130 'description': 'Important and Normal', 

131 }, 

132 'wishlist': { 

133 'display_name': 'M&W', 

134 'description': 'Minor and Wishlist', 

135 }, 

136 'fixed': { 

137 'display_name': 'F&P', 

138 'description': 'Fixed and Pending', 

139 }, 

140 'patch': { 

141 'display_name': 'patch', 

142 'description': 'Patch', 

143 }, 

144 'help': { 

145 'display_name': 'help', 

146 'description': 'Help needed', 

147 }, 

148 'newcomer': { 

149 'display_name': 'NC', 

150 'description': 'newcomer', 

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

152 } 

153 } 

154 

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

156 """ 

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

158 category name. 

159 

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

161 

162 - ``all`` - all bugs for the package 

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

164 - ``rc`` - release critical bugs 

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

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

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

168 ones 

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

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

171 merged ones 

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

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

174 merged ones 

175 

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

177 should be provided. 

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

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

180 ``pseudo``, ``binary``. 

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

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

183 

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

185 category. 

186 """ 

187 URL_PARAMETERS = { 

188 'all': ( 

189 ('repeatmerged', 'no'), 

190 ), 

191 'rc': ( 

192 ('archive', 'no'), 

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

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

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

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

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

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

199 ('repeatmerged', 'no'), 

200 ), 

201 'normal': ( 

202 ('archive', 'no'), 

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

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

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

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

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

208 ('repeatmerged', 'no'), 

209 ), 

210 'wishlist': ( 

211 ('archive', 'no'), 

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

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

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

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

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

217 ('repeatmerged', 'no'), 

218 ), 

219 'fixed': ( 

220 ('archive', 'no'), 

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

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

223 ('repeatmerged', 'no'), 

224 ), 

225 'patch': ( 

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

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

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

229 ('repeatmerged', 'no'), 

230 ), 

231 'help': ( 

232 ('tag', 'help'), 

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

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

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

236 ), 

237 'newcomer': ( 

238 ('tag', 'newcomer'), 

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

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

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

242 ), 

243 'all-merged': ( 

244 ('repeatmerged', 'yes'), 

245 ), 

246 'rc-merged': ( 

247 ('archive', 'no'), 

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

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

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

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

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

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

254 ('repeatmerged', 'yes'), 

255 ), 

256 'normal-merged': ( 

257 ('archive', 'no'), 

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

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

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

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

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

263 ('repeatmerged', 'yes'), 

264 ), 

265 'wishlist-merged': ( 

266 ('archive', 'no'), 

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

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

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

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

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

272 ('repeatmerged', 'yes'), 

273 ), 

274 'fixed-merged': ( 

275 ('archive', 'no'), 

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

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

278 ('repeatmerged', 'yes'), 

279 ), 

280 'patch-merged': ( 

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

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

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

284 ('repeatmerged', 'yes'), 

285 ), 

286 } 

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

288 return 

289 

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

291 query_parameters = URL_PARAMETERS[category_name] 

292 

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

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

295 elif package_type == 'binary': 

296 if category_name == 'all': 

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

298 # the rest of the URLs. 

299 return domain + package_name 

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

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

302 

303 return ( 

304 domain + 

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

306 urlencode(query_parameters) 

307 ) 

308 

309 def get_bugs_categories_list(self, stats, package): 

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

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

312 

313 categories = [] 

314 total, total_merged = 0, 0 

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

316 for category in stats: 

317 category_name = category['category_name'] 

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

319 continue 

320 # Add main bug count 

321 category_stats = { 

322 'category_name': category['category_name'], 

323 'bug_count': category['bug_count'], 

324 } 

325 # Add merged bug count 

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

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

328 category_stats['merged'] = { 

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

330 } 

331 # Add descriptions 

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

333 categories.append(category_stats) 

334 

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

336 if category_name not in exclude_from_count: 

337 total += category['bug_count'] 

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

339 

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

341 all_category = { 

342 'category_name': 'all', 

343 'display_name': 'all', 

344 'bug_count': total, 

345 } 

346 if total != total_merged: 

347 all_category['merged'] = { 

348 'bug_count': total_merged, 

349 } 

350 # The totals are the first displayed row. 

351 categories.insert(0, all_category) 

352 

353 # Add URLs for all categories 

354 for category in categories: 

355 # URL for the non-merged category 

356 url = self.get_bug_tracker_url( 

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

358 category['url'] = url 

359 

360 # URL for the merged category 

361 if 'merged' in category: 

362 url_merged = self.get_bug_tracker_url( 

363 package.name, 'source', 

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

365 ) 

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

367 

368 return categories 

369 

370 def table_field_context(self, package): 

371 """ 

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

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

374 for Debian BTS. 

375 """ 

376 try: 

377 stats = package.bug_stats.stats 

378 except ObjectDoesNotExist: 

379 stats = [] 

380 

381 data = {} 

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

383 

384 total = 0 

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

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

387 total = category['bug_count'] 

388 break 

389 data['all'] = total 

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

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

392 

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

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

395 for bug in data['bugs']: 

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

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

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

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

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

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

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

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

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

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

406 return data 

407 

408 def panel_context(self, package): 

409 """ 

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

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

412 

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

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

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

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

417 

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

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

420 category. 

421 

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

423 

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

425 the rendered template. 

426 """ 

427 bug_stats = get_or_none(PackageBugStats, package=package) 

428 

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

430 stats = bug_stats.stats 

431 else: 

432 stats = [] 

433 

434 categories = self.get_bugs_categories_list(stats, package) 

435 

436 # Debian also includes a custom graph of bug history 

437 graph_url = ( 

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

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

440 ) 

441 

442 # Final context variables which are available in the template 

443 return { 

444 'categories': categories, 

445 'graph_url': graph_url.format( 

446 package_hash=package_hashdir(package.name), 

447 package_name=package.name), 

448 } 

449 

450 def get_binary_bug_stats(self, binary_name): 

451 """ 

452 Returns the bug statistics for the given binary package. 

453 

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

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

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

457 

458 - rc - critical, grave serious 

459 - normal - important and normal 

460 - wishlist - wishlist and minor 

461 - fixed - pending and fixed 

462 """ 

463 stats = get_or_none(BinaryPackageBugStats, package__name=binary_name) 

464 if stats is None: 

465 return 

466 category_descriptions = { 

467 'rc': { 

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

469 }, 

470 'normal': { 

471 'display_name': 'important and normal', 

472 }, 

473 'wishlist': { 

474 'display_name': 'wishlist and minor', 

475 }, 

476 'fixed': { 

477 'display_name': 'pending and fixed', 

478 }, 

479 } 

480 

481 def extend_category(category, extra_parameters): 

482 category.update(extra_parameters) 

483 return category 

484 

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

486 # display name for each of them. 

487 return [ 

488 extend_category(category, 

489 category_descriptions[category['category_name']]) 

490 for category in stats.stats 

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

492 ]