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 jsonfield import JSONField 

22 

23from distro_tracker.core.models import ( 

24 BinaryPackageBugStats, 

25 BugDisplayManager, 

26 PackageBugStats, 

27 PackageName, 

28 SourcePackageName, 

29) 

30from distro_tracker.core.utils import SpaceDelimitedTextField, get_or_none 

31from distro_tracker.core.utils.packages import package_hashdir 

32 

33 

34class DebianContributor(models.Model): 

35 """ 

36 Model containing additional Debian-specific information about contributors. 

37 """ 

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

39 on_delete=models.CASCADE) 

40 agree_with_low_threshold_nmu = models.BooleanField(default=False) 

41 is_debian_maintainer = models.BooleanField(default=False) 

42 allowed_packages = SpaceDelimitedTextField(blank=True) 

43 

44 def __str__(self): 

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

46 

47 

48class LintianStats(models.Model): 

49 """ 

50 Model for lintian stats of packages. 

51 """ 

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

53 on_delete=models.CASCADE) 

54 stats = JSONField() 

55 

56 def __str__(self): 

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

58 package=self.package) 

59 

60 def get_lintian_url(self): 

61 """ 

62 Returns the lintian URL for the package matching the 

63 :class:`LintianStats 

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

65 """ 

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

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

68 return '' 

69 

70 return ( 

71 'https://lintian.debian.org/sources/{pkg}'.format( 

72 pkg=self.package) 

73 ) 

74 

75 

76class PackageTransition(models.Model): 

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

78 on_delete=models.CASCADE) 

79 transition_name = models.CharField(max_length=50) 

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

81 reject = models.BooleanField(default=False) 

82 

83 def __str__(self): 

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

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

86 

87 

88class PackageExcuses(models.Model): 

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

90 on_delete=models.CASCADE) 

91 excuses = JSONField() 

92 

93 def __str__(self): 

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

95 

96 

97class BuildLogCheckStats(models.Model): 

98 package = models.OneToOneField( 

99 SourcePackageName, 

100 related_name='build_logcheck_stats', 

101 on_delete=models.CASCADE) 

102 stats = JSONField() 

103 

104 def __str__(self): 

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

106 

107 

108class UbuntuPackage(models.Model): 

109 package = models.OneToOneField( 

110 PackageName, 

111 related_name='ubuntu_package', 

112 on_delete=models.CASCADE) 

113 version = models.TextField(max_length=100) 

114 bugs = JSONField(null=True, blank=True) 

115 patch_diff = JSONField(null=True, blank=True) 

116 

117 def __str__(self): 

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

119 

120 

121class DebianBugDisplayManager(BugDisplayManager): 

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

123 panel_template_name = 'debian/bugs.html' 

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

125 category_descriptions = { 

126 'rc': { 

127 'display_name': 'RC', 

128 'description': 'Release Critical', 

129 }, 

130 'normal': { 

131 'display_name': 'I&N', 

132 'description': 'Important and Normal', 

133 }, 

134 'wishlist': { 

135 'display_name': 'M&W', 

136 'description': 'Minor and Wishlist', 

137 }, 

138 'fixed': { 

139 'display_name': 'F&P', 

140 'description': 'Fixed and Pending', 

141 }, 

142 'patch': { 

143 'display_name': 'patch', 

144 'description': 'Patch', 

145 }, 

146 'help': { 

147 'display_name': 'help', 

148 'description': 'Help needed', 

149 }, 

150 'newcomer': { 

151 'display_name': 'NC', 

152 'description': 'newcomer', 

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

154 } 

155 } 

156 

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

158 """ 

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

160 category name. 

161 

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

163 

164 - ``all`` - all bugs for the package 

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

166 - ``rc`` - release critical bugs 

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

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

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

170 ones 

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

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

173 merged ones 

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

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

176 merged ones 

177 

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

179 should be provided. 

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

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

182 ``pseudo``, ``binary``. 

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

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

185 

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

187 category. 

188 """ 

189 URL_PARAMETERS = { 

190 'all': ( 

191 ('repeatmerged', 'no'), 

192 ), 

193 'rc': ( 

194 ('archive', 'no'), 

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

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

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

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

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

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

201 ('repeatmerged', 'no'), 

202 ), 

203 'normal': ( 

204 ('archive', 'no'), 

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

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

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

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

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

210 ('repeatmerged', 'no'), 

211 ), 

212 'wishlist': ( 

213 ('archive', 'no'), 

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

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

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

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

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

219 ('repeatmerged', 'no'), 

220 ), 

221 'fixed': ( 

222 ('archive', 'no'), 

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

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

225 ('repeatmerged', 'no'), 

226 ), 

227 'patch': ( 

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

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

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

231 ('repeatmerged', 'no'), 

232 ), 

233 'help': ( 

234 ('tag', 'help'), 

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

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

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

238 ), 

239 'newcomer': ( 

240 ('tag', 'newcomer'), 

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

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

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

244 ), 

245 'all-merged': ( 

246 ('repeatmerged', 'yes'), 

247 ), 

248 'rc-merged': ( 

249 ('archive', 'no'), 

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

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

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

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

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

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

256 ('repeatmerged', 'yes'), 

257 ), 

258 'normal-merged': ( 

259 ('archive', 'no'), 

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

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

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

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

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

265 ('repeatmerged', 'yes'), 

266 ), 

267 'wishlist-merged': ( 

268 ('archive', 'no'), 

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

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

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

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

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

274 ('repeatmerged', 'yes'), 

275 ), 

276 'fixed-merged': ( 

277 ('archive', 'no'), 

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

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

280 ('repeatmerged', 'yes'), 

281 ), 

282 'patch-merged': ( 

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

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

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

286 ('repeatmerged', 'yes'), 

287 ), 

288 } 

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

290 return 

291 

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

293 query_parameters = URL_PARAMETERS[category_name] 

294 

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

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

297 elif package_type == 'binary': 

298 if category_name == 'all': 

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

300 # the rest of the URLs. 

301 return domain + package_name 

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

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

304 

305 return ( 

306 domain + 

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

308 urlencode(query_parameters) 

309 ) 

310 

311 def get_bugs_categories_list(self, stats, package): 

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

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

314 

315 categories = [] 

316 total, total_merged = 0, 0 

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

318 for category in stats: 

319 category_name = category['category_name'] 

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

321 continue 

322 # Add main bug count 

323 category_stats = { 

324 'category_name': category['category_name'], 

325 'bug_count': category['bug_count'], 

326 } 

327 # Add merged bug count 

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

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

330 category_stats['merged'] = { 

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

332 } 

333 # Add descriptions 

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

335 categories.append(category_stats) 

336 

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

338 if category_name not in exclude_from_count: 

339 total += category['bug_count'] 

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

341 

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

343 all_category = { 

344 'category_name': 'all', 

345 'display_name': 'all', 

346 'bug_count': total, 

347 } 

348 if total != total_merged: 

349 all_category['merged'] = { 

350 'bug_count': total_merged, 

351 } 

352 # The totals are the first displayed row. 

353 categories.insert(0, all_category) 

354 

355 # Add URLs for all categories 

356 for category in categories: 

357 # URL for the non-merged category 

358 url = self.get_bug_tracker_url( 

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

360 category['url'] = url 

361 

362 # URL for the merged category 

363 if 'merged' in category: 

364 url_merged = self.get_bug_tracker_url( 

365 package.name, 'source', 

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

367 ) 

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

369 

370 return categories 

371 

372 def table_field_context(self, package): 

373 """ 

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

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

376 for Debian BTS. 

377 """ 

378 try: 

379 stats = package.bug_stats.stats 

380 except ObjectDoesNotExist: 

381 stats = [] 

382 

383 data = {} 

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

385 

386 total = 0 

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

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

389 total = category['bug_count'] 

390 break 

391 data['all'] = total 

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

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

394 

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

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

397 for bug in data['bugs']: 

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

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

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

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

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

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

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

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

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

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

408 return data 

409 

410 def panel_context(self, package): 

411 """ 

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

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

414 

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

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

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

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

419 

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

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

422 category. 

423 

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

425 

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

427 the rendered template. 

428 """ 

429 bug_stats = get_or_none(PackageBugStats, package=package) 

430 

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

432 stats = bug_stats.stats 

433 else: 

434 stats = [] 

435 

436 categories = self.get_bugs_categories_list(stats, package) 

437 

438 # Debian also includes a custom graph of bug history 

439 graph_url = ( 

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

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

442 ) 

443 

444 # Final context variables which are available in the template 

445 return { 

446 'categories': categories, 

447 'graph_url': graph_url.format( 

448 package_hash=package_hashdir(package.name), 

449 package_name=package.name), 

450 } 

451 

452 def get_binary_bug_stats(self, binary_name): 

453 """ 

454 Returns the bug statistics for the given binary package. 

455 

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

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

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

459 

460 - rc - critical, grave serious 

461 - normal - important and normal 

462 - wishlist - wishlist and minor 

463 - fixed - pending and fixed 

464 """ 

465 stats = get_or_none(BinaryPackageBugStats, package__name=binary_name) 

466 if stats is None: 

467 return 

468 category_descriptions = { 

469 'rc': { 

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

471 }, 

472 'normal': { 

473 'display_name': 'important and normal', 

474 }, 

475 'wishlist': { 

476 'display_name': 'wishlist and minor', 

477 }, 

478 'fixed': { 

479 'display_name': 'pending and fixed', 

480 }, 

481 } 

482 

483 def extend_category(category, extra_parameters): 

484 category.update(extra_parameters) 

485 return category 

486 

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

488 # display name for each of them. 

489 return [ 

490 extend_category(category, 

491 category_descriptions[category['category_name']]) 

492 for category in stats.stats 

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

494 ]