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
« 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
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)
147 package = logic.add_overlay_data(package)
149 for channel in package["channel-map"]:
150 channel["channel"]["released-at"] = logic.convert_date(
151 channel["channel"]["released-at"]
152 )
154 return package
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)
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 ]
179 package = get_package(
180 entity_name, channel_request, FIELDS.copy() + extra_fields
181 )
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 }
192 description = None
193 summary = None
195 doc = logic.get_doc_link(package)
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()
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"
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", "")
231 navigation = docs.navigation
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 }
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 ]
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)
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)
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 ]
305 package = get_package(
306 entity_name, channel_request, FIELDS.copy() + extra_fields
307 )
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))
313 docs_url_prefix = f"/{package['name']}/docs"
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()
322 if path:
323 topic_id = None
324 try:
325 topic_id = docs.resolve_path(path)[0]
326 except PathNotFoundError:
327 abort(404)
329 topic = docs.api.get_topic(topic_id)
330 else:
331 topic = docs.index_topic
333 document = docs.parse_topic(topic)
335 navigation = docs.navigation
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 }
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 ]
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 }
381 return render_template("details/docs.html", **context)
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 ]
401 package = get_package(
402 entity_name, channel_request, FIELDS.copy() + extra_fields
403 )
404 subpackage = None
406 if package["type"] == "bundle":
407 bundle_charms = package["store_front"]["bundle"]["charms"]
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 )
420 if path:
421 if not any(d["name"] == path for d in bundle_charms):
422 abort(404)
424 try:
425 subpackage = get_package(path)
426 except StoreApiResponseErrorList:
427 subpackage = None
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 )
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 ]
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 )
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)
466 libraries = logic.process_libraries(
467 publisher_gateway.get_charm_libraries(entity_name)
468 )
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 )
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 )
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)
501 libraries = logic.process_libraries(
502 publisher_gateway.get_charm_libraries(entity_name)
503 )
505 library_id = logic.get_library(library_name, libraries)
507 if not library_id:
508 abort(404)
510 library = publisher_gateway.get_charm_library(entity_name, library_id)
511 docstrings = logic.process_python_docs(library, module_name=library_name)
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}"
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 )
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)
544 libraries = logic.process_libraries(
545 publisher_gateway.get_charm_libraries(entity_name)
546 )
548 library_id = logic.get_library(library_name, libraries)
549 if not library_id:
550 abort(404)
552 library = publisher_gateway.get_charm_library(entity_name, library_id)
553 source_code = library.get("source-code", "")
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 )
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(".")
578 if len(lib_parts) > 2:
579 lib_name = "." + ".".join(lib_parts[-2:])
580 else:
581 lib_name = library_name
583 libraries = logic.process_libraries(
584 publisher_gateway.get_charm_libraries(entity_name)
585 )
587 library = next(
588 (lib for lib in libraries if lib.get("name") == lib_name),
589 None,
590 )
592 if not library:
593 abort(404)
595 library = publisher_gateway.get_charm_library(entity_name, library["id"])
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 )
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)
618 return render_template(
619 "details/integrations.html",
620 package=package,
621 channel_requested=channel_request,
622 )
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)
638 relations = (
639 package.get("default-release", {})
640 .get("revision", {})
641 .get("relations", {})
642 )
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 )
653 grouped_relations = {
654 "provides": provides,
655 "requires": requires,
656 }
658 return jsonify({"grouped_relations": grouped_relations})
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
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)
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 )
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"]
713 if not resources:
714 return redirect(url_for(".details_resources", entity_name=entity_name))
716 resource = next(
717 (item for item in resources if item["name"] == resource_name), None
718 )
720 if not resource:
721 return redirect(url_for(".details_resources", entity_name=entity_name))
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]
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
743 revisions = device_gateway.get_resource_revisions(
744 entity_name, resource_name
745 )
746 revisions = sorted(revisions, key=lambda k: k["revision"], reverse=True)
748 # Humanize sizes
749 resource["size"] = humanize.naturalsize(resource["download"]["size"])
750 resource["updated"] = logic.convert_date(resource["created-at"])
752 for revision in revisions:
753 revision["size"] = humanize.naturalsize(revision["download"]["size"])
754 revision["updated"] = logic.convert_date(revision["created-at"])
756 return render_template(
757 "details/resources.html",
758 package=package,
759 channel_requested=channel_request,
760 resource=resource,
761 revisions=revisions,
762 )
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)
773 return render_template(
774 "details/integrate.html",
775 package=package,
776 channel_requested=channel_request,
777 )
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)
785 channel_request = request.args.get("channel")
787 if not package["default-release"]:
788 abort(404)
790 release = package["default-release"]
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
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 )
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 )
842 return svg, 200, {"Content-Type": "image/svg+xml"}
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)
854 package["default-release"]["channel"]["released-at"] = (
855 logic.convert_date(
856 package["default-release"]["channel"]["released-at"]
857 )
858 )
860 button = request.args.get("button")
861 if button and button not in ["black", "white"]:
862 button = None
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 }
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 )
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)
898 package["default-release"]["channel"]["released-at"] = (
899 logic.convert_date(
900 package["default-release"]["channel"]["released-at"]
901 )
902 )
904 libraries = logic.process_libraries(
905 publisher_gateway.get_charm_libraries(entity_name)
906 )
908 context = {
909 "package": package,
910 "libraries": libraries,
911 }
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 )
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
935 try:
936 package = publisher_gateway.get_item_details(
937 entity_name,
938 fields=[
939 "result.media",
940 ],
941 )
942 except StoreApiResponseErrorList:
943 pass
945 if package and package["result"]["media"]:
946 icon_url = package["result"]["media"][0]["url"]
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 )
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
961 try:
962 package = publisher_gateway.get_item_details(
963 entity_name,
964 fields=[
965 "result.media",
966 ],
967 )
968 except StoreApiResponseErrorList:
969 pass
971 if package and package["result"]["media"]:
972 icon_url = package["result"]["media"][0]["url"]
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 )
979 abort(404)
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)
989 if package["type"] != "bundle":
990 return "Requested object should be a bundle", 400
992 return jsonify({"charms": package["store_front"]["bundle"]["charms"]})
995@trace_function
996@store.route("/")
997def store_index():
998 response = make_response(render_template("store.html"))
999 return response