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.
pull/8577/head
Tibor Leupold 2021-10-25 15:03:42 -07:00 zatwierdzone przez LB (Ben Johnston)
rodzic f4d5207fbd
commit 4f5d688021
13 zmienionych plików z 868 dodań i 94 usunięć

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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,
);
});
});

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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';

Wyświetl plik

@ -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();
});
});
});

Wyświetl plik

@ -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 =
/^(?<major>\d+)\.{1}(?<minor>\d+)((\.{1}(?<patch>\d+))|(?<preReleaseStep>a|b|rc){1}(?<preReleaseVersion>\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,
};

Wyświetl plik

@ -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);
});
});

Wyświetl plik

@ -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
=========================

Wyświetl plik

@ -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

Wyświetl plik

@ -1,5 +1,17 @@
{% load wagtailcore_tags wagtailadmin_tags %}
<div data-upgrade data-wagtail-version="{% wagtail_version %}" class="panel nice-padding panel-upgrade-notification" style="display:none">
<div class="help-block help-warning">{% icon name='warning' %}Wagtail upgrade available. Your version: <strong>{% wagtail_version %}</strong>. New version: <strong data-upgrade-version></strong>. <a data-upgrade-link href="">Read the release notes.</a></div>
{% load i18n wagtailcore_tags wagtailadmin_tags %}
{% wagtail_version as current_version %}
<div
class="w-panel-upgrade-notification panel nice-padding"
data-upgrade-notification
data-current-version="{{ current_version }}"
{% if lts_only %}data-upgrade-lts-only{% endif %}
style="display:none"
>
<div class="help-block help-warning">
{% icon name='warning' %}
{% blocktrans trimmed %}
Wagtail upgrade available. Your version: <strong>{{ current_version }}</strong>. New version: <strong data-upgrade-version></strong>.
{% endblocktrans %}
<a data-upgrade-link href="">{% trans "Read the release notes." %}</a>
</div>
</div>

Wyświetl plik

@ -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)

Wyświetl plik

@ -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: