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
« 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
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
25store = Blueprint(
26 "store", __name__, template_folder="/templates", static_folder="/static"
27)
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 """
36 error_info = {}
37 items = []
38 extra_fields = [
39 "result.publisher",
40 "result.description",
41 ]
43 response = device_gateway.find(
44 publisher=publisher,
45 fields=SEARCH_FIELDS.copy() + extra_fields,
46 )
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)
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 }
68 # HTML template will be returned here for the front end
69 return render_template("details/publisher.html", **context)
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}
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(",")
91 results = publisher_gateway.find(
92 provides=provides, requires=requires, fields=SEARCH_FIELDS
93 ).get("results")
95 context["provides"] = provides
96 context["requires"] = requires
97 else:
98 results = publisher_gateway.find(fields=SEARCH_FIELDS).get(
99 "results", []
100 )
102 packages = []
103 total_packages = 0
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)
110 context["packages"] = packages
111 context["size"] = total_packages
113 return context
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]
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 )
135 # If the package is not published, return a 404
136 if not package["default-release"]:
137 abort(404)
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"]
146 package = logic.add_store_front_data(package, True)
148 for channel in package["channel-map"]:
149 channel["channel"]["released-at"] = logic.convert_date(
150 channel["channel"]["released-at"]
151 )
153 return package
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)
169 extra_fields = [
170 "result.bugs-url",
171 "result.website",
172 "result.summary",
173 "default-release.revision.metadata-yaml",
174 "result.links",
175 ]
177 package = get_package(
178 entity_name, channel_request, FIELDS.copy() + extra_fields
179 )
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 }
190 description = None
191 summary = None
193 docs_topic = package["store_front"].get("docs_topic")
195 if docs_topic:
196 docs_url_prefix = f"/{package['name']}/docs"
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", "")
209 navigation = docs.navigation
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 }
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 ]
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 )
258 else:
259 navigation = None
260 description, summary = logic.add_description_and_summary(package)
261 description = markdown_to_html(description)
263 context["description"] = description
264 context["summary"] = summary
265 context["package_type"] = package["type"]
267 return render_template("details/overview.html", **context)
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 ]
284 package = get_package(
285 entity_name, channel_request, FIELDS.copy() + extra_fields
286 )
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))
292 docs_url_prefix = f"/{package['name']}/docs"
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()
301 if path:
302 topic_id = None
303 try:
304 topic_id = docs.resolve_path(path)[0]
305 except PathNotFoundError:
306 abort(404)
308 topic = docs.api.get_topic(topic_id)
309 else:
310 topic = docs.index_topic
312 document = docs.parse_topic(topic)
314 navigation = docs.navigation
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 }
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 ]
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 }
360 return render_template("details/docs.html", **context)
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 ]
380 package = get_package(
381 entity_name, channel_request, FIELDS.copy() + extra_fields
382 )
383 subpackage = None
385 if package["type"] == "bundle":
386 bundle_charms = package["store_front"]["bundle"]["charms"]
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 )
399 if path:
400 if not any(d["name"] == path for d in bundle_charms):
401 abort(404)
403 try:
404 subpackage = get_package(path)
405 except StoreApiResponseErrorList:
406 subpackage = None
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 )
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 ]
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 )
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)
445 libraries = logic.process_libraries(
446 publisher_gateway.get_charm_libraries(entity_name)
447 )
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 )
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 )
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)
480 libraries = logic.process_libraries(
481 publisher_gateway.get_charm_libraries(entity_name)
482 )
484 library_id = logic.get_library(library_name, libraries)
486 if not library_id:
487 abort(404)
489 library = publisher_gateway.get_charm_library(entity_name, library_id)
490 docstrings = logic.process_python_docs(library, module_name=library_name)
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}"
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 )
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)
523 libraries = logic.process_libraries(
524 publisher_gateway.get_charm_libraries(entity_name)
525 )
527 library_id = logic.get_library(library_name, libraries)
528 if not library_id:
529 abort(404)
531 library = publisher_gateway.get_charm_library(entity_name, library_id)
532 source_code = library.get("source-code", "")
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 )
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(".")
557 if len(lib_parts) > 2:
558 lib_name = "." + ".".join(lib_parts[-2:])
559 else:
560 lib_name = library_name
562 libraries = logic.process_libraries(
563 publisher_gateway.get_charm_libraries(entity_name)
564 )
566 library = next(
567 (lib for lib in libraries if lib.get("name") == lib_name),
568 None,
569 )
571 if not library:
572 abort(404)
574 library = publisher_gateway.get_charm_library(entity_name, library["id"])
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 )
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)
597 return render_template(
598 "details/integrations.html",
599 package=package,
600 channel_requested=channel_request,
601 )
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)
617 relations = (
618 package.get("default-release", {})
619 .get("revision", {})
620 .get("relations", {})
621 )
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 )
632 grouped_relations = {
633 "provides": provides,
634 "requires": requires,
635 }
637 return jsonify({"grouped_relations": grouped_relations})
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
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)
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 )
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"]
692 if not resources:
693 return redirect(url_for(".details_resources", entity_name=entity_name))
695 resource = next(
696 (item for item in resources if item["name"] == resource_name), None
697 )
699 if not resource:
700 return redirect(url_for(".details_resources", entity_name=entity_name))
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]
712 revisions = device_gateway.get_resource_revisions(
713 entity_name, resource_name
714 )
715 revisions = sorted(revisions, key=lambda k: k["revision"], reverse=True)
717 # Humanize sizes
718 resource["size"] = humanize.naturalsize(resource["download"]["size"])
719 resource["updated"] = logic.convert_date(resource["created-at"])
721 for revision in revisions:
722 revision["size"] = humanize.naturalsize(revision["download"]["size"])
723 revision["updated"] = logic.convert_date(revision["created-at"])
725 return render_template(
726 "details/resources.html",
727 package=package,
728 channel_requested=channel_request,
729 resource=resource,
730 revisions=revisions,
731 )
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)
742 return render_template(
743 "details/integrate.html",
744 package=package,
745 channel_requested=channel_request,
746 )
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)
754 channel_request = request.args.get("channel")
756 if not package["default-release"]:
757 abort(404)
759 release = package["default-release"]
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
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 )
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 )
811 return svg, 200, {"Content-Type": "image/svg+xml"}
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)
823 package["default-release"]["channel"]["released-at"] = (
824 logic.convert_date(
825 package["default-release"]["channel"]["released-at"]
826 )
827 )
829 button = request.args.get("button")
830 if button and button not in ["black", "white"]:
831 button = None
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 }
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 )
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)
867 package["default-release"]["channel"]["released-at"] = (
868 logic.convert_date(
869 package["default-release"]["channel"]["released-at"]
870 )
871 )
873 libraries = logic.process_libraries(
874 publisher_gateway.get_charm_libraries(entity_name)
875 )
877 context = {
878 "package": package,
879 "libraries": libraries,
880 }
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 )
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
904 try:
905 package = publisher_gateway.get_item_details(
906 entity_name,
907 fields=[
908 "result.media",
909 ],
910 )
911 except StoreApiResponseErrorList:
912 pass
914 if package and package["result"]["media"]:
915 icon_url = package["result"]["media"][0]["url"]
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 )
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
930 try:
931 package = publisher_gateway.get_item_details(
932 entity_name,
933 fields=[
934 "result.media",
935 ],
936 )
937 except StoreApiResponseErrorList:
938 pass
940 if package and package["result"]["media"]:
941 icon_url = package["result"]["media"][0]["url"]
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 )
948 abort(404)
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)
958 if package["type"] != "bundle":
959 return "Requested object should be a bundle", 400
961 return jsonify({"charms": package["store_front"]["bundle"]["charms"]})
964@trace_function
965@store.route("/")
966def store_index():
967 response = make_response(render_template("store.html"))
968 return response