diff --git a/controlpanel/api/migrations/0050_alter_tool_deprecated_message.py b/controlpanel/api/migrations/0050_alter_tool_deprecated_message.py new file mode 100644 index 000000000..8a7a2409a --- /dev/null +++ b/controlpanel/api/migrations/0050_alter_tool_deprecated_message.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.2 on 2024-12-09 15:07 + +# Third-party +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0049_tool_deprecated_message_tool_is_retired"), + ] + + operations = [ + migrations.AlterField( + model_name="tool", + name="deprecated_message", + field=models.TextField( + blank=True, help_text="If no message is provided, a default message will be used." + ), + ), + ] diff --git a/controlpanel/api/models/tool.py b/controlpanel/api/models/tool.py index 1303fbc2a..36560403e 100644 --- a/controlpanel/api/models/tool.py +++ b/controlpanel/api/models/tool.py @@ -27,6 +27,7 @@ class Tool(TimeStampedModel): "rstudio": "rstudio", "vscode": "vscode", } + DEFAULT_DEPRECATED_MESSAGE = "The selected release has been deprecated and will be retired soon. Please update to a more recent version." # noqa description = models.TextField(blank=True) chart_name = models.CharField(max_length=100, blank=False) @@ -52,7 +53,9 @@ class Tool(TimeStampedModel): ) is_deprecated = models.BooleanField(default=False) - deprecated_message = models.TextField(blank=True) + deprecated_message = models.TextField( + blank=True, help_text="If no message is provided, a default message will be used." + ) is_retired = models.BooleanField(default=False) class Meta(TimeStampedModel.Meta): @@ -83,6 +86,16 @@ def image_tag(self): "{}.image.tag".format(chart_image_key_name) ) + @property + def get_deprecated_message(self): + if not self.is_deprecated: + return "" + + if self.is_retired: + return "" + + return self.deprecated_message or self.DEFAULT_DEPRECATED_MESSAGE + class ToolDeploymentManager: """ diff --git a/controlpanel/frontend/jinja2/release-detail.html b/controlpanel/frontend/jinja2/release-detail.html index 0cd63c6c0..d74dea699 100644 --- a/controlpanel/frontend/jinja2/release-detail.html +++ b/controlpanel/frontend/jinja2/release-detail.html @@ -153,7 +153,7 @@

{{ page_title }}

}, "classes": "govuk-!-width-two-thirds", "hint": { - "text": 'A flag to indicate the release is deprecated and will soon become unavailable to deploy' + "text": 'Checking this will display a deprecation message to users when they select this release' }, "name": "is_deprecated", "items": [ @@ -172,7 +172,7 @@

{{ page_title }}

}, "classes": "govuk-!-width-two-thirds", "hint": { - "text": 'A message to display to users if they select this tool' + "text": form.deprecated_message.help_text }, "name": "deprecated_message", "value": form.deprecated_message.value(), @@ -188,7 +188,7 @@

{{ page_title }}

}, "classes": "govuk-!-width-two-thirds", "hint": { - "text": 'A flag to indicate the release is retired. Users will not be able to deploy this release.' + "text": 'Checking this will remove this release from all users dropdown options on the Your Tools page.' }, "name": "is_retired", "items": [ diff --git a/controlpanel/frontend/jinja2/tool-list.html b/controlpanel/frontend/jinja2/tool-list.html index 8648aecf1..3da29b75c 100644 --- a/controlpanel/frontend/jinja2/tool-list.html +++ b/controlpanel/frontend/jinja2/tool-list.html @@ -41,16 +41,22 @@

{{ tool_info.name }}

{% if deployment and deployment.tool_id > -1 %} {% set installed_chart_version = deployment.description %} {% set installed_release_version = deployment.tool_id %} - {% else %} {% endif %} {% for release_version, release_detail in tool_info["releases"].items(): %} {% if release_version != installed_release_version: %} - {% endif %} {% endfor %} @@ -107,6 +113,8 @@

{{ tool_info.name }}

+

{{ deployment.deprecated_message }}

+ {% if deployment and deployment.tool_id == -1 %}
diff --git a/controlpanel/frontend/static/javascripts/modules/tool-status.js b/controlpanel/frontend/static/javascripts/modules/tool-status.js index 2121c4b35..ddc61b8e9 100644 --- a/controlpanel/frontend/static/javascripts/modules/tool-status.js +++ b/controlpanel/frontend/static/javascripts/modules/tool-status.js @@ -156,5 +156,21 @@ moj.Modules.toolStatus = { // If "(not installed)" or "(installed)" version selected // the "Deploy" button needs to be disabled deployButton.disabled = notInstalledSelected || installedSelected; + + this.toggleDeprecationMessage(selected, targetTool); }, + + toggleDeprecationMessage(selected, targetTool) { + const isDeprecated = selected.attributes["data-is-deprecated"].value === "True"; + const deprecationMessageElement = document.getElementById(targetTool.value + "-deprecation-message"); + const deprecationMessage = selected.attributes["data-deprecated-message"].value; + + if (isDeprecated) { + deprecationMessageElement.firstChild.innerText = deprecationMessage; + deprecationMessageElement.classList.remove(this.hidden); + } else { + deprecationMessageElement.classList.add(this.hidden); + deprecationMessageElement.firstChild.innerText = ""; + } + } }; diff --git a/controlpanel/frontend/views/tool.py b/controlpanel/frontend/views/tool.py index 3cd3f118f..a78e416a1 100644 --- a/controlpanel/frontend/views/tool.py +++ b/controlpanel/frontend/views/tool.py @@ -71,13 +71,15 @@ def _find_related_tool_record(self, chart_name, chart_version, image_tag): """ tool_set = Tool.objects.filter( chart_name=chart_name, version=chart_version, is_restricted=False - ) - for item in tool_set: - if item.image_tag == image_tag: - return item - return tool_set.first() + ).exclude(is_retired=True) + for tool in tool_set: + if tool.image_tag == image_tag: + return tool + # If we cant find a tool with the same image tag, this must mean that it was retired or + # deleted. So return none, and let the calling function handle it + return None - def _add_new_item_to_tool_box(self, user, tool_box, tool, tools_info, charts_info): + def _add_new_item_to_tool_box(self, user, tool_box, tool, tools_info): if tool_box not in tools_info: tools_info[tool_box] = { "name": tool.name, @@ -85,16 +87,19 @@ def _add_new_item_to_tool_box(self, user, tool_box, tool, tools_info, charts_inf "deployment": None, "releases": {}, } - image_tag = tool.image_tag - if not image_tag: - image_tag = charts_info.get(tool.version, {}) or "unknown" + # TODO We should update model to always store an image tag + # image_tag = tool.image_tag + # if not image_tag: + # image_tag = charts_info.get(tool.version, {}) or "unknown" if tool.id not in tools_info[tool_box]["releases"]: tools_info[tool_box]["releases"][tool.id] = { "tool_id": tool.id, "chart_name": tool.chart_name, "description": tool.description, "chart_version": tool.version, - "image_tag": image_tag, + "image_tag": tool.image_tag, + "is_deprecated": tool.is_deprecated, + "deprecated_message": tool.get_deprecated_message, } def _get_tool_deployed_image_tag(self, containers): @@ -103,8 +108,10 @@ def _get_tool_deployed_image_tag(self, containers): return container.image.split(":")[1] return None - def _add_deployed_charts_info(self, tools_info, user, id_token, charts_info): + def _add_deployed_charts_info(self, tools_info, user, id_token): # Get list of deployed tools + # TODO this sets what tool the user currently has deployed. If we were to refactor to store + # deployed tools in the database, we could remove a lot of this logic deployments = cluster.ToolDeployment.get_deployments(user, id_token) for deployment in deployments: chart_name, chart_version = deployment.metadata.labels["chart"].rsplit("-", 1) @@ -119,7 +126,7 @@ def _add_deployed_charts_info(self, tools_info, user, id_token, charts_info): ) ) else: - self._add_new_item_to_tool_box(user, tool_box, tool, tools_info, charts_info) + self._add_new_item_to_tool_box(user, tool_box, tool, tools_info) if tool_box not in tools_info: # up to this stage, if the tool_box is still empty, it means # there is no tool release available in db @@ -131,21 +138,26 @@ def _add_deployed_charts_info(self, tools_info, user, id_token, charts_info): "image_tag": image_tag, "description": tool.description if tool else "Not available", "status": ToolDeployment(tool, user).get_status(id_token, deployment=deployment), + "is_deprecated": tool.is_deprecated if tool else False, + "deprecated_message": tool.get_deprecated_message if tool else "", + "is_retired": tool is None, } - def _retrieve_detail_tool_info(self, user, tools, charts_info): + def _retrieve_detail_tool_info(self, user, tools): + # TODO why do we need this? We could change so that all information required about available tools comes from the DB # noqa: E501 tools_info = {} for tool in tools: # Work out which bucket the chart should be in tool_box = self._locate_tool_box_by_chart_name(tool.chart_name) # No matching tool bucket for the given chart. So ignore. if tool_box: - self._add_new_item_to_tool_box(user, tool_box, tool, tools_info, charts_info) + self._add_new_item_to_tool_box(user, tool_box, tool, tools_info) return tools_info def _get_charts_info(self, tool_list): # We may need the default image_tag from helm chart # unless we configure it specifically in parameters of tool release + # TODO if we make sure that we always have an image_tag for a tool, then building charts_info is redundant and could be removed # noqa: E501 charts_info = {} chart_entries = None for tool in tool_list: @@ -229,14 +241,14 @@ def get_context_data(self, *args, **kwargs): context["managed_airflow_prod_url"] = f"{settings.AWS_SERVICE_URL}/?{args_airflow_prod_url}" # Arrange tools information - charts_info = self._get_charts_info(context["tools"]) - tools_info = self._retrieve_detail_tool_info(user, context["tools"], charts_info) + # charts_info = self._get_charts_info(context["tools"]) + tools_info = self._retrieve_detail_tool_info(user, context["tools"]) if "vscode" in tools_info: url = tools_info["vscode"]["url"] tools_info["vscode"]["url"] = f"{url}?folder=/home/analyticalplatform/workspace" - self._add_deployed_charts_info(tools_info, user, id_token, charts_info) + self._add_deployed_charts_info(tools_info, user, id_token) context["tools_info"] = tools_info return context