1# Copyright 2018 The Distro Tracker Developers 

2# See the COPYRIGHT file at the top-level directory of this distribution and 

3# at https://deb.li/DTAuthors 

4# 

5# This file is part of Distro Tracker. It is subject to the license terms 

6# in the LICENSE file found in the top-level directory of this 

7# distribution and at https://deb.li/DTLicense. No part of Distro Tracker, 

8# including this file, may be copied, modified, propagated, or distributed 

9# except according to the terms contained in the LICENSE file. 

10"""Implements the core package tables shown on team pages.""" 

11import importlib 

12import logging 

13 

14from django.conf import settings 

15from django.db.models import Prefetch 

16from django.template import Context, Template 

17from django.template.loader import get_template 

18 

19from distro_tracker import vendor 

20from distro_tracker.core.models import ( 

21 BugDisplayManagerMixin, 

22 PackageData, 

23 PackageName, 

24) 

25from distro_tracker.core.utils import add_developer_extras, get_vcs_name 

26from distro_tracker.core.utils.plugins import PluginRegistry 

27 

28logger = logging.getLogger(__name__) 

29 

30 

31class BaseTableField(metaclass=PluginRegistry): 

32 """ 

33 A base class representing fields to be displayed on package tables. 

34 

35 To create a new field for packages table, users only need to create a 

36 subclass and implement the necessary properties and methods. 

37 

38 .. note:: 

39 To make sure the subclass is loaded, make sure to put it in a 

40 ``tracker_package_tables`` module at the top level of a Django app. 

41 """ 

42 

43 def context(self, package): 

44 """ 

45 Should return a dictionary representing context variables necessary for 

46 the package table field. 

47 When the field's template is rendered, it will have access to the values 

48 in this dictionary. 

49 """ 

50 return {} 

51 

52 @property 

53 def column_name(): 

54 """ 

55 The column name for the field 

56 """ 

57 return '' 

58 

59 @property 

60 def template_name(self): 

61 """ 

62 If the field has a corresponding template which is used to render its 

63 HTML output, this property should contain the name of this template. 

64 """ 

65 return None 

66 

67 def render(self, package, context=None, request=None): 

68 """ 

69 Render the field's HTML output for the given package. 

70 """ 

71 if not hasattr(self, '_template'): 

72 self._template = get_template(self.template_name) 

73 if context is None: 

74 context = {self.slug: self.context(package)} 

75 return self._template.render(context, request) 

76 

77 @property 

78 def prefetch_related_lookups(): 

79 """ 

80 Returns a list of lookups to be prefetched along with 

81 Table's QuerySet of packages. Elements may be either a String 

82 or Prefetch object 

83 """ 

84 return [] 

85 

86 

87class GeneralInformationTableField(BaseTableField): 

88 """ 

89 This table field displays general information to identify a package. 

90 

91 It displays the package's name in the cell and the following information 

92 on details popup 

93 - name 

94 - short description 

95 - version (in the default repository) 

96 - maintainer 

97 - uploaders 

98 - architectures 

99 - standards version 

100 - binaries 

101 """ 

102 column_name = 'Package' 

103 slug = 'general' 

104 template_name = 'core/package-table-fields/general.html' 

105 prefetch_related_lookups = [ 

106 Prefetch( 

107 'data', 

108 queryset=PackageData.objects.filter(key='general'), 

109 to_attr='general_data' 

110 ), 

111 Prefetch( 

112 'data', 

113 queryset=PackageData.objects.filter(key='binaries'), 

114 to_attr='binaries_data' 

115 ), 

116 ] 

117 

118 def context(self, package): 

119 try: 

120 info = package.general_data[0] 

121 except IndexError: 

122 # There is no general info for the package 

123 return { 

124 'url': package.get_absolute_url, 

125 'name': package.name 

126 } 

127 

128 general = info.value 

129 general['url'] = package.get_absolute_url 

130 

131 # Add developer information links and any other vendor-specific extras 

132 general = add_developer_extras(general, url_only=True) 

133 

134 try: 

135 info = package.binaries_data[0] 

136 general['binaries'] = info.value 

137 except IndexError: 

138 general['binaries'] = [] 

139 

140 return general 

141 

142 

143class VcsTableField(BaseTableField): 

144 """ 

145 This table field displays information regarding the package VCS repository. 

146 It is customizable to enable vendors to add specific data 

147 regarding the package's vcs repository. 

148 

149 The default behavior is to display the package's repository type with a 

150 (browser) link to it. 

151 

152 A vendor can provide a 

153 :data:`DISTRO_TRACKER_VCS_TABLE_FIELD_TEMPLATE 

154 <distro_tracker.project.local_settings.DISTRO_TRACKER_VCS_TABLE_FIELD_TEMPLATE>` 

155 settings value which gives the path to a template which should 

156 be used to render the field. It is recommended that this template extends 

157 ``core/package-table-fields/vcs.html``, but not mandatory. 

158 If a custom 

159 :func:`get_vcs_data 

160 <distro_tracker.vendor.skeleton.rules.get_vcs_data>` 

161 function in order to provide custom data to be displayed in the field. 

162 Refer to the function's documentation for the format of the return value. 

163 If this function is defined then its return value is simply passed to the 

164 template and does not require any special format; the vendor's template can 

165 access this value in the ``field.context`` context variable and can use it 

166 any way it wants. 

167 

168 To avoid performance issues, if :func:`get_vcs_data 

169 <distro_tracker.vendor.skeleton.rules.get_vcs_data>` function 

170 depends on data from other database tables than packages, the vendor app 

171 should also implement the :func:`additional_prefetch_related_lookups 

172 <distro_tracker.vendor.skeleton.rules.additional_prefetch_related_lookups>` 

173 """ 

174 column_name = 'VCS' 

175 slug = 'vcs' 

176 _default_template_name = 'core/package-table-fields/vcs.html' 

177 prefetch_related_lookups = [ 

178 Prefetch( 

179 'data', 

180 queryset=PackageData.objects.filter(key='general'), 

181 to_attr='general_data' 

182 ) 

183 ] 

184 

185 @property 

186 def template_name(self): 

187 return getattr( 

188 settings, 

189 'DISTRO_TRACKER_VCS_TABLE_FIELD_TEMPLATE', 

190 self._default_template_name) 

191 

192 def context(self, package): 

193 try: 

194 info = package.general_data[0] 

195 except IndexError: 

196 # There is no general info for the package 

197 return 

198 

199 general = {} 

200 if 'vcs' in info.value: 200 ↛ 206line 200 didn't jump to line 206, because the condition on line 200 was never false

201 general['vcs'] = info.value['vcs'] 

202 # Map the VCS type to its name. 

203 shorthand = general['vcs'].get('type', 'Unknown') 

204 general['vcs']['full_name'] = get_vcs_name(shorthand) 

205 

206 result, implemented = vendor.call( 

207 'get_vcs_data', package) 

208 

209 if implemented: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true

210 general.update(result) 

211 

212 return general 

213 

214 

215class ArchiveTableField(BaseTableField): 

216 """ 

217 This table field displays information regarding the package version on 

218 archive. 

219 

220 It displays the package's version on archive 

221 """ 

222 column_name = 'Archive' 

223 slug = 'archive' 

224 template_name = 'core/package-table-fields/archive.html' 

225 prefetch_related_lookups = [ 

226 Prefetch( 

227 'data', 

228 queryset=PackageData.objects.filter(key='general'), 

229 to_attr='general_data' 

230 ), 

231 Prefetch( 

232 'data', 

233 queryset=PackageData.objects.filter(key='versions'), 

234 to_attr='versions' 

235 ) 

236 ] 

237 

238 def context(self, package): 

239 try: 

240 info = package.general_data[0] 

241 except IndexError: 

242 # There is no general info for the package 

243 return 

244 

245 general = {} 

246 if 'version' in info.value: 246 ↛ 249line 246 didn't jump to line 249, because the condition on line 246 was never false

247 general['version'] = info.value['version'] 

248 

249 try: 

250 info = package.versions[0].value 

251 general['default_pool_url'] = info['default_pool_url'] 

252 except IndexError: 

253 # There is no versions info for the package 

254 general['default_pool_url'] = '#' 

255 

256 return general 

257 

258 

259class BugStatsTableField(BaseTableField, BugDisplayManagerMixin): 

260 """ 

261 This table field displays bug statistics for the package. 

262 It is customizable to enable vendors to add specific data. 

263 

264 The default behavior defined by :class:`BugDisplayManager 

265 <distro_tracker.core.models.BugDisplayManager>` 

266 is to display the number of bugs for a package. It also 

267 shows the bugs categories on popover content. 

268 

269 A vendor may provide a custom way of displaying bugs data in 

270 packages tables by implementing :func:`get_bug_display_manager_class 

271 <distro_tracker.vendor.skeleton.rules.get_bug_display_manager_class>` 

272 function in order to provide a custom class to handle the bugs data 

273 presentation. Refer to the function's documentation for the format of the 

274 return value. 

275 

276 To avoid performance issues, if additional database lookups are 

277 required to display custom bugs data, the vendor app 

278 should also implement the :func:`additional_prefetch_related_lookups 

279 <distro_tracker.vendor.skeleton.rules.additional_prefetch_related_lookups>` 

280 """ 

281 column_name = 'Bugs' 

282 slug = 'bugs' 

283 prefetch_related_lookups = ['bug_stats'] 

284 

285 @property 

286 def template_name(self): 

287 return self.bug_manager.table_field_template_name 

288 

289 def context(self, package): 

290 return self.bug_manager.table_field_context(package) 

291 

292 

293class BasePackageTable(metaclass=PluginRegistry): 

294 """ 

295 A base class representing package tables which are displayed on a team page. 

296 

297 To include a package table on the team page, users only need to create a 

298 subclass and implement the necessary properties and methods. 

299 

300 .. note:: 

301 To make sure the subclass is loaded, make sure to put it in a 

302 ``tracker_package_tables`` module at the top level of a Django app. 

303 

304 The following vendor-specific functions can be implemented to augment 

305 this table: 

306 

307 - :func:`get_table_fields 

308 <distro_tracker.vendor.skeleton.rules.get_table_fields>` 

309 """ 

310 

311 #: The slug of the table which is used to define its url. 

312 #: Must be overriden and set to a unique non-empty value. 

313 slug = None 

314 

315 def __init__(self, scope, title=None, limit=None, tag=None): 

316 """ 

317 :param scope: a convenient object that can be used to define the list 

318 of packages to be displayed on the table. For instance, if you want 

319 to consider all the packages of a specific team, you must pass that 

320 team through the `scope` attribute to allow the method 

321 :meth:`packages` to access it to define the packages to be 

322 presented. 

323 :param title: a string to be displayed instead of the default title 

324 :param limit: an integer that can be used to define the max number of 

325 packages to be displayed 

326 :param tag: if defined, it is used to display only packages tagged with 

327 the informed tag 

328 """ 

329 self.scope = scope 

330 self._title = title 

331 self.limit = limit 

332 self.tag = tag 

333 if tag and not tag.startswith('tag:'): 

334 self.tag = 'tag:' + tag 

335 

336 def context(self): 

337 """ 

338 Should return a dictionary representing context variables necessary for 

339 the package table. 

340 When the table's template is rendered, it will have access to the values 

341 in this dictionary. 

342 """ 

343 return {} 

344 

345 @property 

346 def default_title(self): 

347 """ 

348 The default title of the table. 

349 """ 

350 return '' 

351 

352 @property 

353 def title(self): 

354 """ 

355 The title of the table. 

356 """ 

357 if self._title: 

358 return self._title 

359 elif self.tag: 

360 # TODO: need better design 

361 data = PackageData.objects.filter(key=self.tag).first() 

362 if data and 'table_title' in data.value: 

363 return data.value['table_title'] 

364 return self.default_title 

365 

366 @property 

367 def relative_url(self, **kwargs): 

368 """ 

369 The relative url for the table. 

370 """ 

371 url = '+table/' + self.slug 

372 if self.tag: 

373 tag = self.tag[4:] 

374 url = url + '?tag=' + tag 

375 return url 

376 

377 @property 

378 def packages_with_prefetch_related(self): 

379 """ 

380 Returns the list of packages with prefetched relationships defined by 

381 table fields 

382 """ 

383 attributes_name = set() 

384 package_query_set = self.packages 

385 for field in self.table_fields: 

386 for lookup in field.prefetch_related_lookups: 

387 if isinstance(lookup, Prefetch): 

388 if lookup.to_attr in attributes_name: 

389 continue 

390 else: 

391 attributes_name.add(lookup.to_attr) 

392 package_query_set = package_query_set.prefetch_related(lookup) 

393 

394 additional_data, implemented = vendor.call( 

395 'additional_prefetch_related_lookups' 

396 ) 

397 if implemented and additional_data: 397 ↛ 398line 397 didn't jump to line 398, because the condition on line 397 was never true

398 for lookup in additional_data: 

399 package_query_set = package_query_set.prefetch_related(lookup) 

400 return package_query_set 

401 

402 @property 

403 def packages(self): 

404 """ 

405 Returns the list of packages shown in the table. One may define this 

406 based on the scope 

407 """ 

408 return PackageName.objects.all().order_by('name') 

409 

410 @property 

411 def column_names(self): 

412 """ 

413 Returns a list of column names that will compose the table 

414 in the proper order 

415 """ 

416 names = [] 

417 for field in self.table_fields: 

418 names.append(field.column_name) 

419 return names 

420 

421 @property 

422 def default_fields(self): 

423 """ 

424 Returns a list of default :class:`BaseTableField` that will compose the 

425 table 

426 """ 

427 return [ 

428 GeneralInformationTableField, 

429 VcsTableField, 

430 ArchiveTableField, 

431 BugStatsTableField, 

432 ] 

433 

434 @property 

435 def table_fields(self): 

436 """ 

437 Returns the tuple of :class:`BaseTableField` that will compose the 

438 table 

439 """ 

440 fields, implemented = vendor.call('get_table_fields', **{ 

441 'table': self, 

442 }) 

443 if implemented and fields: 443 ↛ 444line 443 didn't jump to line 444, because the condition on line 443 was never true

444 return tuple(fields) 

445 else: 

446 return tuple(self.default_fields) 

447 

448 @property 

449 def rows(self): 

450 """ 

451 Returns the content of the table's rows, where each row has the list 

452 of :class:`BaseTableField` for each package 

453 """ 

454 rows = [] 

455 packages = self.packages_with_prefetch_related 

456 if self.limit: 

457 packages = packages[:self.limit] 

458 

459 template = self.get_row_template() 

460 fields = [f() for f in self.table_fields] 

461 context = {} 

462 

463 for package in packages: 

464 context['package'] = package 

465 for field in fields: 

466 context[field.slug] = field.context(package) 

467 rows.append(template.render(Context(context))) 

468 

469 return rows 

470 

471 @property 

472 def number_of_packages(self): 

473 """ 

474 Returns the number of packages displayed in the table 

475 """ 

476 if hasattr(self.packages_with_prefetch_related, 'count'): 476 ↛ 479line 476 didn't jump to line 479, because the condition on line 476 was never false

477 return self.packages_with_prefetch_related.count() 

478 else: 

479 return 0 

480 

481 @staticmethod 

482 def get_template_content(template_name): 

483 with open(get_template(template_name).origin.name) as f: 

484 return f.read() 

485 

486 def get_row_template(self): 

487 template = "<tr scope='row'>\n" 

488 for f in self.table_fields: 

489 template += "<td class='center' scope='col'>" 

490 template += self.get_template_content(f().template_name) 

491 template += "</td>\n" 

492 template += "</tr>\n" 

493 return Template(template) 

494 

495 

496def create_table(slug, scope, title=None, limit=None, tag=None): 

497 """ 

498 A helper function to create packages table. The table class is defined 

499 through the `slug`. If no children class of 

500 :class:`BasePackageTable` exists with the given slug, the function returns 

501 `None`. 

502 

503 :returns: an instance of the table created with the informed params 

504 :rtype: :class:`BasePackageTable` 

505 """ 

506 for app in settings.INSTALLED_APPS: 

507 try: 

508 module_name = app + '.' + 'tracker_package_tables' 

509 importlib.import_module(module_name) 

510 except ImportError: 

511 # The app does not implement package tables. 

512 pass 

513 

514 if limit: 

515 limit = int(limit) 

516 for table_class in BasePackageTable.plugins: 

517 if table_class is not BasePackageTable: 517 ↛ 516line 517 didn't jump to line 516, because the condition on line 517 was never false

518 if table_class.slug == slug: 

519 return table_class(scope, title=title, limit=limit, tag=tag) 

520 

521 return None 

522 

523 

524class GeneralTeamPackageTable(BasePackageTable): 

525 """ 

526 This table displays the packages information of a team in a simple fashion. 

527 It must receive a :class:`Team <distro_tracker.core.models.Team>` as scope 

528 """ 

529 default_title = "All team packages" 

530 slug = 'general' 

531 

532 @property 

533 def packages(self): 

534 """ 

535 Returns the list of packages shown in the table of a team (scope) 

536 """ 

537 if self.tag: 

538 return self.scope.packages.filter( 

539 data__key=self.tag).order_by('name') 

540 else: 

541 return self.scope.packages.all().order_by('name')