diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py index 624c1b9a4..9a9ca1e94 100644 --- a/plinth/modules/upgrades/__init__.py +++ b/plinth/modules/upgrades/__init__.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later """FreedomBox app for upgrades.""" +import datetime import logging import os import subprocess @@ -215,12 +216,105 @@ def check_dist_upgrade(_): """Check for upgrade to new stable release.""" if is_dist_upgrade_enabled(): status = distupgrade.get_status() - if status['next_action'] in ('continue', 'ready'): + starting = status['next_action'] in ('continue', 'ready') + dist_upgrade_show_notification(status, starting) + if starting: + logger.info('Starting distribution upgrade - %s', status) privileged.start_dist_upgrade() else: logger.info('Not ready for distribution upgrade - %s', status) +def dist_upgrade_show_notification(status: dict, starting: bool): + """Show various notifications regarding distribution upgrade. + + - Show a notification 60 days, 30 days, 1 week, and 1 day before + distribution upgrade. If a notification is dismissed for any of these + periods don't show again until new period starts. Override any previous + notification. + + - Show a notification just before the distribution upgrade showing that the + process has started. Override any previous notification. + + - Show a notification after the distribution upgrade is completed that it + is done. Override any previous notification. Keep this until it is 60 + days before next distribution upgrade. If user dismisses the + notification, don't show it again. + """ + from plinth.notification import Notification + + try: + note = Notification.get('upgrades-dist-upgrade') + data = note.data + except KeyError: + data = {} + + in_days = None + if status['next_action_date']: + in_days = (status['next_action_date'] - + datetime.datetime.now(tz=datetime.timezone.utc)) + + if in_days is None or in_days > datetime.timedelta(days=60): + for_days = None + elif in_days > datetime.timedelta(days=30): + for_days = 60 # 60 day notification + elif in_days > datetime.timedelta(days=7): + for_days = 30 # 30 day notification + elif in_days > datetime.timedelta(days=1): + for_days = 7 # 1 week notification + else: + for_days = 1 # 1 day notification, or overdue notification + + if status['running']: + # Do nothing while the distribution upgrade is running. + return + + state = 'starting' if starting else 'waiting' + if (not for_days and status['current_codename'] + and data.get('next_codename') == status['current_codename']): + # Previously shown notification's codename is current codename. + # Distribution upgrade was successfully completed. + state = 'done' + + if not status['next_action'] and state != 'done': + # There is no upgrade available, don't show any notification. + return + + if not for_days and data.get('state') == 'done': + # Don't remove notification showing upgrade is complete until next + # distribution upgrade is coming up in 2 months or sooner. + return + + if not for_days and state == 'waiting': + # More than 60 days to next distribution update. Don't show + # notification. + return + + if (for_days == data.get('for_days') and state == data.get('state') + and status['next_codename'] == data.get('next_codename')): + # If the notification was shown for same distribution codename, same + # duration, and same state, then don't show it again. + return + + data = { + 'app_name': 'translate:' + gettext_noop('Software Update'), + 'app_icon': 'fa-refresh', + 'current_codename': status['current_codename'], + 'current_version': status['current_version'], + 'next_codename': status['next_codename'], + 'next_version': status['next_version'], + 'state': state, + 'for_days': for_days, + 'in_days': in_days.days if in_days else None, + } + title = gettext_noop('Distribution Update') + note = Notification.update_or_create( + id='upgrades-dist-upgrade', app_id='upgrades', severity='info', + title=title, body_template='upgrades-dist-upgrade-notification.html', + data=data, group='admin') + note.dismiss(should_dismiss=False) + + def is_backports_requested(): """Return whether user has chosen to activate backports.""" return kvstore.get_default(BACKPORTS_REQUESTED_KEY, False) diff --git a/plinth/modules/upgrades/templates/upgrades-dist-upgrade-notification.html b/plinth/modules/upgrades/templates/upgrades-dist-upgrade-notification.html new file mode 100644 index 000000000..eb6cba373 --- /dev/null +++ b/plinth/modules/upgrades/templates/upgrades-dist-upgrade-notification.html @@ -0,0 +1,48 @@ +{% comment %} +# SPDX-License-Identifier: AGPL-3.0-or-later +{% endcomment %} + +{% load i18n %} +{% load static %} + +
+ {% trans "Distribution Update" %} +
+ +

+ {% url 'help:manual-page' lang='-' page='DebianUpgradeNotes' as dist_upgrade_url %} + {% if data.state == 'starting' %} + {% blocktrans trimmed %} + Distribution update has started. This operation may take several hours. + Most apps will be unavailable during this period. Don't interrupt the + process by shutting down or interrupting power to the machine. + {% endblocktrans %} + {% elif data.state == 'done' %} + {% blocktrans trimmed %} + Distribution update has completed. Reboot the machine, if necessary. + {% endblocktrans %} + {% elif data.for_days == 1 %} + {% blocktrans trimmed %} + Distribution update will start soon. Take a backup of apps and data before + then. See manual page for expected + changes and transitions during the distribution upgrade. + {% endblocktrans %} + {% elif data.for_days %} + {% blocktrans trimmed with in_days=data.in_days %} + Distribution update will start in {{ in_days }} days. Take a backup of + apps and data before then. See manual + page for expected changes and transitions during the distribution upgrade. + {% endblocktrans %} + {% endif %} +

+ +

+ + {% trans "Go to Distribution Update" %} + + + {% trans "Dismiss" %} + +

diff --git a/plinth/modules/upgrades/views.py b/plinth/modules/upgrades/views.py index 7fa4c67c8..67ed3952a 100644 --- a/plinth/modules/upgrades/views.py +++ b/plinth/modules/upgrades/views.py @@ -111,6 +111,8 @@ class DistUpgradeConfirmView(TemplateView): def post(self, request): """Start the distribution upgrade process.""" + status = distupgrade.get_status() + upgrades.dist_upgrade_show_notification(status, starting=True) privileged.start_dist_upgrade() messages.success(request, _('Started distribution update.')) return redirect(reverse_lazy('upgrades:dist-upgrade'))