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

394 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-27 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 

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

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

150 channel["channel"]["released-at"] 

151 ) 

152 

153 return package 

154 

155 

156@trace_function 

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

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

159@store_maintenance 

160@redirect_uppercase_to_lowercase 

161def details_overview(entity_name): 

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

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

164 url = f"/{entity_name}" 

165 if channel_request: 

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

167 return redirect(url) 

168 

169 extra_fields = [ 

170 "result.bugs-url", 

171 "result.website", 

172 "result.summary", 

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

174 "result.links", 

175 ] 

176 

177 package = get_package( 

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

179 ) 

180 

181 context = { 

182 "package": package, 

183 "channel_requested": channel_request, 

184 "navigation": None, 

185 "last_update": None, 

186 "forum_url": None, 

187 "topic_path": None, 

188 } 

189 

190 description = None 

191 summary = None 

192 

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

194 

195 if docs_topic: 

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

197 

198 docs = DocParser( 

199 api=discourse_api, 

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

201 url_prefix=docs_url_prefix, 

202 ) 

203 try: 

204 docs.parse() 

205 topic = docs.index_topic 

206 docs_content = docs.parse_topic(topic) 

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

208 

209 navigation = docs.navigation 

210 

211 overview = { 

212 "hidden": False, 

213 "level": 1, 

214 "path": "", 

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

216 "navlink_fragment": "", 

217 "navlink_text": "Overview", 

218 "is_active": True, 

219 "has_active_child": False, 

220 "children": [], 

221 } 

222 

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

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

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

226 # prefix with "Docs - " 

227 if ( 

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

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

230 "navlink_text" 

231 ] 

232 == "Overview" 

233 ): 

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

235 else: 

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

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

238 navigation["nav_items"] = [ 

239 { 

240 "level": 0, 

241 "children": [ 

242 overview, 

243 ], 

244 } 

245 ] 

246 

247 context["navigation"] = navigation 

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

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

250 context["topic_path"] = docs_content["topic_path"] 

251 except Exception as e: 

252 if e.response.status_code == 404: 

253 navigation = None 

254 description, summary = logic.add_description_and_summary( 

255 package 

256 ) 

257 

258 else: 

259 navigation = None 

260 description, summary = logic.add_description_and_summary(package) 

261 description = markdown_to_html(description) 

262 

263 context["description"] = description 

264 context["summary"] = summary 

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

266 

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

268 

269 

270@trace_function 

271@store.route( 

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

273) 

274@store_maintenance 

275@redirect_uppercase_to_lowercase 

276def details_docs(entity_name, path=None): 

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

278 extra_fields = [ 

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

280 "result.bugs-url", 

281 "result.website", 

282 ] 

283 

284 package = get_package( 

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

286 ) 

287 

288 # If no docs, redirect to main page 

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

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

291 

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

293 

294 docs = DocParser( 

295 api=discourse_api, 

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

297 url_prefix=docs_url_prefix, 

298 ) 

299 docs.parse() 

300 

301 if path: 

302 topic_id = None 

303 try: 

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

305 except PathNotFoundError: 

306 abort(404) 

307 

308 topic = docs.api.get_topic(topic_id) 

309 else: 

310 topic = docs.index_topic 

311 

312 document = docs.parse_topic(topic) 

313 

314 navigation = docs.navigation 

315 

316 overview = { 

317 "hidden": False, 

318 "level": 1, 

319 "path": "", 

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

321 "navlink_fragment": "", 

322 "navlink_text": "Overview", 

323 "is_active": False, 

324 "has_active_child": False, 

325 "children": [], 

326 } 

327 

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

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

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

331 # prefix with "Docs - " 

332 if ( 

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

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

335 == "Overview" 

336 ): 

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

338 else: 

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

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

341 navigation["nav_items"] = [ 

342 { 

343 "level": 0, 

344 "children": [ 

345 overview, 

346 ], 

347 } 

348 ] 

349 

350 context = { 

351 "package": package, 

352 "navigation": navigation, 

353 "body_html": document["body_html"], 

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

355 "forum_url": docs.api.base_url, 

356 "topic_path": document["topic_path"], 

357 "channel_requested": channel_request, 

358 } 

359 

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

361 

362 

363@trace_function 

364@store.route( 

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

366) 

367@store.route( 

368 '/<regex("' 

369 + DETAILS_VIEW_REGEX 

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

371) 

372@store_maintenance 

373@redirect_uppercase_to_lowercase 

374def details_configuration(entity_name, path=None): 

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

376 extra_fields = [ 

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

378 ] 

379 

380 package = get_package( 

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

382 ) 

383 subpackage = None 

384 

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

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

387 

388 if not path and bundle_charms: 

389 default_charm = bundle_charms[0] 

390 return redirect( 

391 url_for( 

392 ".details_configuration", 

393 entity_name=entity_name, 

394 path=default_charm["name"], 

395 channel=channel_request, 

396 ) 

397 ) 

398 

399 if path: 

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

401 abort(404) 

402 

403 try: 

404 subpackage = get_package(path) 

405 except StoreApiResponseErrorList: 

406 subpackage = None 

407 

408 return render_template( 

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

410 package=package, 

411 subpackage=subpackage, 

412 channel_requested=channel_request, 

413 subpackage_path=path, 

414 ) 

415 

416 

417@trace_function 

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

419@store_maintenance 

420@redirect_uppercase_to_lowercase 

421def details_actions(entity_name): 

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

423 extra_fields = [ 

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

425 ] 

426 

427 package = get_package( 

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

429 ) 

430 return render_template( 

431 "details/actions.html", 

432 package=package, 

433 channel_requested=channel_request, 

434 ) 

435 

436 

437@trace_function 

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

439@store_maintenance 

440@redirect_uppercase_to_lowercase 

441def details_libraries(entity_name): 

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

443 package = get_package(entity_name, channel_request, FIELDS) 

444 

445 libraries = logic.process_libraries( 

446 publisher_gateway.get_charm_libraries(entity_name) 

447 ) 

448 

449 if libraries: 

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

451 return redirect( 

452 url_for( 

453 ".details_library", 

454 entity_name=entity_name, 

455 library_name=first_lib, 

456 ) 

457 ) 

458 

459 return render_template( 

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

461 entity_name=entity_name, 

462 package=package, 

463 libraries=libraries, 

464 channel_requested=channel_request, 

465 ) 

466 

467 

468@trace_function 

469@store.route( 

470 '/<regex("' 

471 + DETAILS_VIEW_REGEX 

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

473) 

474@store_maintenance 

475@redirect_uppercase_to_lowercase 

476def details_library(entity_name, library_name): 

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

478 package = get_package(entity_name, channel_request, FIELDS) 

479 

480 libraries = logic.process_libraries( 

481 publisher_gateway.get_charm_libraries(entity_name) 

482 ) 

483 

484 library_id = logic.get_library(library_name, libraries) 

485 

486 if not library_id: 

487 abort(404) 

488 

489 library = publisher_gateway.get_charm_library(entity_name, library_id) 

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

491 

492 # Charmcraft string to fetch the library 

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

494 fetch_api = library["api"] 

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

496 

497 return render_template( 

498 "details/libraries/docstring.html", 

499 entity_name=entity_name, 

500 package=package, 

501 libraries=libraries, 

502 library=library, 

503 docstrings=docstrings, 

504 channel_requested=channel_request, 

505 library_name=library_name, 

506 fetch_string=fetch_string, 

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

508 ) 

509 

510 

511@trace_function 

512@store.route( 

513 '/<regex("' 

514 + DETAILS_VIEW_REGEX 

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

516) 

517@store_maintenance 

518@redirect_uppercase_to_lowercase 

519def details_library_source_code(entity_name, library_name): 

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

521 package = get_package(entity_name, channel_request, FIELDS) 

522 

523 libraries = logic.process_libraries( 

524 publisher_gateway.get_charm_libraries(entity_name) 

525 ) 

526 

527 library_id = logic.get_library(library_name, libraries) 

528 if not library_id: 

529 abort(404) 

530 

531 library = publisher_gateway.get_charm_library(entity_name, library_id) 

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

533 

534 return render_template( 

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

536 entity_name=entity_name, 

537 package=package, 

538 libraries=libraries, 

539 library=library, 

540 source_code=source_code, 

541 channel_requested=channel_request, 

542 library_name=library_name, 

543 ) 

544 

545 

546@trace_function 

547@store.route( 

548 '/<regex("' 

549 + DETAILS_VIEW_REGEX 

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

551) 

552@store_maintenance 

553@redirect_uppercase_to_lowercase 

554def download_library(entity_name, library_name): 

555 lib_parts = library_name.split(".") 

556 

557 if len(lib_parts) > 2: 

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

559 else: 

560 lib_name = library_name 

561 

562 libraries = logic.process_libraries( 

563 publisher_gateway.get_charm_libraries(entity_name) 

564 ) 

565 

566 library = next( 

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

568 None, 

569 ) 

570 

571 if not library: 

572 abort(404) 

573 

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

575 

576 return Response( 

577 library["content"], 

578 mimetype="text/x-python", 

579 headers={ 

580 "Content-disposition": "attachment; " 

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

582 }, 

583 ) 

584 

585 

586@trace_function 

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

588@store_maintenance 

589@redirect_uppercase_to_lowercase 

590def details_integrations(entity_name): 

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

592 extra_fields = [ 

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

594 ] 

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

596 

597 return render_template( 

598 "details/integrations.html", 

599 package=package, 

600 channel_requested=channel_request, 

601 ) 

602 

603 

604@trace_function 

605@store.route( 

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

607) 

608@store_maintenance 

609@redirect_uppercase_to_lowercase 

610def details_integrations_data(entity_name): 

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

612 extra_fields = [ 

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

614 ] 

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

616 

617 relations = ( 

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

619 .get("revision", {}) 

620 .get("relations", {}) 

621 ) 

622 

623 provides = add_required_fields( 

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

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

626 ) 

627 requires = add_required_fields( 

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

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

630 ) 

631 

632 grouped_relations = { 

633 "provides": provides, 

634 "requires": requires, 

635 } 

636 

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

638 

639 

640@trace_function 

641def add_required_fields(metadata_relations, relations): 

642 processed_relations = [ 

643 { 

644 **relations[key], 

645 "key": key, 

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

647 } 

648 for key in relations.keys() 

649 ] 

650 return processed_relations 

651 

652 

653@trace_function 

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

655@store_maintenance 

656@redirect_uppercase_to_lowercase 

657def details_resources(entity_name): 

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

659 package = get_package(entity_name, channel_request, FIELDS) 

660 

661 # /resources redirect to the first resource 

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

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

664 return redirect( 

665 url_for( 

666 ".details_resource", 

667 entity_name=entity_name, 

668 resource_name=name, 

669 ) 

670 ) 

671 else: 

672 return render_template( 

673 "details/no-resources.html", 

674 package=package, 

675 channel_requested=channel_request, 

676 ) 

677 

678 

679@trace_function 

680@store.route( 

681 '/<regex("' 

682 + DETAILS_VIEW_REGEX 

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

684) 

685@store_maintenance 

686@redirect_uppercase_to_lowercase 

687def details_resource(entity_name, resource_name): 

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

689 package = get_package(entity_name, channel_request, FIELDS) 

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

691 

692 if not resources: 

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

694 

695 resource = next( 

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

697 ) 

698 

699 if not resource: 

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

701 

702 # Get OCI image details 

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

704 oci_details = publisher_gateway.process_response( 

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

706 ) 

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

708 "ImageName" 

709 ].split("@") 

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

711 

712 revisions = device_gateway.get_resource_revisions( 

713 entity_name, resource_name 

714 ) 

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

716 

717 # Humanize sizes 

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

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

720 

721 for revision in revisions: 

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

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

724 

725 return render_template( 

726 "details/resources.html", 

727 package=package, 

728 channel_requested=channel_request, 

729 resource=resource, 

730 revisions=revisions, 

731 ) 

732 

733 

734@trace_function 

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

736@store_maintenance 

737@redirect_uppercase_to_lowercase 

738def details_integrate(entity_name): 

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

740 package = get_package(entity_name, channel_request, FIELDS) 

741 

742 return render_template( 

743 "details/integrate.html", 

744 package=package, 

745 channel_requested=channel_request, 

746 ) 

747 

748 

749@trace_function 

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

751def entity_badge(entity_name): 

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

753 

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

755 

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

757 abort(404) 

758 

759 release = package["default-release"] 

760 

761 if channel_request: 

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

763 channel = release_channel["channel"] 

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

765 release = release_channel 

766 break 

767 

768 entity_link = request.url_root + entity_name 

769 right_text = "".join( 

770 [ 

771 release["channel"]["track"], 

772 "/", 

773 release["channel"]["risk"], 

774 " ", 

775 release["revision"]["version"], 

776 ] 

777 ) 

778 

779 svg = badge( 

780 left_text=package["name"], 

781 right_text=right_text, 

782 right_color="#0e8420", 

783 left_link=entity_link, 

784 right_link=entity_link, 

785 logo=( 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

808 ), 

809 ) 

810 

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

812 

813 

814@trace_function 

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

816@exclude_xframe_options_header 

817def entity_embedded_card(entity_name): 

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

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

820 try: 

821 package = get_package(entity_name, channel_request, FIELDS) 

822 

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

824 logic.convert_date( 

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

826 ) 

827 ) 

828 

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

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

831 button = None 

832 

833 context = { 

834 "store_design": store_design, 

835 "button": button, 

836 "package": package, 

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

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

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

840 } 

841 

842 return render_template( 

843 "embeddable-card.html", 

844 **context, 

845 ) 

846 except Exception: 

847 return ( 

848 render_template( 

849 "embeddable-404.html", 

850 store_design=store_design, 

851 entity_name=entity_name, 

852 ), 

853 404, 

854 ) 

855 

856 

857@trace_function 

858@store.route( 

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

860) 

861@exclude_xframe_options_header 

862def entity_embedded_interface_card(entity_name): 

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

864 try: 

865 package = get_package(entity_name, channel_request, FIELDS) 

866 

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

868 logic.convert_date( 

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

870 ) 

871 ) 

872 

873 libraries = logic.process_libraries( 

874 publisher_gateway.get_charm_libraries(entity_name) 

875 ) 

876 

877 context = { 

878 "package": package, 

879 "libraries": libraries, 

880 } 

881 

882 return render_template( 

883 "interface-card.html", 

884 **context, 

885 ) 

886 except Exception: 

887 return ( 

888 render_template( 

889 "interface-card-404.html", 

890 entity_name=entity_name, 

891 ), 

892 404, 

893 ) 

894 

895 

896@trace_function 

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

898def entity_icon(entity_name): 

899 icon_url = ( 

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

901 ) 

902 package = None 

903 

904 try: 

905 package = publisher_gateway.get_item_details( 

906 entity_name, 

907 fields=[ 

908 "result.media", 

909 ], 

910 ) 

911 except StoreApiResponseErrorList: 

912 pass 

913 

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

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

916 

917 return redirect( 

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

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

920 ) 

921 

922 

923@trace_function 

924@store.route( 

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

926) 

927def entity_icon_missing(entity_name): 

928 package = None 

929 

930 try: 

931 package = publisher_gateway.get_item_details( 

932 entity_name, 

933 fields=[ 

934 "result.media", 

935 ], 

936 ) 

937 except StoreApiResponseErrorList: 

938 pass 

939 

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

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

942 

943 return redirect( 

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

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

946 ) 

947 

948 abort(404) 

949 

950 

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

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

953@trace_function 

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

955def get_charms_from_bundle(entity_name): 

956 package = get_package(entity_name) 

957 

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

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

960 

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

962 

963 

964@trace_function 

965@store.route("/") 

966def store_index(): 

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

968 return response