Coverage for webapp/store/views.py: 46%

410 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-08 22:07 +0000

1import humanize 

2from canonicalwebteam.discourse import DocParser 

3from canonicalwebteam.discourse.exceptions import PathNotFoundError 

4from canonicalwebteam.flask_base.decorators import ( 

5 exclude_xframe_options_header, 

6) 

7from canonicalwebteam.exceptions import StoreApiResponseErrorList 

8from flask import Blueprint, Response, abort, url_for 

9from flask import jsonify, redirect, render_template, request, make_response 

10from pybadges import badge 

11 

12from webapp.store_api import publisher_gateway 

13from webapp.config import DETAILS_VIEW_REGEX 

14from webapp.decorators import ( 

15 redirect_uppercase_to_lowercase, 

16 store_maintenance, 

17) 

18from webapp.helpers import discourse_api, markdown_to_html 

19from webapp.store import logic 

20from webapp.config import SEARCH_FIELDS 

21from webapp.observability.utils import trace_function 

22from webapp.store_api import device_gateway 

23 

24 

25store = Blueprint( 

26 "store", __name__, template_folder="/templates", static_folder="/static" 

27) 

28 

29 

30@store.route("/publisher/<regex('[a-z0-9-]*[a-z][a-z0-9-]*'):publisher>") 

31def get_publisher_details(publisher): 

32 """ 

33 A view to display the publisher details page for specific publisher. 

34 """ 

35 

36 error_info = {} 

37 items = [] 

38 extra_fields = [ 

39 "result.publisher", 

40 "result.description", 

41 ] 

42 

43 response = device_gateway.find( 

44 publisher=publisher, 

45 fields=SEARCH_FIELDS.copy() + extra_fields, 

46 ) 

47 

48 for package in response["results"]: 

49 item = package["result"] 

50 item["name"] = package["name"] 

51 item["type"] = package["type"] 

52 item["icon"] = next( 

53 (media for media in item["media"] if media["type"] == "icon"), None 

54 ) 

55 items.append(item) 

56 

57 context = { 

58 "items": items, 

59 "items_count": len(items), 

60 "publisher": ( 

61 items[0]["publisher"] 

62 if len(items) > 0 

63 else {"display-name": publisher} 

64 ), 

65 "error_info": error_info, 

66 } 

67 

68 # HTML template will be returned here for the front end 

69 return render_template("details/publisher.html", **context) 

70 

71 

72@trace_function 

73@store.route("/packages.json") 

74def get_packages(): 

75 query = request.args.get("q", default=None, type=str) 

76 provides = request.args.get("provides", default=None, type=str) 

77 requires = request.args.get("requires", default=None, type=str) 

78 context = {"packages": [], "size": 0} 

79 

80 if query: 

81 results = publisher_gateway.find( 

82 query=query, fields=SEARCH_FIELDS 

83 ).get("results") 

84 context["q"] = query 

85 elif provides or requires: 

86 if provides: 

87 provides = provides.split(",") 

88 if requires: 

89 requires = requires.split(",") 

90 

91 results = publisher_gateway.find( 

92 provides=provides, requires=requires, fields=SEARCH_FIELDS 

93 ).get("results") 

94 

95 context["provides"] = provides 

96 context["requires"] = requires 

97 else: 

98 results = publisher_gateway.find(fields=SEARCH_FIELDS).get( 

99 "results", [] 

100 ) 

101 

102 packages = [] 

103 total_packages = 0 

104 

105 for i, item in enumerate(results): 

106 total_packages += 1 

107 package = logic.add_store_front_data(results[i], False) 

108 packages.append(package) 

109 

110 context["packages"] = packages 

111 context["size"] = total_packages 

112 

113 return context 

114 

115 

116FIELDS = [ 

117 "result.media", 

118 "default-release", 

119 "result.categories", 

120 "result.publisher.display-name", 

121 "result.title", 

122 "result.unlisted", 

123 "channel-map", 

124 "result.deployable-on", 

125] 

126 

127 

128@trace_function 

129def get_package(entity_name, channel_request=None, fields=FIELDS): 

130 # Get entity info from API 

131 package = publisher_gateway.get_item_details( 

132 entity_name, channel=channel_request, fields=fields 

133 ) 

134 

135 # If the package is not published, return a 404 

136 if not package["default-release"]: 

137 abort(404) 

138 

139 # Fix issue #1010 

140 if channel_request: 

141 channel_map = publisher_gateway.get_item_details( 

142 entity_name, fields=["channel-map"] 

143 ) 

144 package["channel-map"] = channel_map["channel-map"] 

145 

146 package = logic.add_store_front_data(package, True) 

147 package = logic.add_overlay_data(package) 

148 

149 for channel in package["channel-map"]: 

150 channel["channel"]["released-at"] = logic.convert_date( 

151 channel["channel"]["released-at"] 

152 ) 

153 

154 return package 

155 

156 

157@trace_function 

158@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>') 

159@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/docs') 

160@store_maintenance 

161@redirect_uppercase_to_lowercase 

162def details_overview(entity_name): 

163 channel_request = request.args.get("channel", default=None, type=str) 

164 if request.base_url.endswith("/docs"): 

165 url = f"/{entity_name}" 

166 if channel_request: 

167 url = f"{url}?channel={channel_request}" 

168 return redirect(url) 

169 

170 extra_fields = [ 

171 "result.bugs-url", 

172 "result.website", 

173 "result.summary", 

174 "default-release.revision.metadata-yaml", 

175 "default-release.revision.readme-md", 

176 "result.links", 

177 ] 

178 

179 package = get_package( 

180 entity_name, channel_request, FIELDS.copy() + extra_fields 

181 ) 

182 

183 context = { 

184 "package": package, 

185 "channel_requested": channel_request, 

186 "navigation": None, 

187 "last_update": None, 

188 "forum_url": None, 

189 "topic_path": None, 

190 } 

191 

192 description = None 

193 summary = None 

194 

195 doc = logic.get_doc_link(package) 

196 

197 # If the doc link does NOT include "discourse", 

198 # it is accepted as a ReadTheDocs link. 

199 # Update this logic when a better way to distinguish 

200 # ReadTheDocs links becomes available. 

201 is_rtd = doc and "discourse" not in doc.lower() 

202 

203 docs_topic = package["store_front"].get("docs_topic") 

204 if is_rtd: 

205 readme = ( 

206 package.get("default-release", {}) 

207 .get("revision", {}) 

208 .get("readme-md") 

209 ) 

210 summary = logic.get_summary(package) 

211 description = ( 

212 markdown_to_html(readme) 

213 if readme 

214 else logic.get_description(package, parse_to_html=True) 

215 ) 

216 navigation = None 

217 elif docs_topic: 

218 docs_url_prefix = f"/{package['name']}/docs" 

219 

220 docs = DocParser( 

221 api=discourse_api, 

222 index_topic_id=package["store_front"]["docs_topic"], 

223 url_prefix=docs_url_prefix, 

224 ) 

225 try: 

226 docs.parse() 

227 topic = docs.index_topic 

228 docs_content = docs.parse_topic(topic) 

229 description = docs_content.get("body_html", "") 

230 

231 navigation = docs.navigation 

232 

233 overview = { 

234 "hidden": False, 

235 "level": 1, 

236 "path": "", 

237 "navlink_href": f"/{entity_name}", 

238 "navlink_fragment": "", 

239 "navlink_text": "Overview", 

240 "is_active": True, 

241 "has_active_child": False, 

242 "children": [], 

243 } 

244 

245 if len(navigation["nav_items"]) > 0: 

246 navigation["nav_items"][0]["children"].insert(0, overview) 

247 # If the first item in docs nav is "overview", 

248 # prefix with "Docs - " 

249 if ( 

250 len(navigation["nav_items"][0]["children"]) > 1 

251 and navigation["nav_items"][0]["children"][1][ 

252 "navlink_text" 

253 ] 

254 == "Overview" 

255 ): 

256 del navigation["nav_items"][0]["children"][1] 

257 else: 

258 # If there is no navigation but we've got here, there are docs 

259 # So add a top level "Docs item". Example: /easyrsa 

260 navigation["nav_items"] = [ 

261 { 

262 "level": 0, 

263 "children": [ 

264 overview, 

265 ], 

266 } 

267 ] 

268 

269 context["navigation"] = navigation 

270 context["forum_url"] = docs.api.base_url 

271 context["last_update"] = docs_content["updated"] 

272 context["topic_path"] = docs_content["topic_path"] 

273 except Exception as e: 

274 if e.response.status_code == 404: 

275 navigation = None 

276 description = logic.get_description(package) 

277 summary = logic.get_summary(package) 

278 else: 

279 navigation = None 

280 description = logic.get_description(package, parse_to_html=True) 

281 summary = logic.get_summary(package) 

282 

283 context["description"] = description 

284 context["summary"] = summary 

285 context["package_type"] = package["type"] 

286 context["doc_url"] = doc 

287 context["is_rtd"] = is_rtd 

288 return render_template("details/overview.html", **context) 

289 

290 

291@trace_function 

292@store.route( 

293 '/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/docs/<path:path>' 

294) 

295@store_maintenance 

296@redirect_uppercase_to_lowercase 

297def details_docs(entity_name, path=None): 

298 channel_request = request.args.get("channel", default=None, type=str) 

299 extra_fields = [ 

300 "default-release.revision.metadata-yaml", 

301 "result.bugs-url", 

302 "result.website", 

303 ] 

304 

305 package = get_package( 

306 entity_name, channel_request, FIELDS.copy() + extra_fields 

307 ) 

308 

309 # If no docs, redirect to main page 

310 if not package["store_front"]["docs_topic"]: 

311 return redirect(url_for(".details_overview", entity_name=entity_name)) 

312 

313 docs_url_prefix = f"/{package['name']}/docs" 

314 

315 docs = DocParser( 

316 api=discourse_api, 

317 index_topic_id=package["store_front"]["docs_topic"], 

318 url_prefix=docs_url_prefix, 

319 ) 

320 docs.parse() 

321 

322 if path: 

323 topic_id = None 

324 try: 

325 topic_id = docs.resolve_path(path)[0] 

326 except PathNotFoundError: 

327 abort(404) 

328 

329 topic = docs.api.get_topic(topic_id) 

330 else: 

331 topic = docs.index_topic 

332 

333 document = docs.parse_topic(topic) 

334 

335 navigation = docs.navigation 

336 

337 overview = { 

338 "hidden": False, 

339 "level": 1, 

340 "path": "", 

341 "navlink_href": f"/{entity_name}", 

342 "navlink_fragment": "", 

343 "navlink_text": "Overview", 

344 "is_active": False, 

345 "has_active_child": False, 

346 "children": [], 

347 } 

348 

349 if len(navigation["nav_items"]) > 0: 

350 navigation["nav_items"][0]["children"].insert(0, overview) 

351 # If the first item in docs nav is "overview", 

352 # prefix with "Docs - " 

353 if ( 

354 len(navigation["nav_items"][0]["children"]) > 1 

355 and navigation["nav_items"][0]["children"][1]["navlink_text"] 

356 == "Overview" 

357 ): 

358 del navigation["nav_items"][0]["children"][1] 

359 else: 

360 # If there is no navigation but we've got here, there are docs 

361 # So add a top level "Docs item". Example: /easyrsa 

362 navigation["nav_items"] = [ 

363 { 

364 "level": 0, 

365 "children": [ 

366 overview, 

367 ], 

368 } 

369 ] 

370 

371 context = { 

372 "package": package, 

373 "navigation": navigation, 

374 "body_html": document["body_html"], 

375 "last_update": document["updated"], 

376 "forum_url": docs.api.base_url, 

377 "topic_path": document["topic_path"], 

378 "channel_requested": channel_request, 

379 } 

380 

381 return render_template("details/docs.html", **context) 

382 

383 

384@trace_function 

385@store.route( 

386 '/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/configurations' 

387) 

388@store.route( 

389 '/<regex("' 

390 + DETAILS_VIEW_REGEX 

391 + '"):entity_name>/configurations/<path:path>' 

392) 

393@store_maintenance 

394@redirect_uppercase_to_lowercase 

395def details_configuration(entity_name, path=None): 

396 channel_request = request.args.get("channel", default=None, type=str) 

397 extra_fields = [ 

398 "default-release.revision.config-yaml", 

399 ] 

400 

401 package = get_package( 

402 entity_name, channel_request, FIELDS.copy() + extra_fields 

403 ) 

404 subpackage = None 

405 

406 if package["type"] == "bundle": 

407 bundle_charms = package["store_front"]["bundle"]["charms"] 

408 

409 if not path and bundle_charms: 

410 default_charm = bundle_charms[0] 

411 return redirect( 

412 url_for( 

413 ".details_configuration", 

414 entity_name=entity_name, 

415 path=default_charm["name"], 

416 channel=channel_request, 

417 ) 

418 ) 

419 

420 if path: 

421 if not any(d["name"] == path for d in bundle_charms): 

422 abort(404) 

423 

424 try: 

425 subpackage = get_package(path) 

426 except StoreApiResponseErrorList: 

427 subpackage = None 

428 

429 return render_template( 

430 f"details/configure-{package['type']}.html", 

431 package=package, 

432 subpackage=subpackage, 

433 channel_requested=channel_request, 

434 subpackage_path=path, 

435 ) 

436 

437 

438@trace_function 

439@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/actions') 

440@store_maintenance 

441@redirect_uppercase_to_lowercase 

442def details_actions(entity_name): 

443 channel_request = request.args.get("channel", default=None, type=str) 

444 extra_fields = [ 

445 "default-release.revision.actions-yaml", 

446 ] 

447 

448 package = get_package( 

449 entity_name, channel_request, FIELDS.copy() + extra_fields 

450 ) 

451 return render_template( 

452 "details/actions.html", 

453 package=package, 

454 channel_requested=channel_request, 

455 ) 

456 

457 

458@trace_function 

459@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/libraries') 

460@store_maintenance 

461@redirect_uppercase_to_lowercase 

462def details_libraries(entity_name): 

463 channel_request = request.args.get("channel", default=None, type=str) 

464 package = get_package(entity_name, channel_request, FIELDS) 

465 

466 libraries = logic.process_libraries( 

467 publisher_gateway.get_charm_libraries(entity_name) 

468 ) 

469 

470 if libraries: 

471 first_lib = libraries[0]["name"] 

472 return redirect( 

473 url_for( 

474 ".details_library", 

475 entity_name=entity_name, 

476 library_name=first_lib, 

477 ) 

478 ) 

479 

480 return render_template( 

481 "details/libraries/no-libraries.html", 

482 entity_name=entity_name, 

483 package=package, 

484 libraries=libraries, 

485 channel_requested=channel_request, 

486 ) 

487 

488 

489@trace_function 

490@store.route( 

491 '/<regex("' 

492 + DETAILS_VIEW_REGEX 

493 + '"):entity_name>/libraries/<string:library_name>' 

494) 

495@store_maintenance 

496@redirect_uppercase_to_lowercase 

497def details_library(entity_name, library_name): 

498 channel_request = request.args.get("channel", default=None, type=str) 

499 package = get_package(entity_name, channel_request, FIELDS) 

500 

501 libraries = logic.process_libraries( 

502 publisher_gateway.get_charm_libraries(entity_name) 

503 ) 

504 

505 library_id = logic.get_library(library_name, libraries) 

506 

507 if not library_id: 

508 abort(404) 

509 

510 library = publisher_gateway.get_charm_library(entity_name, library_id) 

511 docstrings = logic.process_python_docs(library, module_name=library_name) 

512 

513 # Charmcraft string to fetch the library 

514 fetch_charm = entity_name.replace("-", "_") 

515 fetch_api = library["api"] 

516 fetch_string = f"charms.{fetch_charm}.v{fetch_api}.{library_name}" 

517 

518 return render_template( 

519 "details/libraries/docstring.html", 

520 entity_name=entity_name, 

521 package=package, 

522 libraries=libraries, 

523 library=library, 

524 docstrings=docstrings, 

525 channel_requested=channel_request, 

526 library_name=library_name, 

527 fetch_string=fetch_string, 

528 creation_date=logic.convert_date(library["created-at"]), 

529 ) 

530 

531 

532@trace_function 

533@store.route( 

534 '/<regex("' 

535 + DETAILS_VIEW_REGEX 

536 + '"):entity_name>/libraries/<string:library_name>/source-code' 

537) 

538@store_maintenance 

539@redirect_uppercase_to_lowercase 

540def details_library_source_code(entity_name, library_name): 

541 channel_request = request.args.get("channel", default=None, type=str) 

542 package = get_package(entity_name, channel_request, FIELDS) 

543 

544 libraries = logic.process_libraries( 

545 publisher_gateway.get_charm_libraries(entity_name) 

546 ) 

547 

548 library_id = logic.get_library(library_name, libraries) 

549 if not library_id: 

550 abort(404) 

551 

552 library = publisher_gateway.get_charm_library(entity_name, library_id) 

553 source_code = library.get("source-code", "") 

554 

555 return render_template( 

556 "details/libraries/source-code.html", 

557 entity_name=entity_name, 

558 package=package, 

559 libraries=libraries, 

560 library=library, 

561 source_code=source_code, 

562 channel_requested=channel_request, 

563 library_name=library_name, 

564 ) 

565 

566 

567@trace_function 

568@store.route( 

569 '/<regex("' 

570 + DETAILS_VIEW_REGEX 

571 + '"):entity_name>/libraries/<string:library_name>/download' 

572) 

573@store_maintenance 

574@redirect_uppercase_to_lowercase 

575def download_library(entity_name, library_name): 

576 lib_parts = library_name.split(".") 

577 

578 if len(lib_parts) > 2: 

579 lib_name = "." + ".".join(lib_parts[-2:]) 

580 else: 

581 lib_name = library_name 

582 

583 libraries = logic.process_libraries( 

584 publisher_gateway.get_charm_libraries(entity_name) 

585 ) 

586 

587 library = next( 

588 (lib for lib in libraries if lib.get("name") == lib_name), 

589 None, 

590 ) 

591 

592 if not library: 

593 abort(404) 

594 

595 library = publisher_gateway.get_charm_library(entity_name, library["id"]) 

596 

597 return Response( 

598 library["content"], 

599 mimetype="text/x-python", 

600 headers={ 

601 "Content-disposition": "attachment; " 

602 f"filename={library['library-name']}.py" 

603 }, 

604 ) 

605 

606 

607@trace_function 

608@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/integrations') 

609@store_maintenance 

610@redirect_uppercase_to_lowercase 

611def details_integrations(entity_name): 

612 channel_request = request.args.get("channel", default=None, type=str) 

613 extra_fields = [ 

614 "default-release.revision.metadata-yaml", 

615 ] 

616 package = get_package(entity_name, channel_request, FIELDS + extra_fields) 

617 

618 return render_template( 

619 "details/integrations.html", 

620 package=package, 

621 channel_requested=channel_request, 

622 ) 

623 

624 

625@trace_function 

626@store.route( 

627 '/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/integrations.json' 

628) 

629@store_maintenance 

630@redirect_uppercase_to_lowercase 

631def details_integrations_data(entity_name): 

632 channel_request = request.args.get("channel", default=None, type=str) 

633 extra_fields = [ 

634 "default-release.revision.metadata-yaml", 

635 ] 

636 package = get_package(entity_name, channel_request, FIELDS + extra_fields) 

637 

638 relations = ( 

639 package.get("default-release", {}) 

640 .get("revision", {}) 

641 .get("relations", {}) 

642 ) 

643 

644 provides = add_required_fields( 

645 package["store_front"]["metadata"].get("provides", {}), 

646 relations.get("provides", {}), 

647 ) 

648 requires = add_required_fields( 

649 package["store_front"]["metadata"].get("requires", {}), 

650 relations.get("requires", {}), 

651 ) 

652 

653 grouped_relations = { 

654 "provides": provides, 

655 "requires": requires, 

656 } 

657 

658 return jsonify({"grouped_relations": grouped_relations}) 

659 

660 

661@trace_function 

662def add_required_fields(metadata_relations, relations): 

663 processed_relations = [ 

664 { 

665 **relations[key], 

666 "key": key, 

667 "required": metadata_relations[key].get("required", False), 

668 } 

669 for key in relations.keys() 

670 ] 

671 return processed_relations 

672 

673 

674@trace_function 

675@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/resources') 

676@store_maintenance 

677@redirect_uppercase_to_lowercase 

678def details_resources(entity_name): 

679 channel_request = request.args.get("channel", default=None, type=str) 

680 package = get_package(entity_name, channel_request, FIELDS) 

681 

682 # /resources redirect to the first resource 

683 if package["default-release"]["resources"]: 

684 name = package["default-release"]["resources"][0]["name"] 

685 return redirect( 

686 url_for( 

687 ".details_resource", 

688 entity_name=entity_name, 

689 resource_name=name, 

690 ) 

691 ) 

692 else: 

693 return render_template( 

694 "details/no-resources.html", 

695 package=package, 

696 channel_requested=channel_request, 

697 ) 

698 

699 

700@trace_function 

701@store.route( 

702 '/<regex("' 

703 + DETAILS_VIEW_REGEX 

704 + '"):entity_name>/resources/<string:resource_name>' 

705) 

706@store_maintenance 

707@redirect_uppercase_to_lowercase 

708def details_resource(entity_name, resource_name): 

709 channel_request = request.args.get("channel", default=None, type=str) 

710 package = get_package(entity_name, channel_request, FIELDS) 

711 resources = package["default-release"]["resources"] 

712 

713 if not resources: 

714 return redirect(url_for(".details_resources", entity_name=entity_name)) 

715 

716 resource = next( 

717 (item for item in resources if item["name"] == resource_name), None 

718 ) 

719 

720 if not resource: 

721 return redirect(url_for(".details_resources", entity_name=entity_name)) 

722 

723 # Get OCI image details 

724 if resource["type"] == "oci-image": 

725 oci_details = publisher_gateway.process_response( 

726 publisher_gateway.session.get(resource["download"]["url"]) 

727 ) 

728 resource["image_name"], resource["digest"] = oci_details[ 

729 "ImageName" 

730 ].split("@") 

731 resource["short_digest"] = resource["digest"].split(":")[1][:12] 

732 

733 # Get upstream-source (if available) 

734 metadata_resources = package["store_front"]["metadata"].get( 

735 "resources", {} 

736 ) 

737 if resource_name in metadata_resources: 

738 upstream = metadata_resources[resource_name].get("upstream-source") 

739 resource["upstream_source"] = upstream 

740 else: 

741 resource["upstream_source"] = None 

742 

743 revisions = device_gateway.get_resource_revisions( 

744 entity_name, resource_name 

745 ) 

746 revisions = sorted(revisions, key=lambda k: k["revision"], reverse=True) 

747 

748 # Humanize sizes 

749 resource["size"] = humanize.naturalsize(resource["download"]["size"]) 

750 resource["updated"] = logic.convert_date(resource["created-at"]) 

751 

752 for revision in revisions: 

753 revision["size"] = humanize.naturalsize(revision["download"]["size"]) 

754 revision["updated"] = logic.convert_date(revision["created-at"]) 

755 

756 return render_template( 

757 "details/resources.html", 

758 package=package, 

759 channel_requested=channel_request, 

760 resource=resource, 

761 revisions=revisions, 

762 ) 

763 

764 

765@trace_function 

766@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/integrate') 

767@store_maintenance 

768@redirect_uppercase_to_lowercase 

769def details_integrate(entity_name): 

770 channel_request = request.args.get("channel", default=None, type=str) 

771 package = get_package(entity_name, channel_request, FIELDS) 

772 

773 return render_template( 

774 "details/integrate.html", 

775 package=package, 

776 channel_requested=channel_request, 

777 ) 

778 

779 

780@trace_function 

781@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/badge.svg') 

782def entity_badge(entity_name): 

783 package = publisher_gateway.get_item_details(entity_name, fields=FIELDS) 

784 

785 channel_request = request.args.get("channel") 

786 

787 if not package["default-release"]: 

788 abort(404) 

789 

790 release = package["default-release"] 

791 

792 if channel_request: 

793 for release_channel in package["channel-map"]: 

794 channel = release_channel["channel"] 

795 if f"{channel['track']}/{channel['risk']}" == channel_request: 

796 release = release_channel 

797 break 

798 

799 entity_link = request.url_root + entity_name 

800 right_text = "".join( 

801 [ 

802 release["channel"]["track"], 

803 "/", 

804 release["channel"]["risk"], 

805 " ", 

806 release["revision"]["version"], 

807 ] 

808 ) 

809 

810 svg = badge( 

811 left_text=package["name"], 

812 right_text=right_text, 

813 right_color="#0e8420", 

814 left_link=entity_link, 

815 right_link=entity_link, 

816 logo=( 

817 "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' " 

818 "viewBox='0 0 64 64'%3E%3Cg fill-rule='evenodd' " 

819 "fill='none'%3E%3Cg fill-rule='nonzero'%3E%3Ccircle cy='32' " 

820 "cx='32' r='32' fill='%23fff'/%3E%3Cg transform='translate(13.622" 

821 " 5.6546)' fill='%23585858'%3E%3Ccircle cy='24.614' cx='20.307' " 

822 "r='1.9732'/%3E%3Cpath d='m22.129 20.971h-3.643v-14.571c0-3.5154" 

823 " 2.86-6.3753 6.375-6.3753 3.515-0.000025 6.375 2.8599 6.375 6." 

824 "375v3.6433h-3.643v-3.6433c0-0.7297-0.284-1.4159-0.8-1.932-0.51" 

825 "6-0.5159-1.202-0.8002-1.932-0.8002-1.506 0-2.732 1.2255-2.732 " 

826 "2.7322v14.571z'/%3E%3Cpath d='m33.968 27.346c-3.515 0-6.375-2." 

827 "859-6.375-6.375v-9.107h3.643v9.107c0 1.507 1.226 2.732 2.732 2." 

828 "732 1.507 0 2.733-1.225 2.733-2.732v-9.107h3.642v9.107c0 1.703" 

829 "-0.663 3.304-1.867 4.508s-2.805 1.867-4.508 1.867z'/%3E%3Ccircle" 

830 " cy='46.471' cx='2.093' r='1.9732'/%3E%3Cpath d='m3.9143 42.829" 

831 "h-3.6429l0.00002-20.036c0-3.515 2.86-6.375 6.3751-6.375 3.5155 0" 

832 " 6.3755 2.86 6.3755 6.375v3.643h-3.6433v-3.643c0-0.73-0.284-1." 

833 "416-0.8001-1.932-0.5159-0.516-1.2022-0.8-1.9319-0.8-1.5064 0-2." 

834 "7322 1.225-2.7322 2.732l-0.0002 20.036z'/%3E%3Cpath d='m15.754 " 

835 "43.74c-3.516 0-6.3753-2.86-6.3753-6.376v-9.107h3.6433v9.107c0 " 

836 "1.506 1.225 2.732 2.732 2.732 1.506 0 2.732-1.226 2.732-2.732v-" 

837 "9.107h3.643v9.107c0 1.703-0.663 3.304-1.867 4.508-1.205 1.204-2" 

838 ".805 1.868-4.508 1.868z'/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E" 

839 ), 

840 ) 

841 

842 return svg, 200, {"Content-Type": "image/svg+xml"} 

843 

844 

845@trace_function 

846@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/embedded') 

847@exclude_xframe_options_header 

848def entity_embedded_card(entity_name): 

849 store_design = request.args.get("store_design", default=False, type=bool) 

850 channel_request = request.args.get("channel", default=None, type=str) 

851 try: 

852 package = get_package(entity_name, channel_request, FIELDS) 

853 

854 package["default-release"]["channel"]["released-at"] = ( 

855 logic.convert_date( 

856 package["default-release"]["channel"]["released-at"] 

857 ) 

858 ) 

859 

860 button = request.args.get("button") 

861 if button and button not in ["black", "white"]: 

862 button = None 

863 

864 context = { 

865 "store_design": store_design, 

866 "button": button, 

867 "package": package, 

868 "show_channels": request.args.get("channels"), 

869 "show_summary": request.args.get("summary"), 

870 "show_base": request.args.get("base"), 

871 } 

872 

873 return render_template( 

874 "embeddable-card.html", 

875 **context, 

876 ) 

877 except Exception: 

878 return ( 

879 render_template( 

880 "embeddable-404.html", 

881 store_design=store_design, 

882 entity_name=entity_name, 

883 ), 

884 404, 

885 ) 

886 

887 

888@trace_function 

889@store.route( 

890 '/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/embedded/interface' 

891) 

892@exclude_xframe_options_header 

893def entity_embedded_interface_card(entity_name): 

894 channel_request = request.args.get("channel", default=None, type=str) 

895 try: 

896 package = get_package(entity_name, channel_request, FIELDS) 

897 

898 package["default-release"]["channel"]["released-at"] = ( 

899 logic.convert_date( 

900 package["default-release"]["channel"]["released-at"] 

901 ) 

902 ) 

903 

904 libraries = logic.process_libraries( 

905 publisher_gateway.get_charm_libraries(entity_name) 

906 ) 

907 

908 context = { 

909 "package": package, 

910 "libraries": libraries, 

911 } 

912 

913 return render_template( 

914 "interface-card.html", 

915 **context, 

916 ) 

917 except Exception: 

918 return ( 

919 render_template( 

920 "interface-card-404.html", 

921 entity_name=entity_name, 

922 ), 

923 404, 

924 ) 

925 

926 

927@trace_function 

928@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/icon') 

929def entity_icon(entity_name): 

930 icon_url = ( 

931 "https://assets.ubuntu.com/v1/be6eb412-snapcraft-missing-icon.svg" 

932 ) 

933 package = None 

934 

935 try: 

936 package = publisher_gateway.get_item_details( 

937 entity_name, 

938 fields=[ 

939 "result.media", 

940 ], 

941 ) 

942 except StoreApiResponseErrorList: 

943 pass 

944 

945 if package and package["result"]["media"]: 

946 icon_url = package["result"]["media"][0]["url"] 

947 

948 return redirect( 

949 "https://res.cloudinary.com/canonical/image/fetch/f_auto" 

950 f",q_auto,fl_sanitize,w_64,h_64/{icon_url}" 

951 ) 

952 

953 

954@trace_function 

955@store.route( 

956 '/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/icon-no-default' 

957) 

958def entity_icon_missing(entity_name): 

959 package = None 

960 

961 try: 

962 package = publisher_gateway.get_item_details( 

963 entity_name, 

964 fields=[ 

965 "result.media", 

966 ], 

967 ) 

968 except StoreApiResponseErrorList: 

969 pass 

970 

971 if package and package["result"]["media"]: 

972 icon_url = package["result"]["media"][0]["url"] 

973 

974 return redirect( 

975 "https://res.cloudinary.com/canonical/image/fetch/f_auto" 

976 f",q_auto,fl_sanitize,w_64,h_64/{icon_url}" 

977 ) 

978 

979 abort(404) 

980 

981 

982# This method is a temporary hack to show bundle icons on the 

983# homepage, and should be removed once the icons are available via the api 

984@trace_function 

985@store.route('/<regex("' + DETAILS_VIEW_REGEX + '"):entity_name>/charms.json') 

986def get_charms_from_bundle(entity_name): 

987 package = get_package(entity_name) 

988 

989 if package["type"] != "bundle": 

990 return "Requested object should be a bundle", 400 

991 

992 return jsonify({"charms": package["store_front"]["bundle"]["charms"]}) 

993 

994 

995@trace_function 

996@store.route("/") 

997def store_index(): 

998 response = make_response(render_template("store.html")) 

999 return response