diff --git a/templates/store/snap-details/_details.html b/templates/store/snap-details/_details.html
index 5a44170f04..eac9a8d7e4 100644
--- a/templates/store/snap-details/_details.html
+++ b/templates/store/snap-details/_details.html
@@ -76,6 +76,65 @@
Source code
{% endif %}
+ {%- if aliases -%}
+ {%- set show_more = "Show more" -%}
+ {%- set show_less = "Show less" -%}
+
+ {#-
+ # The list starts displaying maximum 3 elements with the rest hidden.
+ # With the script provided and the link element rendered the user can toggle
+ # displaying and hiding all the aliases.
+ -#}
+ Command › Alias
+
+ {%- set aliases_len = aliases|length -%}
+ {%- set default_visible_aliases = 3 -%}
+ {%- for alias in aliases -%}
+ {%- set invisible_class = "" -%}
+ {%- if loop.index > default_visible_aliases -%}
+ {%- set invisible_class = "js-hidable u-hide" -%}
+ {%- endif -%}
+ -
+ {{ alias[0]|safe }} › {{ alias[1]|safe }}
+
+ {%- endfor -%}
+ {%- if aliases_len > default_visible_aliases -%}
+ -
+
+ {{ show_more }}
+
+
+ {%- endif -%}
+
+
+
+
+ {%- endif -%}
+
{% if links["issues"] %}
Report a bug
diff --git a/tests/store/tests_details.py b/tests/store/tests_details.py
index da322cab24..1bdeb63e89 100644
--- a/tests/store/tests_details.py
+++ b/tests/store/tests_details.py
@@ -4,6 +4,9 @@
from webapp.app import create_app
+EMPTY_EXTRA_DETAILS_PAYLOAD = {"aliases": None, "package_name": "vault"}
+
+
class GetDetailsPageTest(TestCase):
def setUp(self):
self.snap_name = "toto"
@@ -40,6 +43,15 @@ def setUp(self):
]
)
self.endpoint_url = "/" + self.snap_name
+ self.api_url_details = "".join(
+ [
+ "https://api.snapcraft.io/api/v1/",
+ "snaps/details/",
+ self.snap_name,
+ "?",
+ urlencode({"fields": ",".join(["aliases"])}),
+ ]
+ )
def create_app(self):
app = create_app(testing=True)
@@ -48,6 +60,16 @@ def create_app(self):
return app
+ def assert_not_in_context(self, name):
+ try:
+ self.get_context_variable(name)
+ except Exception:
+ # flask-testing throws exception if context doesn't have "name"
+ # that's what we expect so we just return and let the test pass
+ return
+ # If we reach this point it means the variable IS in context
+ self.fail(f"Context variable exists: {name}")
+
@responses.activate
def test_api_404(self):
payload = {"error-list": [{"code": "resource-not-found"}]}
@@ -65,6 +87,77 @@ def test_api_404(self):
assert response.status_code == 404
+ @responses.activate
+ def test_extra_details_404(self):
+ payload = {
+ "snap-id": "id",
+ "name": "toto",
+ "default-track": None,
+ "snap": {
+ "title": "Snap Title",
+ "summary": "This is a summary",
+ "description": "this is a description",
+ "media": [],
+ "license": "license",
+ "publisher": {
+ "display-name": "Toto",
+ "username": "toto",
+ "validation": True,
+ },
+ "categories": [{"name": "test"}],
+ "trending": False,
+ "unlisted": False,
+ "links": {},
+ },
+ "channel-map": [
+ {
+ "channel": {
+ "architecture": "amd64",
+ "name": "stable",
+ "risk": "stable",
+ "track": "latest",
+ "released-at": "2018-09-18T14:45:28.064633+00:00",
+ },
+ "created-at": "2018-09-18T14:45:28.064633+00:00",
+ "version": "1.0",
+ "confinement": "conf",
+ "download": {"size": 100000},
+ }
+ ],
+ }
+ extra_details_payload = {
+ "error_list": [
+ {
+ "code": "resource-not-found",
+ "message": "No snap named 'toto' found in series '16'.",
+ }
+ ],
+ "errors": ["No snap named 'toto' found in series '16'."],
+ "result": "error",
+ }
+
+ responses.add(
+ responses.Response(
+ method="GET", url=self.api_url, json=payload, status=200
+ )
+ )
+ responses.add(
+ responses.Response(
+ method="GET",
+ url=self.api_url_details,
+ json=extra_details_payload,
+ status=404,
+ )
+ )
+
+ response = self.client.get(self.endpoint_url)
+
+ assert len(responses.calls) == 2
+ assert responses.calls[0].request.url == self.api_url
+ assert responses.calls[1].request.url == self.api_url_details
+
+ assert response.status_code == 404
+
@responses.activate
def test_api_500(self):
payload = {"error-list": []}
@@ -125,6 +218,14 @@ def test_no_channel_map(self):
method="GET", url=self.api_url, json=payload, status=200
)
)
+ responses.add(
+ responses.Response(
+ method="GET",
+ url=self.api_url_details,
+ json=EMPTY_EXTRA_DETAILS_PAYLOAD,
+ status=200,
+ )
+ )
response = self.client.get(self.endpoint_url)
@@ -174,6 +275,14 @@ def test_user_connected(self):
method="GET", url=self.api_url, json=payload, status=200
)
)
+ responses.add(
+ responses.Response(
+ method="GET",
+ url=self.api_url_details,
+ json=EMPTY_EXTRA_DETAILS_PAYLOAD,
+ status=200,
+ )
+ )
metrics_url = "https://api.snapcraft.io/api/v1/snaps/metrics"
responses.add(
@@ -239,6 +348,14 @@ def test_user_not_connected(self):
method="GET", url=self.api_url, json=payload, status=200
)
)
+ responses.add(
+ responses.Response(
+ method="GET",
+ url=self.api_url_details,
+ json=EMPTY_EXTRA_DETAILS_PAYLOAD,
+ status=200,
+ )
+ )
metrics_url = "https://api.snapcraft.io/api/v1/snaps/metrics"
responses.add(
@@ -296,6 +413,14 @@ def test_user_connected_on_not_own_snap(self):
method="GET", url=self.api_url, json=payload, status=200
)
)
+ responses.add(
+ responses.Response(
+ method="GET",
+ url=self.api_url_details,
+ json=EMPTY_EXTRA_DETAILS_PAYLOAD,
+ status=200,
+ )
+ )
metrics_url = "https://api.snapcraft.io/api/v1/snaps/metrics"
responses.add(
@@ -311,3 +436,91 @@ def test_user_connected_on_not_own_snap(self):
assert response.status_code == 200
self.assert_context("is_users_snap", False)
+
+ @responses.activate
+ def test_extra_details(self):
+ payload = {
+ "snap-id": "toto_id",
+ "name": "toto",
+ "default-track": None,
+ "snap": {
+ "title": "Snap Title",
+ "summary": "This is a summary",
+ "description": "this is a description",
+ "media": [],
+ "license": "license",
+ "publisher": {
+ "display-name": "Toto",
+ "username": "toto",
+ "validation": True,
+ },
+ "categories": [{"name": "test"}],
+ "trending": False,
+ "unlisted": False,
+ "links": {},
+ },
+ "channel-map": [
+ {
+ "channel": {
+ "architecture": "amd64",
+ "name": "stable",
+ "risk": "stable",
+ "track": "latest",
+ "released-at": "2018-09-18T14:45:28.064633+00:00",
+ },
+ "created-at": "2018-09-18T14:45:28.064633+00:00",
+ "version": "1.0",
+ "confinement": "conf",
+ "download": {"size": 100000},
+ }
+ ],
+ }
+ payload_extra_details = {
+ "aliases": [
+ {"name": "nu", "target": "nu"},
+ {
+ "name": "nu_plugin_stress_internals",
+ "target": "nu-plugin-stress-internals",
+ },
+ {"name": "nu_plugin_gstat", "target": "nu-plugin-gstat"},
+ {"name": "nu_plugin_formats", "target": "nu-plugin-formats"},
+ {"name": "nu_plugin_polars", "target": "nu-plugin-polars"},
+ ],
+ "package_name": "toto",
+ }
+
+ responses.add(
+ responses.Response(
+ method="GET", url=self.api_url, json=payload, status=200
+ )
+ )
+ responses.add(
+ responses.Response(
+ method="GET",
+ url=self.api_url_details,
+ json=payload_extra_details,
+ status=200,
+ )
+ )
+ metrics_url = "https://api.snapcraft.io/api/v1/snaps/metrics"
+ responses.add(
+ responses.Response(
+ method="POST", url=metrics_url, json={}, status=200
+ )
+ )
+
+ response = self.client.get(self.endpoint_url)
+ assert response.status_code == 200
+ self.assert_context(
+ "aliases",
+ [
+ ["toto.nu", "nu"],
+ [
+ "toto.nu-plugin-stress-internals",
+ "nu_plugin_stress_internals",
+ ],
+ ["toto.nu-plugin-gstat", "nu_plugin_gstat"],
+ ["toto.nu-plugin-formats", "nu_plugin_formats"],
+ ["toto.nu-plugin-polars", "nu_plugin_polars"],
+ ],
+ )
diff --git a/webapp/store/snap_details_views.py b/webapp/store/snap_details_views.py
index 592e7017ca..afdd2ff77c 100644
--- a/webapp/store/snap_details_views.py
+++ b/webapp/store/snap_details_views.py
@@ -39,6 +39,10 @@
"links",
]
+FIELDS_EXTRA_DETAILS = [
+ "aliases",
+]
+
def snap_details_views(store):
snap_regex = "[a-z0-9-]*[a-z][a-z0-9-]*"
@@ -208,6 +212,18 @@ def snap_details(snap_name):
status_code = 200
context = _get_context_snap_details(snap_name)
+ extra_details = device_gateway.get_snap_details(
+ snap_name, fields=FIELDS_EXTRA_DETAILS
+ )
+
+ if extra_details and extra_details["aliases"]:
+ context["aliases"] = [
+ [
+ f"{extra_details['package_name']}.{alias_obj['target']}",
+ alias_obj["name"],
+ ]
+ for alias_obj in extra_details["aliases"]
+ ]
country_metric_name = "weekly_installed_base_by_country_percent"
os_metric_name = "weekly_installed_base_by_operating_system_normalized"