Repository: wagtail/wagtail

Issue:
Change release notification to link to latest major release notes
Wagtail has a notifcation on the dashboard that is shown if the Wagtail installation is out of date:

![image](https://user-images.githubusercontent.com/1093808/125284628-95aca180-e311-11eb-82cf-797a38e2d7d1.png)

This notification always links to the latest patch release. So clicking the link in the above screenshot will take the user to https://docs.wagtail.io/en/stable/releases/2.13.3.html

The problem with this is that this Wagtail site is a major version behind, so the release notes of the point release aren't relevant to this user since we only fix new regressions in point releases.

We should instead be pointing to the [2.13 release notes](https://docs.wagtail.io/en/stable/releases/2.13.html), which is much more relevant and interesting to someone on Wagtail 2.12.
Add release channels for update notification
### Issue Summary

Now that there are LTS versions of wagtail add a release channel setting to configure the update message for users and perhaps which users see the upgrade message (I'm always an admin so maybe it already only shows in certain cases)

This request is so that users do not see upgrades if the development team or the business has requested that we only upgrade security patches on the latest LTS version of wagtail. In this case it is not necessary to see every update in between LTS version for our users. 

### Steps to Reproduce

Log into wagtail with a version of wagtail that is not the current version and observe the new upgrade available message. 

Upgrade notification (dashboard/home) is not translated 


### Issue Summary

Upgrade notification is not translated 


### Steps to Reproduce

1. Change your admin user language to non-english 
2. Be on an older version of Wagtail 
3. Sign in as an admin user
4. Observe the admin upgrade notification
5. Expected - it should match the user's language
6. Actual - it is still in English

Any other relevant information. We may no consider this a bug or we may not have addressed it as the translation would contain a lot of HTML. 

* I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: (no)


### Technical details

https://github.com/wagtail/wagtail/blob/33552136559f1d6b33950858e8bd89364087240c/wagtail/admin/templates/wagtailadmin/home/upgrade_notification.html#L4





Discussion:
@gasman please could you add a separate link to the major release notes to https://releases.wagtail.io/latest.txt ?
@kaedroho Sure. I figure we might as well add the data for the latest LTS release at the same time, so that we can potentially add a settings flag for people on LTS releases to ignore notifications for non-LTS upgrades - how about the following structure?

```
{
    "version": "2.13.4",
    "url":     "https://docs.wagtail.io/en/stable/releases/2.13.4.html",
    "majorUrl": "https://docs.wagtail.io/en/stable/releases/2.13.html",
    "lts": {
        "version": "2.11.8",
        "url": "https://docs.wagtail.io/en/stable/releases/2.11.8.html",
        "majorUrl": "https://docs.wagtail.io/en/stable/releases/2.11.html"
    }
}
```

If that looks good, let me know and I'll upload that to mark today's (non-urgent) 2.13.4 release.
@gasman Looks good to me, thanks!
OK, done (but it may take 24h for the cache to clear).
I would like to give this one a shot if it is not in the works yet
Hi @nek4life, thanks for this. The upgrade message is only ever shown to admins. If it's still important for you to hide the message from admins for non-LTS releases, could you please suggest:

 - the message that should be shown when `WAGTAIL_ENABLE_LTS_UPDATE_CHECK` is true
   - when there is a point release to an LTS version that is later than the current version (e.g. you're on 1.8 and 1.8.1 is released)
   - when there is a new LTS release which is later than the current version (e.g. when you're on 1.8 and 2.2 is released)

Perhaps it would be adequate to highlight that the release you're on is an LTS release? E.g. 'Wagtail upgrade available. Your version: 1.8 LTS. New version: 1.13'
@tomdyson that sounds like a pretty solid plan. Ideally we'd like to see if there is a security update available e.g. a point release for the current LTS or if there is a new LTS version available display that version as well.

If that option is on then I would not want to see the next version that is not an LTS version. We have admins who are not necessarily the development team. Why tempt them with new features if our development policy is to only update when a new LTS version is available? :D
> Ideally we'd like to see if there is a security update available e.g. a point release for the current LTS or if there is a new LTS version available display that version as well.

That makes sense. So, if `WAGTAIL_ENABLE_LTS_UPDATE_CHECK` is true:

 - Your version: 1.8 LTS. New version: 1.12 LTS _or_
 - Your version: 1.8 LTS. New version: 1.18.1 _but not_
 - Your version: 1.8 LTS. New version: 1.9

Does that sound right?

> If that option is on then I would not want to see the next version that is not an LTS version. We have admins who are not necessarily the development team. Why tempt them with new features if our development policy is to only update when a new LTS version is available? :D

Not to hijack your ticket, but Wagtail's release schedule (small, regular, frequent updates) is designed to encourage site owners to take advantage of the new features which are being added all the time, at a minimal expense. We target 30 minutes as the maximum time it should take to upgrade from one version to the next.


Resolving patch:
diff --git a/wagtail/admin/views/home.py b/wagtail/admin/views/home.py
--- a/wagtail/admin/views/home.py
+++ b/wagtail/admin/views/home.py
@@ -1,4 +1,5 @@
 import itertools
+from typing import Any, Mapping, Union
 
 from django.conf import settings
 from django.contrib.auth import get_user_model
@@ -34,9 +35,22 @@ class UpgradeNotificationPanel(Component):
     template_name = "wagtailadmin/home/upgrade_notification.html"
     order = 100
 
-    def render_html(self, parent_context):
-        if parent_context["request"].user.is_superuser and getattr(
-            settings, "WAGTAIL_ENABLE_UPDATE_CHECK", True
+    def get_upgrade_check_setting(self) -> Union[bool, str]:
+        return getattr(settings, "WAGTAIL_ENABLE_UPDATE_CHECK", True)
+
+    def upgrade_check_lts_only(self) -> bool:
+        upgrade_check = self.get_upgrade_check_setting()
+        if isinstance(upgrade_check, str) and upgrade_check.lower() == "lts":
+            return True
+        return False
+
+    def get_context_data(self, parent_context: Mapping[str, Any]) -> Mapping[str, Any]:
+        return {"lts_only": self.upgrade_check_lts_only()}
+
+    def render_html(self, parent_context: Mapping[str, Any] = None) -> str:
+        if (
+            parent_context["request"].user.is_superuser
+            and self.get_upgrade_check_setting()
         ):
             return super().render_html(parent_context)
         else:


Test patch:
diff --git a/client/src/includes/initUpgradeNotification.test.js b/client/src/includes/initUpgradeNotification.test.js
new file mode 100644
--- /dev/null
+++ b/client/src/includes/initUpgradeNotification.test.js
@@ -0,0 +1,67 @@
+import { initUpgradeNotification } from './initUpgradeNotification';
+
+// https://stackoverflow.com/a/51045733
+const flushPromises = () => new Promise(setImmediate);
+
+describe('initUpgradeNotification', () => {
+  const version = '2.3';
+
+  document.body.innerHTML = `
+  <div
+    class="panel"
+    id="panel"
+    data-upgrade-notification
+    data-current-version="${version}"
+    style="display:none"
+  >
+      <div class="help-block help-warning">
+          Your version: <strong>${version}</strong>.
+          New version: <strong id="latest-version" data-upgrade-version></strong>.
+          <a href="" id="link" data-upgrade-link>Release notes</a>
+      </div>
+  </div>
+  `;
+
+  it('should show the notification and update the version & link', async () => {
+    const data = {
+      version: '5.15.1',
+      url: 'https://docs.wagtail.org/latest/url',
+      minorUrl: 'https://docs.wagtail.org/latest-minor/url',
+      lts: {
+        version: '5.12.2',
+        url: 'https://docs.wagtail.org/lts/url',
+        minorUrl: 'https://docs.wagtail.org/lts-minor/url',
+      },
+    };
+
+    fetch.mockResponseSuccess(JSON.stringify(data));
+
+    expect(global.fetch).not.toHaveBeenCalled();
+
+    initUpgradeNotification();
+
+    // trigger next browser render cycle
+    await Promise.resolve(true);
+
+    expect(global.fetch).toHaveBeenCalledWith(
+      'https://releases.wagtail.io/latest.txt',
+      { referrerPolicy: 'strict-origin-when-cross-origin' },
+    );
+    expect(document.getElementById('panel').style.display).toBe('none');
+
+    await flushPromises();
+
+    // should remove the hidden class on success
+    expect(document.getElementById('panel').style.display).toBe('');
+
+    // should update the version in the message
+    expect(document.getElementById('latest-version').innerText).toEqual(
+      data.version,
+    );
+
+    // should update the link
+    expect(document.getElementById('link').getAttribute('href')).toEqual(
+      data.minorUrl,
+    );
+  });
+});
diff --git a/client/src/utils/utils.test.js b/client/src/utils/utils.test.js
deleted file mode 100644
--- a/client/src/utils/utils.test.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import { versionOutOfDate } from './version';
-
-describe('wagtail package utils', () => {
-  describe('version.versionOutOfDate', () => {
-    it('compares 1.5 and 2.4 correctly', () => {
-      expect(versionOutOfDate('1.5', '2.4')).toBeFalsy();
-    });
-    it('compares 1.5.4 and 1.5.5 correctly', () => {
-      expect(versionOutOfDate('1.5.4', '1.5.5')).toBeFalsy();
-    });
-    it('compares 1.5 and 1.5 correctly', () => {
-      expect(versionOutOfDate('1.5', '1.5')).toBeFalsy();
-    });
-    it('compares 2.6a0 and 2.4 correctly', () => {
-      expect(versionOutOfDate('2.6a0', '2.4')).toBeTruthy();
-    });
-  });
-});
diff --git a/client/src/utils/version.test.js b/client/src/utils/version.test.js
new file mode 100644
--- /dev/null
+++ b/client/src/utils/version.test.js
@@ -0,0 +1,473 @@
+import {
+  CanOnlyComparePreReleaseVersionsError,
+  VersionNumberFormatError,
+  VersionNumber,
+  VersionDeltaType,
+} from './version';
+
+describe('version.VersionDeltaType', () => {
+  it('types equal themselves', () => {
+    expect(VersionDeltaType.MAJOR).toBe(VersionDeltaType.MAJOR);
+    expect(VersionDeltaType.MINOR).toBe(VersionDeltaType.MINOR);
+    expect(VersionDeltaType.PATCH).toBe(VersionDeltaType.PATCH);
+  });
+
+  it('types do not equal others', () => {
+    expect(VersionDeltaType.MAJOR).not.toBe(VersionDeltaType.MINOR);
+    expect(VersionDeltaType.MAJOR).not.toBe(VersionDeltaType.PATCH);
+    expect(VersionDeltaType.MAJOR).not.toBe(new VersionDeltaType('Other'));
+
+    expect(VersionDeltaType.MINOR).not.toBe(VersionDeltaType.MAJOR);
+    expect(VersionDeltaType.MINOR).not.toBe(VersionDeltaType.PATCH);
+    expect(VersionDeltaType.MINOR).not.toBe(new VersionDeltaType('Other'));
+
+    expect(VersionDeltaType.PATCH).not.toBe(VersionDeltaType.MAJOR);
+    expect(VersionDeltaType.PATCH).not.toBe(VersionDeltaType.MINOR);
+    expect(VersionDeltaType.PATCH).not.toBe(new VersionDeltaType('Other'));
+  });
+});
+
+describe('version.VersionNumber initialisation', () => {
+  it('initialises 1.0', () => {
+    const result = new VersionNumber('1.0');
+
+    expect(result.major).toBe(1);
+    expect(result.minor).toBe(0);
+    expect(result.patch).toBe(0);
+  });
+
+  it('initialises 12.0', () => {
+    const result = new VersionNumber('12.0');
+
+    expect(result.major).toBe(12);
+    expect(result.minor).toBe(0);
+    expect(result.patch).toBe(0);
+  });
+
+  it('initialises 2.1', () => {
+    const result = new VersionNumber('2.1');
+
+    expect(result.major).toBe(2);
+    expect(result.minor).toBe(1);
+    expect(result.patch).toBe(0);
+  });
+
+  it('initialises 2.13', () => {
+    const result = new VersionNumber('2.13');
+
+    expect(result.major).toBe(2);
+    expect(result.minor).toBe(13);
+    expect(result.patch).toBe(0);
+  });
+
+  it('initialises 2.13.0', () => {
+    const result = new VersionNumber('2.13.0');
+
+    expect(result.major).toBe(2);
+    expect(result.minor).toBe(13);
+    expect(result.patch).toBe(0);
+  });
+
+  it('initialises 2.13.1', () => {
+    const result = new VersionNumber('2.13.1');
+
+    expect(result.major).toBe(2);
+    expect(result.minor).toBe(13);
+    expect(result.patch).toBe(1);
+  });
+
+  it('initialises prerelease 2.0a0', () => {
+    const result = new VersionNumber('2.0a0');
+
+    expect(result.major).toBe(2);
+    expect(result.minor).toBe(0);
+    expect(result.patch).toBe(0);
+    expect(result.preReleaseStep).toBe('a');
+    expect(result.preReleaseVersion).toBe(0);
+  });
+
+  it('initialises prerelease 2.12a1', () => {
+    const result = new VersionNumber('2.12a1');
+
+    expect(result.major).toBe(2);
+    expect(result.minor).toBe(12);
+    expect(result.patch).toBe(0);
+    expect(result.preReleaseStep).toBe('a');
+    expect(result.preReleaseVersion).toBe(1);
+  });
+
+  it('initialises prerelease 2.12b2', () => {
+    const result = new VersionNumber('2.12b2');
+
+    expect(result.major).toBe(2);
+    expect(result.minor).toBe(12);
+    expect(result.patch).toBe(0);
+    expect(result.preReleaseStep).toBe('b');
+    expect(result.preReleaseVersion).toBe(2);
+  });
+
+  it('initialises prerelease 2.12rc23', () => {
+    const result = new VersionNumber('2.12rc23');
+
+    expect(result.major).toBe(2);
+    expect(result.minor).toBe(12);
+    expect(result.patch).toBe(0);
+    expect(result.preReleaseStep).toBe('rc');
+    expect(result.preReleaseVersion).toBe(23);
+  });
+
+  it('initialisation throws error for 1', () => {
+    expect(() => new VersionNumber('1')).toThrow(VersionNumberFormatError);
+  });
+
+  it('initialisation throws error for 1a', () => {
+    expect(() => new VersionNumber('1a')).toThrow(VersionNumberFormatError);
+  });
+
+  it('initialisation throws error for 1a0', () => {
+    expect(() => new VersionNumber('1a0')).toThrow(VersionNumberFormatError);
+  });
+
+  it('initialisation throws error for 1.0.0a0', () => {
+    expect(() => new VersionNumber('1.0.0a0')).toThrow(
+      VersionNumberFormatError,
+    );
+  });
+
+  it('initialisation throws error for text string', () => {
+    expect(() => new VersionNumber('not a number')).toThrow(
+      VersionNumberFormatError,
+    );
+  });
+});
+
+describe('version.VersionNumber.isPreRelease', () => {
+  it('responds correctly for 1.0a0', () => {
+    const versionNumber = new VersionNumber('1.0a0');
+    expect(versionNumber.isPreRelease()).toBe(true);
+  });
+
+  it('responds correctly for 1.1a1', () => {
+    const versionNumber = new VersionNumber('1.1a1');
+    expect(versionNumber.isPreRelease()).toBe(true);
+  });
+
+  it('responds correctly for 1.1', () => {
+    const versionNumber = new VersionNumber('1.1');
+    expect(versionNumber.isPreRelease()).toBe(false);
+  });
+
+  it('responds correctly for 1.1.1', () => {
+    const versionNumber = new VersionNumber('1.1.1');
+    expect(versionNumber.isPreRelease()).toBe(false);
+  });
+});
+
+describe('version.VersionNumber.isPreReleaseStepBehind', () => {
+  it('responds correctly for 1.0a0 v 1.0a0', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.0a0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(false);
+  });
+  it('responds correctly for 1.0a0 v 1.0b0', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.0b0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(true);
+  });
+  it('responds correctly for 1.0a0 v 1.0rc0', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.0rc0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(true);
+  });
+  it('responds correctly for 1.0b0 v 1.0a0', () => {
+    const thisVersion = new VersionNumber('1.0b0');
+    const thatVersion = new VersionNumber('1.0a0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(false);
+  });
+  it('responds correctly for 1.0b0 v 1.0b0', () => {
+    const thisVersion = new VersionNumber('1.0b0');
+    const thatVersion = new VersionNumber('1.0b0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(false);
+  });
+  it('responds correctly for 1.0b0 v 1.0rc0', () => {
+    const thisVersion = new VersionNumber('1.0b0');
+    const thatVersion = new VersionNumber('1.0rc0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(true);
+  });
+  it('responds correctly for 1.0rc0 v 1.0a0', () => {
+    const thisVersion = new VersionNumber('1.0rc0');
+    const thatVersion = new VersionNumber('1.0a0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(false);
+  });
+  it('responds correctly for 1.0rc0 v 1.0b0', () => {
+    const thisVersion = new VersionNumber('1.0rc0');
+    const thatVersion = new VersionNumber('1.0b0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(false);
+  });
+  it('responds correctly for 1.0rc0 v 1.0rc0', () => {
+    const thisVersion = new VersionNumber('1.0rc0');
+    const thatVersion = new VersionNumber('1.0rc0');
+    expect(thisVersion.isPreReleaseStepBehind(thatVersion)).toBe(false);
+  });
+
+  it('throws error for this being non-prerelease version', () => {
+    const thisVersion = new VersionNumber('1.0.0');
+    const thatVersion = new VersionNumber('1.0rc0');
+    expect(() => thisVersion.isPreReleaseStepBehind(thatVersion)).toThrowError(
+      CanOnlyComparePreReleaseVersionsError,
+    );
+  });
+  it('throws error for that being non-prerelease version', () => {
+    const thisVersion = new VersionNumber('1.0rc0');
+    const thatVersion = new VersionNumber('1.0.0');
+    expect(() => thisVersion.isPreReleaseStepBehind(thatVersion)).toThrowError(
+      CanOnlyComparePreReleaseVersionsError,
+    );
+  });
+});
+
+describe('version.VersionNumber.howMuchBehind', () => {
+  // MAJOR
+  it('correctly compares 1.0 to 2.0', () => {
+    const thisVersion = new VersionNumber('1.0');
+    const thatVersion = new VersionNumber('2.0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MAJOR);
+  });
+  it('correctly compares 1.2 to 2.1', () => {
+    const thisVersion = new VersionNumber('1.2');
+    const thatVersion = new VersionNumber('2.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MAJOR);
+  });
+  it('correctly compares 1.0rc0 to 2.0', () => {
+    const thisVersion = new VersionNumber('1.0rc0');
+    const thatVersion = new VersionNumber('2.0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MAJOR);
+  });
+
+  // MINOR
+  it('correctly compares 1.0 to 1.1', () => {
+    const thisVersion = new VersionNumber('1.0');
+    const thatVersion = new VersionNumber('1.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MINOR);
+  });
+  it('correctly compares 1.1.0 to 1.2.0', () => {
+    const thisVersion = new VersionNumber('1.1.0');
+    const thatVersion = new VersionNumber('1.2.0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MINOR);
+  });
+  it('correctly compares 1.0a0 to 1.0', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MINOR);
+  });
+  it('correctly compares 1.0a1 to 1.0', () => {
+    const thisVersion = new VersionNumber('1.0a1');
+    const thatVersion = new VersionNumber('1.0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MINOR);
+  });
+  it('correctly compares 1.0a1 to 1.0.1', () => {
+    const thisVersion = new VersionNumber('1.0a1');
+    const thatVersion = new VersionNumber('1.0.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MINOR);
+  });
+  it('correctly compares 1.0a0 to 1.1', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MINOR);
+  });
+  it('correctly compares 1.0a0 to 1.1a0', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.1a0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.MINOR);
+  });
+
+  // PATCH
+  it('correctly compares 1.0.0 to 1.0.1', () => {
+    const thisVersion = new VersionNumber('1.0.0');
+    const thatVersion = new VersionNumber('1.0.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.PATCH);
+  });
+  it('correctly compares 1.0.0 to 1.0.2', () => {
+    const thisVersion = new VersionNumber('1.0.0');
+    const thatVersion = new VersionNumber('1.0.2');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.PATCH);
+  });
+
+  // PRE_RELEASE_STEP
+  it('correctly compares 1.0a0 to 1.0b0', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.0b0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.PRE_RELEASE_STEP);
+  });
+  it('correctly compares 1.0a0 to 1.0rc0', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.0rc0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.PRE_RELEASE_STEP);
+  });
+  it('correctly compares 1.0b0 to 1.0rc0', () => {
+    const thisVersion = new VersionNumber('1.0b0');
+    const thatVersion = new VersionNumber('1.0rc0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.PRE_RELEASE_STEP);
+  });
+
+  // PRE_RELEASE_VERSION
+  it('correctly compares 1.0a0 to 1.0a1', () => {
+    const thisVersion = new VersionNumber('1.0a0');
+    const thatVersion = new VersionNumber('1.0a1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.PRE_RELEASE_VERSION);
+  });
+  it('correctly compares 1.0b0 to 1.0b1', () => {
+    const thisVersion = new VersionNumber('1.0b0');
+    const thatVersion = new VersionNumber('1.0b1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.PRE_RELEASE_VERSION);
+  });
+  it('correctly compares 1.0rc0 to 1.0rc1', () => {
+    const thisVersion = new VersionNumber('1.0rc0');
+    const thatVersion = new VersionNumber('1.0rc1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(VersionDeltaType.PRE_RELEASE_VERSION);
+  });
+
+  // null
+  it('correctly compares 1.0 to 1.0', () => {
+    const thisVersion = new VersionNumber('1.0');
+    const thatVersion = new VersionNumber('1.0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 2.0 to 1.0', () => {
+    const thisVersion = new VersionNumber('2.0');
+    const thatVersion = new VersionNumber('1.0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 1.1 to 1.1', () => {
+    const thisVersion = new VersionNumber('1.1');
+    const thatVersion = new VersionNumber('1.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 1.2 to 1.1', () => {
+    const thisVersion = new VersionNumber('1.2');
+    const thatVersion = new VersionNumber('1.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 2.0 to 1.1', () => {
+    const thisVersion = new VersionNumber('2.0');
+    const thatVersion = new VersionNumber('1.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 1.1.1 to 1.1.1', () => {
+    const thisVersion = new VersionNumber('1.1.1');
+    const thatVersion = new VersionNumber('1.1.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 1.1.2 to 1.1.1', () => {
+    const thisVersion = new VersionNumber('1.1.2');
+    const thatVersion = new VersionNumber('1.1.1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 1.2.1 to 1.1.2', () => {
+    const thisVersion = new VersionNumber('1.2.1');
+    const thatVersion = new VersionNumber('1.1.2');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 1.1a1 to 1.1a1', () => {
+    const thisVersion = new VersionNumber('1.1a1');
+    const thatVersion = new VersionNumber('1.1a1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 1.1b1 to 1.1a1', () => {
+    const thisVersion = new VersionNumber('1.1b1');
+    const thatVersion = new VersionNumber('1.1a1');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+  it('correctly compares 2.0a0 to 1.0', () => {
+    const thisVersion = new VersionNumber('2.0a0');
+    const thatVersion = new VersionNumber('1.0');
+
+    const result = thisVersion.howMuchBehind(thatVersion);
+
+    expect(result).toBe(null);
+  });
+});
diff --git a/wagtail/admin/tests/test_upgrade_notification.py b/wagtail/admin/tests/test_upgrade_notification.py
new file mode 100644
--- /dev/null
+++ b/wagtail/admin/tests/test_upgrade_notification.py
@@ -0,0 +1,93 @@
+from django.test import RequestFactory, TestCase, override_settings
+
+from wagtail.admin.views.home import UpgradeNotificationPanel
+from wagtail.tests.utils import WagtailTestUtils
+
+
+class TestUpgradeNotificationPanel(TestCase, WagtailTestUtils):
+    DATA_ATTRIBUTE_UPGRADE_CHECK = "data-upgrade"
+    DATA_ATTRIBUTE_UPGRADE_CHECK_LTS = "data-upgrade-lts-only"
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.panel = UpgradeNotificationPanel()
+        cls.request_factory = RequestFactory()
+        cls.user = cls.create_user(username="tester")
+        cls.superuser = cls.create_superuser(username="supertester")
+        cls.request = cls.request_factory.get("/")
+
+    def test_get_upgrade_check_setting_default(self):
+        self.assertTrue(self.panel.get_upgrade_check_setting())
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK=False)
+    def test_get_upgrade_check_setting_false(self):
+        self.assertFalse(self.panel.get_upgrade_check_setting())
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK="LTS")
+    def test_get_upgrade_check_setting_LTS(self):
+        self.assertEqual(self.panel.get_upgrade_check_setting(), "LTS")
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK="lts")
+    def test_get_upgrade_check_setting_lts(self):
+        self.assertEqual(self.panel.get_upgrade_check_setting(), "lts")
+
+    def test_upgrade_check_lts_only_default(self):
+        self.assertFalse(self.panel.upgrade_check_lts_only())
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK=False)
+    def test_upgrade_check_lts_only_setting_true(self):
+        self.assertFalse(self.panel.upgrade_check_lts_only())
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK="LTS")
+    def test_upgrade_check_lts_only_setting_LTS(self):
+        self.assertTrue(self.panel.upgrade_check_lts_only())
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK="lts")
+    def test_upgrade_check_lts_only_setting_lts(self):
+        self.assertTrue(self.panel.upgrade_check_lts_only())
+
+    def test_render_html_normal_user(self):
+        self.request.user = self.user
+        parent_context = {"request": self.request}
+
+        result = self.panel.render_html(parent_context)
+
+        self.assertEqual(result, "")
+
+    def test_render_html_superuser(self):
+        self.request.user = self.superuser
+        parent_context = {"request": self.request}
+
+        result = self.panel.render_html(parent_context)
+
+        self.assertIn(self.DATA_ATTRIBUTE_UPGRADE_CHECK, result)
+        self.assertNotIn(self.DATA_ATTRIBUTE_UPGRADE_CHECK_LTS, result)
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK=False)
+    def test_render_html_setting_false(self):
+        self.request.user = self.superuser
+        parent_context = {"request": self.request}
+
+        result = self.panel.render_html(parent_context)
+
+        self.assertEqual(result, "")
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK="LTS")
+    def test_render_html_setting_LTS(self):
+        self.request.user = self.superuser
+        parent_context = {"request": self.request}
+
+        result = self.panel.render_html(parent_context)
+
+        self.assertIn(self.DATA_ATTRIBUTE_UPGRADE_CHECK, result)
+        self.assertIn(self.DATA_ATTRIBUTE_UPGRADE_CHECK_LTS, result)
+
+    @override_settings(WAGTAIL_ENABLE_UPDATE_CHECK="lts")
+    def test_render_html_setting_lts(self):
+        self.request.user = self.superuser
+        parent_context = {"request": self.request}
+
+        result = self.panel.render_html(parent_context)
+
+        self.assertIn(self.DATA_ATTRIBUTE_UPGRADE_CHECK, result)
+        self.assertIn(self.DATA_ATTRIBUTE_UPGRADE_CHECK_LTS, result)