Skip to content
59 changes: 59 additions & 0 deletions templates/store/snap-details/_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,65 @@ <h5 class="p-muted-heading">Source code</h5>
<hr>
{% 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.
-#}
<h5 class="p-muted-heading">Command &rsaquo; Alias</h5>
<ul class="p-list js-expandable-list">
{%- 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 -%}
<li class="p-list__item {{ invisible_class }}">
{{ alias[0]|safe }} &rsaquo; {{ alias[1]|safe }}
</li>
{%- endfor -%}
{%- if aliases_len > default_visible_aliases -%}
<li class="p-list__item">
<a class="js-toggle-full-list" href="#" title="Display more/less aliases">
{{ show_more }}
</a>
</li>
{%- endif -%}
</ul>
<hr>

<script>
// Handle aliases "show more" button
const list = document.querySelector(".js-expandable-list");
const showMoreButton = document.querySelector(".js-toggle-full-list");
let hasAllElementsDisplayed = false;

const updateAliasesListUI = (displayAllElements, elements) => {
// change button
showMoreButton.innerHTML = displayAllElements ? "{{ show_less }}" : "{{ show_more }}";
// toggle hide classes
for (const element of elements) {
element.classList.toggle('u-hide', !displayAllElements);
}
}

if (list && showMoreButton) {
const listElements = list.querySelectorAll(".js-hidable");
showMoreButton.addEventListener("click", (e) => {
e.preventDefault();
// switch toggle
hasAllElementsDisplayed = !hasAllElementsDisplayed;
updateAliasesListUI(hasAllElementsDisplayed, listElements);
});
}
</script>
{%- endif -%}

{% if links["issues"] %}
<h5 class="p-muted-heading">Report a bug</h5>
<ul class="p-list">
Expand Down
213 changes: 213 additions & 0 deletions tests/store/tests_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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"}]}
Expand All @@ -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": []}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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"],
],
)
16 changes: 16 additions & 0 deletions webapp/store/snap_details_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@
"links",
]

FIELDS_EXTRA_DETAILS = [
"aliases",
]


def snap_details_views(store):
snap_regex = "[a-z0-9-]*[a-z][a-z0-9-]*"
Expand Down Expand Up @@ -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"
Expand Down