kopia lustrzana https://github.com/wagtail/wagtail
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
rodzic
f4d5207fbd
commit
4f5d688021
|
@ -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)
|
||||
|
|
|
@ -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 };
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 };
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
=========================
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
|
@ -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:
|
||||
|
|
Ładowanie…
Reference in New Issue