From 4f5d6880213af30b2fee9247cbb7035d73cfb95e Mon Sep 17 00:00:00 2001 From: Tibor Leupold Date: Mon, 25 Oct 2021 15:03:42 -0700 Subject: [PATCH] Add ability for upgrade notification to show the relevant release to the user This now implements the logic to link to the next minor's release notes when such a version difference is computed. If a patch release difference is computed, then those release notes are linked. The utils.tests.js seemed a bit unexpected location, because there is no `utils.js` module. Now the module defining the tests and the one defining the functionality are named similarly, as is common in the rest of the code base. - resolves #7336 - resolves #7405 - resolves #3938 - fixes #8537 Fix issue where upgrade notification was not using translated content. --- CHANGELOG.txt | 3 + .../components/UpgradeNotification/index.js | 53 -- .../includes/initUpgradeNotification.test.js | 67 +++ .../src/includes/initUpgradeNotification.ts | 90 ++++ client/src/index.ts | 2 +- client/src/utils/utils.test.js | 18 - client/src/utils/version.js | 119 ++++- client/src/utils/version.test.js | 473 ++++++++++++++++++ docs/reference/settings.rst | 1 + docs/releases/4.0.md | 3 + .../home/upgrade_notification.html | 20 +- .../admin/tests/test_upgrade_notification.py | 93 ++++ wagtail/admin/views/home.py | 20 +- 13 files changed, 868 insertions(+), 94 deletions(-) delete mode 100644 client/src/components/UpgradeNotification/index.js create mode 100644 client/src/includes/initUpgradeNotification.test.js create mode 100644 client/src/includes/initUpgradeNotification.ts delete mode 100644 client/src/utils/utils.test.js create mode 100644 client/src/utils/version.test.js create mode 100644 wagtail/admin/tests/test_upgrade_notification.py diff --git a/CHANGELOG.txt b/CHANGELOG.txt index a012004249..5e4518f0f9 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -19,6 +19,8 @@ Changelog * Introduce `wagtail.admin.widgets.chooser.BaseChooser` to make it easier to build custom chooser inputs (Matt Westcott) * Introduce JavaScript chooser module, including a SearchController class which encapsulates the standard pattern of re-rendering the results panel in response to search queries and pagination (Matt Westcott) * Add ability to select multiple items at once within bulk actions selections when holding shift on subsequent clicks (Hitansh Shah) + * The upgrade notification, shown to admins on the dashboard if Wagtail is out of date, will now show a more suitable version to update to, not just the latest patch (Tibor Leupold) + * Upgrade notification can now be configured to only show updates when there is a new LTS available via `WAGTAIL_ENABLE_UPDATE_CHECK = 'lts'` (Tibor Leupold) * Fix: Typo in `ResumeWorkflowActionFormatter` message (Stefan Hammer) * Fix: Throw a meaningful error when saving an image to an unrecognised image format (Christian Franke) * Fix: Remove extra padding for headers with breadcrumbs on mobile viewport (Steven Steinwand) @@ -26,6 +28,7 @@ Changelog * Fix: Ensure radio buttons / checkboxes display vertically under Django 4.0 (Matt Westcott) * Fix: Ensure that custom document or image models support custom tag models (Matt Westcott) * Fix: Ensure comments use translated values for their placeholder text (Stefan Hammer) + * Fix: Ensure the upgrade notification, shown to admins on the dashboard if Wagtail is out of date, content is translatable (LB (Ben) Johnston) 3.0 (16.05.2022) diff --git a/client/src/components/UpgradeNotification/index.js b/client/src/components/UpgradeNotification/index.js deleted file mode 100644 index 71133be7af..0000000000 --- a/client/src/components/UpgradeNotification/index.js +++ /dev/null @@ -1,53 +0,0 @@ -import { versionOutOfDate } from '../../utils/version'; - -const initUpgradeNotification = () => { - const container = document.querySelector('[data-upgrade]'); - - if (!container) { - return; - } - - /* - * Expected JSON payload: - * { - * "version" : "1.2.3", // Version number. Can only contain numbers and decimal point. - * "url" : "https://wagtail.org" // Absolute URL to page/file containing release notes or actual package. It's up to you. - * } - */ - const releasesUrl = 'https://releases.wagtail.org/latest.txt'; - const currentVersion = container.dataset.wagtailVersion; - - fetch(releasesUrl, { - referrerPolicy: 'strict-origin-when-cross-origin', - }) - .then((response) => { - if (response.status !== 200) { - // eslint-disable-next-line no-console - console.log( - `Unexpected response from ${releasesUrl}. Status: ${response.status}`, - ); - return false; - } - return response.json(); - }) - .then((data) => { - if ( - data && - data.version && - versionOutOfDate(data.version, currentVersion) - ) { - container.querySelector('[data-upgrade-version]').innerText = - data.version; - container - .querySelector('[data-upgrade-link]') - .setAttribute('href', data.url); - container.style.display = ''; - } - }) - .catch((err) => { - // eslint-disable-next-line no-console - console.log(`Error fetching ${releasesUrl}. Error: ${err}`); - }); -}; - -export { initUpgradeNotification }; diff --git a/client/src/includes/initUpgradeNotification.test.js b/client/src/includes/initUpgradeNotification.test.js new file mode 100644 index 0000000000..80505122f8 --- /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 = ` + + `; + + 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/includes/initUpgradeNotification.ts b/client/src/includes/initUpgradeNotification.ts new file mode 100644 index 0000000000..80233c6d6f --- /dev/null +++ b/client/src/includes/initUpgradeNotification.ts @@ -0,0 +1,90 @@ +import { VersionNumber, VersionDeltaType } from '../utils/version'; + +/** + * Controls the upgrade notification component to request the latest version + * of Wagtail and presents a message to the user if the current version + * is out of date. + * + * Expected JSON payload: + * + * { + * "version": "2.15.2", + * "url": "https://docs.wagtail.io/en/stable/releases/2.15.2.html", + * "minorUrl": "https://docs.wagtail.io/en/stable/releases/2.15.html", + * "lts": { + * "version": "2.12.8", + * "url": "https://docs.wagtail.io/en/stable/releases/2.12.8.html", + * "minorUrl": "https://docs.wagtail.io/en/stable/releases/2.12.html" + * } + * } + */ +const initUpgradeNotification = () => { + const container = document.querySelector( + '[data-upgrade-notification]', + ) as HTMLElement; + + if (!container) return; + + const releasesUrl = 'https://releases.wagtail.io/latest.txt'; + const currentVersion = new VersionNumber(container.dataset.currentVersion); + const showLTSOnly = container.hasAttribute('data-upgrade-lts-only'); + const upgradeVersion = container.querySelector('[data-upgrade-version]'); + const upgradeLink = container.querySelector('[data-upgrade-link]'); + + fetch(releasesUrl, { + referrerPolicy: 'strict-origin-when-cross-origin', + }) + .then((response) => { + if (response.status !== 200) { + // eslint-disable-next-line no-console + console.error( + `Unexpected response from ${releasesUrl}. Status: ${response.status}`, + ); + return false; + } + return response.json(); + }) + .then((payload) => { + let data = payload; + + if (data && data.lts && showLTSOnly) { + data = data.lts; + } + + if (data && data.version) { + const latestVersion = new VersionNumber(data.version); + const versionDelta = currentVersion.howMuchBehind(latestVersion); + + let releaseNotesUrl = null; + if (!versionDelta) { + return; + } + if ( + versionDelta === VersionDeltaType.MAJOR || + versionDelta === VersionDeltaType.MINOR + ) { + releaseNotesUrl = data.minorUrl; + } else { + releaseNotesUrl = data.url; + } + + if (upgradeVersion instanceof HTMLElement) { + upgradeVersion.innerText = [data.version, showLTSOnly ? '(LTS)' : ''] + .join(' ') + .trim(); + } + + if (upgradeLink instanceof HTMLElement) { + upgradeLink.setAttribute('href', releaseNotesUrl || ''); + } + + container.style.display = ''; + } + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.error(`Error fetching ${releasesUrl}. Error: ${err}`); + }); +}; + +export { initUpgradeNotification }; diff --git a/client/src/index.ts b/client/src/index.ts index 75ecdbf14f..3f8b503d25 100644 --- a/client/src/index.ts +++ b/client/src/index.ts @@ -9,5 +9,5 @@ export { default as LoadingSpinner } from './components/LoadingSpinner/LoadingSp export { default as Portal } from './components/Portal/Portal'; export { default as PublicationStatus } from './components/PublicationStatus/PublicationStatus'; export { default as Transition } from './components/Transition/Transition'; -export { initUpgradeNotification } from './components/UpgradeNotification'; export { default as initSkipLink } from './includes/initSkipLink'; +export { initUpgradeNotification } from './includes/initUpgradeNotification'; diff --git a/client/src/utils/utils.test.js b/client/src/utils/utils.test.js deleted file mode 100644 index 02b206dd72..0000000000 --- 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.js b/client/src/utils/version.js index 3e9e075077..9637374ab2 100644 --- a/client/src/utils/version.js +++ b/client/src/utils/version.js @@ -1,20 +1,109 @@ -function compareVersion(versionA, versionB) { - const re = /(\.0)+[^.]*$/; - const va = (versionA + '').replace(re, '').split('.'); - const vb = (versionB + '').replace(re, '').split('.'); - const len = Math.min(va.length, vb.length); - for (let i = 0; i < len; i++) { - const cmp = parseInt(va[i], 10) - parseInt(vb[i], 10); - if (cmp !== 0) { - return cmp; +class VersionNumberFormatError extends Error { + constructor(versionString) { + this.message = `Version number '${versionString}' is not formatted correctly.`; + } +} + +class CanOnlyComparePreReleaseVersionsError extends Error { + constructor() { + this.message = 'Can only compare prerelease versions'; + } +} + +class VersionDeltaType { + static MAJOR = new VersionDeltaType('Major'); + static MINOR = new VersionDeltaType('Minor'); + static PATCH = new VersionDeltaType('Patch'); + static PRE_RELEASE_STEP = new VersionDeltaType('PreReleaseStep'); + static PRE_RELEASE_VERSION = new VersionDeltaType('PreReleaseVersion'); + + constructor(name) { + this.name = name; + } +} + +class VersionNumber { + constructor(versionString) { + const versionRegex = + /^(?\d+)\.{1}(?\d+)((\.{1}(?\d+))|(?a|b|rc){1}(?\d+)){0,1}$/; + const matches = versionString.match(versionRegex); + if (matches === null) { + throw new VersionNumberFormatError(versionString); } + const groups = matches.groups; + + this.major = parseInt(groups.major, 10); + this.minor = parseInt(groups.minor, 10); + this.patch = groups.patch ? parseInt(groups.patch, 10) : 0; + + this.preReleaseStep = groups.preReleaseStep ? groups.preReleaseStep : null; + this.preReleaseVersion = groups.preReleaseVersion + ? parseInt(groups.preReleaseVersion, 10) + : null; } - return va.length - vb.length; + isPreRelease() { + return this.preReleaseStep !== null; + } + + /* + * Check if preReleaseStep of this versionNumber is behind another versionNumber's. + */ + isPreReleaseStepBehind(that) { + if (!this.isPreRelease() || !that.isPreRelease()) { + throw new CanOnlyComparePreReleaseVersionsError(); + } + + if ( + this.preReleaseStep === 'a' && + (that.preReleaseStep === 'b' || that.preReleaseStep === 'rc') + ) { + return true; + } else if (this.preReleaseStep === 'b' && that.preReleaseStep === 'rc') { + return true; + } + return false; + } + + /* + * Get VersionDeltaType that this version is behind the other version passed in. + */ + howMuchBehind(that) { + if (this.major < that.major) { + return VersionDeltaType.MAJOR; + } else if (this.major === that.major && this.minor < that.minor) { + return VersionDeltaType.MINOR; + } else if ( + this.major === that.major && + this.minor === that.minor && + !this.isPreRelease() && + !that.isPreRelease() && + this.patch < that.patch + ) { + return VersionDeltaType.PATCH; + } else if ( + this.major === that.major && + this.minor === that.minor && + this.isPreRelease() + ) { + if (!that.isPreRelease()) { + return VersionDeltaType.MINOR; + } else if (this.isPreReleaseStepBehind(that)) { + return VersionDeltaType.PRE_RELEASE_STEP; + } else if ( + this.preReleaseStep === that.preReleaseStep && + this.preReleaseVersion < that.preReleaseVersion + ) { + return VersionDeltaType.PRE_RELEASE_VERSION; + } + } + return null; + } } -function versionOutOfDate(latestVersion, currentVersion) { - return compareVersion(latestVersion, currentVersion) > 0; -} - -export { compareVersion, versionOutOfDate }; +export { + CanOnlyComparePreReleaseVersionsError, + VersionNumberFormatError, + VersionDeltaType, + VersionNumber, +}; diff --git a/client/src/utils/version.test.js b/client/src/utils/version.test.js new file mode 100644 index 0000000000..89ca9737cc --- /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/docs/reference/settings.rst b/docs/reference/settings.rst index f33caa3a33..6bbce8c87f 100644 --- a/docs/reference/settings.rst +++ b/docs/reference/settings.rst @@ -514,6 +514,7 @@ Wagtail update notifications For admins only, Wagtail performs a check on the dashboard to see if newer releases are available. This also provides the Wagtail team with the hostname of your Wagtail site. If you'd rather not receive update notifications, or if you'd like your site to remain unknown, you can disable it with this setting. +If admins should only be informed of new long term support (LTS) versions, then set this setting to ``"lts"`` (the setting is case-insensitive). Private pages / documents ========================= diff --git a/docs/releases/4.0.md b/docs/releases/4.0.md index 3662aab5df..3837eeb264 100644 --- a/docs/releases/4.0.md +++ b/docs/releases/4.0.md @@ -24,6 +24,8 @@ depth: 1 * Introduce `wagtail.admin.widgets.chooser.BaseChooser` to make it easier to build custom chooser inputs (Matt Westcott) * Introduce JavaScript chooser module, including a SearchController class which encapsulates the standard pattern of re-rendering the results panel in response to search queries and pagination (Matt Westcott) * Add ability to select multiple items at once within bulk actions selections when holding shift on subsequent clicks (Hitansh Shah) + * The upgrade notification, shown to admins on the dashboard if Wagtail is out of date, will now show a more suitable version to update to, not just the latest patch (Tibor Leupold) + * Upgrade notification can now be configured to only show updates when there is a new LTS available via `WAGTAIL_ENABLE_UPDATE_CHECK = 'lts'` (Tibor Leupold) ### Bug fixes @@ -36,6 +38,7 @@ depth: 1 * Ensure radio buttons / checkboxes display vertically under Django 4.0 (Matt Westcott) * Ensure that custom document or image models support custom tag models (Matt Westcott) * Ensure comments use translated values for their placeholder text (Stefan Hammer) + * Ensure the upgrade notification, shown to admins on the dashboard if Wagtail is out of date, content is translatable (LB (Ben) Johnston) ## Upgrade considerations diff --git a/wagtail/admin/templates/wagtailadmin/home/upgrade_notification.html b/wagtail/admin/templates/wagtailadmin/home/upgrade_notification.html index 3d3216cf64..857eefb725 100644 --- a/wagtail/admin/templates/wagtailadmin/home/upgrade_notification.html +++ b/wagtail/admin/templates/wagtailadmin/home/upgrade_notification.html @@ -1,5 +1,17 @@ -{% load wagtailcore_tags wagtailadmin_tags %} - -