mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-05 05:54:44 +00:00
Merge branch 'master' into support-phone-number-links
This commit is contained in:
commit
7e7ca39821
114 changed files with 1799 additions and 670 deletions
13
.travis.yml
13
.travis.yml
|
|
@ -1,6 +1,6 @@
|
|||
language: python
|
||||
cache: pip
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
|
||||
addons:
|
||||
postgresql: "9.6"
|
||||
|
|
@ -17,22 +17,18 @@ matrix:
|
|||
python: 3.6
|
||||
- env: TOXENV=py37-dj21-postgres-noelasticsearch
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
- env: TOXENV=py37-dj22-sqlite-noelasticsearch
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
- env: TOXENV=py37-dj22-mysql-noelasticsearch
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
- env: TOXENV=py37-dj22-postgres-noelasticsearch
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
- env: TOXENV=py37-dj22stable-postgres-noelasticsearch
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
- env: TOXENV=py37-djmaster-postgres-noelasticsearch
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
- env: TOXENV=py38-dj22-postgres-noelasticsearch
|
||||
python: 3.8-dev
|
||||
- env: TOXENV=py36-dj20-sqlite-elasticsearch2 INSTALL_ELASTICSEARCH2=yes
|
||||
python: 3.6
|
||||
- env: TOXENV=py36-dj21-sqlite-elasticsearch2 INSTALL_ELASTICSEARCH2=yes
|
||||
|
|
@ -45,7 +41,6 @@ matrix:
|
|||
python: 3.6
|
||||
- env: TOXENV=py37-dj22-postgres-elasticsearch6 INSTALL_ELASTICSEARCH6=yes
|
||||
python: 3.7
|
||||
dist: xenial
|
||||
allow_failures:
|
||||
# Ignore failures on Elasticsearch tests because ES on Travis is intermittently flaky
|
||||
- env: TOXENV=py36-dj20-sqlite-elasticsearch2 INSTALL_ELASTICSEARCH2=yes
|
||||
|
|
@ -58,6 +53,8 @@ matrix:
|
|||
- env: TOXENV=py37-dj22stable-postgres-noelasticsearch
|
||||
# allow failures against Django master
|
||||
- env: TOXENV=py37-djmaster-postgres-noelasticsearch
|
||||
# allow failures against Python 3.8-dev
|
||||
- env: TOXENV=py38-dj22-postgres-noelasticsearch
|
||||
|
||||
# Services
|
||||
services:
|
||||
|
|
|
|||
|
|
@ -6,9 +6,32 @@ Changelog
|
|||
|
||||
* Added `construct_page_listing_buttons` hook (Michael van Tellingen)
|
||||
* Added more detailed documentation and troubleshooting for installing OpenCV for feature detection (Daniele Procida)
|
||||
* Added Table Block caption for accessibility (Rahmi Pruitt)
|
||||
* Move and refactor upgrade notification JS (Jonny Scholes)
|
||||
* Add ability to insert internal anchor links/links with fragment identifiers in Draftail (rich text) fields (Iman Syed)
|
||||
* Remove need for Elasticsearch `update_all_types` workaround, upgrade minimum release to 6.4.0 or above (Jonathan Liuti)
|
||||
* Upgrade django-modelcluster to>=5.0 and upgrade django-taggit to >=1.0 for Django 3.0 support (Matt Westcott)
|
||||
* Revise tests to ensure all pass in Django 3.0 (Matt Westcott)
|
||||
* Add ability for users to change their own name via the account settings page (Kevin Howbrook)
|
||||
* Fix: Added line breaks to long filenames on multiple image / document uploader (Kevin Howbrook)
|
||||
* Fix: Added https support for Scribd oEmbed provider (Rodrigo)
|
||||
* Fix: Changed StreamField group labels color so labels are visible (Catherine Farman)
|
||||
* Fix: Prevented images with a very wide aspect ratio from being displayed distorted in the rich text editor (Iman Syed)
|
||||
* Fix: Prevent exception when deleting a model with a protected One-to-one relationship (Neal Todd)
|
||||
* Fix: Added labels to snippet bulk edit checkboxes for screen reader users (Martey Dodoo)
|
||||
* Fix: Middleware responses during page preview are now properly returned to the user (Matt Westcott)
|
||||
* Fix: Default text of page links in rich text uses the public page title rather than the admin display title (Andy Chosak)
|
||||
* Fix: Specific page permission checks are now enforced when viewing a page revision (Andy Chosak)
|
||||
* Fix: `pageurl` and `slugurl` tags no longer fail when `request.site` is `None` (Samir Shah)
|
||||
* Fix: Output form media on add/edit image forms with custom models (Matt Westcott)
|
||||
* Fix: Layout for the clear checkbox in default FileField widget (Mikalai Radchuk)
|
||||
* Fix: Remove ASCII conversion from Postgres search backend, to support stemming in non-Latin alphabets (Pavel Denisov)
|
||||
|
||||
|
||||
2.6.1 (05.08.2019)
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Fix: Prevent Javascript errors caused by unescaped quote characters in translation strings (Matt Westcott)
|
||||
|
||||
|
||||
2.6 (01.08.2019)
|
||||
|
|
|
|||
|
|
@ -384,6 +384,12 @@ Contributors
|
|||
* William Blackie
|
||||
* Andrew Miller
|
||||
* Rodrigo
|
||||
* Iman Syed
|
||||
* John Carter
|
||||
* Jonathan Liuti
|
||||
* Rahmi Pruitt
|
||||
* Sanyam Khurana
|
||||
* Pavel Denisov
|
||||
|
||||
Translators
|
||||
===========
|
||||
|
|
|
|||
|
|
@ -105,6 +105,27 @@ select::-ms-expand {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.file_field {
|
||||
.input {
|
||||
label {
|
||||
float: none;
|
||||
display: inline;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
a {
|
||||
&:after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// radio and check boxes
|
||||
input[type=radio],
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@
|
|||
|
||||
@mixin invalid-image-fallback {
|
||||
min-width: 256px;
|
||||
min-height: 100px;
|
||||
min-height: 50px;
|
||||
object-fit: contain;
|
||||
background-color: $color-grey-1;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ const getEmailAddress = mailto => mailto.replace('mailto:', '').split('?')[0];
|
|||
const getPhoneNumber = tel => tel.replace('tel:', '').split('?')[0];
|
||||
const getDomainName = url => url.replace(/(^\w+:|^)\/\//, '').split('/')[0];
|
||||
|
||||
// Determines how to display the link based on its type: page, mail, or external.
|
||||
// Determines how to display the link based on its type: page, mail, anchor or external.
|
||||
export const getLinkAttributes = (data) => {
|
||||
const url = data.url || null;
|
||||
let icon;
|
||||
|
|
@ -33,6 +33,9 @@ export const getLinkAttributes = (data) => {
|
|||
} else if (url.startsWith('tel:')) {
|
||||
icon = LINK_ICON;
|
||||
label = getPhoneNumber(url);
|
||||
} else if (url.startsWith('#')) {
|
||||
icon = LINK_ICON;
|
||||
label = url;
|
||||
} else {
|
||||
icon = LINK_ICON;
|
||||
label = getDomainName(url);
|
||||
|
|
|
|||
|
|
@ -63,6 +63,13 @@ describe('Link', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('anchor', () => {
|
||||
expect(getLinkAttributes({ url: '#testanchor' })).toMatchObject({
|
||||
url: '#testanchor',
|
||||
label: '#testanchor',
|
||||
});
|
||||
});
|
||||
|
||||
it('external', () => {
|
||||
expect(getLinkAttributes({ url: 'http://www.ex.com/' })).toMatchObject({
|
||||
url: 'http://www.ex.com/',
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export const getChooserConfig = (entityType, entity, selectedText) => {
|
|||
allow_external_link: true,
|
||||
allow_email_link: true,
|
||||
allow_phone_link: true,
|
||||
allow_anchor_link: true,
|
||||
link_text: selectedText,
|
||||
};
|
||||
|
||||
|
|
@ -61,6 +62,9 @@ export const getChooserConfig = (entityType, entity, selectedText) => {
|
|||
} else if (data.url.startsWith('tel:')) {
|
||||
url = global.chooserUrls.phoneLinkChooser;
|
||||
urlParams.link_url = data.url.replace('tel:', '');
|
||||
} else if (data.url.startsWith('#')) {
|
||||
url = global.chooserUrls.anchorLinkChooser;
|
||||
urlParams.link_url = data.url.replace('#', '');
|
||||
} else {
|
||||
url = global.chooserUrls.externalLinkChooser;
|
||||
urlParams.link_url = data.url;
|
||||
|
|
|
|||
|
|
@ -144,6 +144,14 @@ describe('ModalWorkflowSource', () => {
|
|||
})).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('anchor', () => {
|
||||
expect(filterEntityData({ type: 'LINK' }, {
|
||||
prefer_this_title_as_link_text: false,
|
||||
title: 'testanchor',
|
||||
url: '#testanchor',
|
||||
})).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('external', () => {
|
||||
expect(filterEntityData({ type: 'LINK' }, {
|
||||
prefer_this_title_as_link_text: false,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`ModalWorkflowSource #filterEntityData LINK anchor 1`] = `
|
||||
Object {
|
||||
"url": "#testanchor",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`ModalWorkflowSource #filterEntityData LINK external 1`] = `
|
||||
Object {
|
||||
"url": "https://www.example.com/",
|
||||
|
|
@ -55,6 +61,7 @@ Object {
|
|||
},
|
||||
"url": "/admin/choose-external-link/",
|
||||
"urlParams": Object {
|
||||
"allow_anchor_link": true,
|
||||
"allow_email_link": true,
|
||||
"allow_external_link": true,
|
||||
"allow_phone_link": true,
|
||||
|
|
@ -72,6 +79,7 @@ Object {
|
|||
},
|
||||
"url": "/admin/choose-email-link/",
|
||||
"urlParams": Object {
|
||||
"allow_anchor_link": true,
|
||||
"allow_email_link": true,
|
||||
"allow_external_link": true,
|
||||
"allow_phone_link": true,
|
||||
|
|
@ -89,6 +97,7 @@ Object {
|
|||
},
|
||||
"url": "/admin/choose-page/",
|
||||
"urlParams": Object {
|
||||
"allow_anchor_link": true,
|
||||
"allow_email_link": true,
|
||||
"allow_external_link": true,
|
||||
"allow_phone_link": true,
|
||||
|
|
@ -105,6 +114,7 @@ Object {
|
|||
},
|
||||
"url": "/admin/choose-page/1/",
|
||||
"urlParams": Object {
|
||||
"allow_anchor_link": true,
|
||||
"allow_email_link": true,
|
||||
"allow_external_link": true,
|
||||
"allow_phone_link": true,
|
||||
|
|
@ -121,6 +131,7 @@ Object {
|
|||
},
|
||||
"url": "/admin/choose-page/",
|
||||
"urlParams": Object {
|
||||
"allow_anchor_link": true,
|
||||
"allow_email_link": true,
|
||||
"allow_external_link": true,
|
||||
"allow_phone_link": true,
|
||||
|
|
|
|||
40
client/src/components/UpgradeNotification/index.js
Normal file
40
client/src/components/UpgradeNotification/index.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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.io" // Absolute URL to page/file containing release notes or actual package. It's up to you.
|
||||
* }
|
||||
*/
|
||||
const releasesUrl = 'https://releases.wagtail.io/latest.txt';
|
||||
const currentVersion = container.dataset.wagtailVersion;
|
||||
|
||||
fetch(releasesUrl).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 };
|
||||
|
|
@ -4,25 +4,27 @@
|
|||
*/
|
||||
|
||||
import Button from './components/Button/Button';
|
||||
import Explorer, { ExplorerToggle, initExplorer } from './components/Explorer';
|
||||
import Icon from './components/Icon/Icon';
|
||||
import PublicationStatus from './components/PublicationStatus/PublicationStatus';
|
||||
import LoadingSpinner from './components/LoadingSpinner/LoadingSpinner';
|
||||
import Portal from './components/Portal/Portal';
|
||||
import PublicationStatus from './components/PublicationStatus/PublicationStatus';
|
||||
import Transition from './components/Transition/Transition';
|
||||
import Explorer, { ExplorerToggle, initExplorer } from './components/Explorer';
|
||||
import { initFocusOutline } from './utils/focus';
|
||||
import { initSubmenus } from './includes/initSubmenus';
|
||||
import { initUpgradeNotification } from './components/UpgradeNotification';
|
||||
|
||||
export {
|
||||
Button,
|
||||
Icon,
|
||||
PublicationStatus,
|
||||
LoadingSpinner,
|
||||
Portal,
|
||||
Transition,
|
||||
Explorer,
|
||||
ExplorerToggle,
|
||||
Icon,
|
||||
LoadingSpinner,
|
||||
Portal,
|
||||
PublicationStatus,
|
||||
Transition,
|
||||
initExplorer,
|
||||
initFocusOutline,
|
||||
initSubmenus,
|
||||
initUpgradeNotification,
|
||||
};
|
||||
|
|
|
|||
18
client/src/utils/utils.test.js
Normal file
18
client/src/utils/utils.test.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
23
client/src/utils/version.js
Normal file
23
client/src/utils/version.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return va.length - vb.length;
|
||||
}
|
||||
|
||||
function versionOutOfDate(latestVersion, currentVersion) {
|
||||
return compareVersion(latestVersion, currentVersion) > 0;
|
||||
}
|
||||
|
||||
export {
|
||||
compareVersion,
|
||||
versionOutOfDate,
|
||||
};
|
||||
|
|
@ -55,6 +55,7 @@ global.wagtail = {};
|
|||
global.chooserUrls = {
|
||||
documentChooser: '/admin/documents/chooser/',
|
||||
emailLinkChooser: '/admin/choose-email-link/',
|
||||
anchorLinkChooser: '/admin/choose-anchor-link',
|
||||
embedsChooser: '/admin/embeds/chooser/',
|
||||
externalLinkChooser: '/admin/choose-external-link/',
|
||||
imageChooser: '/admin/images/chooser/',
|
||||
|
|
|
|||
BIN
docs/_static/images/screen40_table_block.png
vendored
BIN
docs/_static/images/screen40_table_block.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 88 KiB |
|
|
@ -159,6 +159,7 @@ or to add custom validation logic for your models:
|
|||
.. code-block:: python
|
||||
|
||||
from django import forms
|
||||
from django.db import models
|
||||
import geocoder # not in Wagtail, for example only - http://geocoder.readthedocs.io/
|
||||
from wagtail.admin.edit_handlers import FieldPanel
|
||||
from wagtail.admin.forms import WagtailAdminPageForm
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ You can create custom rewrite handlers to support your own new ``linktype`` and
|
|||
|
||||
Required. The ``identifier`` attribute is a string that indicates which rich text tags should be handled by this handler.
|
||||
|
||||
For example, ``PageLinkHandler.get_identifier`` returns the string ``"page"``, indicating that any rich text tags with ``<a linktype="page">`` should be handled by it.
|
||||
For example, ``PageLinkHandler.identifier`` is set to the string ``"page"``, indicating that any rich text tags with ``<a linktype="page">`` should be handled by it.
|
||||
|
||||
.. method:: expand_db_attributes(attrs)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,7 @@
|
|||
Committing code
|
||||
===============
|
||||
|
||||
This section is for the committers of Wagtail,
|
||||
or for anyone interested in the process of getting code committed to Wagtail.
|
||||
**This section is for the core team of Wagtail, or for anyone interested in the process of getting code committed to Wagtail.**
|
||||
|
||||
Code should only be committed after it has been reviewed
|
||||
by at least one other reviewer or committer,
|
||||
|
|
@ -67,6 +66,10 @@ depending on which will be more readable in the commit history.
|
|||
Update ``CHANGELOG.txt`` and release notes
|
||||
==========================================
|
||||
|
||||
.. note::
|
||||
|
||||
This should only be done by core committers, once the changes have been reviewed and accepted.
|
||||
|
||||
Every significant change to Wagtail should get an entry in the ``CHANGELOG.txt``,
|
||||
and the release notes for the current version.
|
||||
|
||||
|
|
|
|||
|
|
@ -716,7 +716,7 @@ If you want to change the content of the email that is sent when a form submits
|
|||
To do this, you need to:
|
||||
|
||||
* Ensure you have your form model defined that extends ``wagtail.contrib.forms.models.AbstractEmailForm``.
|
||||
* In your models.py file, import the ``wagtail.admin.utils.send_mail`` function.
|
||||
* In your models.py file, import the ``wagtail.admin.mail.send_mail`` function.
|
||||
* Override the ``send_mail`` method in your page model.
|
||||
|
||||
Example:
|
||||
|
|
@ -725,7 +725,7 @@ Example:
|
|||
|
||||
from datetime import date
|
||||
# ... additional wagtail imports
|
||||
from wagtail.admin.utils import send_mail
|
||||
from wagtail.admin.mail import send_mail
|
||||
from wagtail.contrib.forms.models import AbstractEmailForm
|
||||
|
||||
|
||||
|
|
@ -763,6 +763,6 @@ Example:
|
|||
# Content is joined with a new line to separate each text line
|
||||
content = '\n'.join(content)
|
||||
|
||||
# wagtail.wagtailadmin.utils - send_mail function is called
|
||||
# wagtail.admin.mail - send_mail function is called
|
||||
# This function extends the Django default send_mail function
|
||||
send_mail(subject, content, addresses, self.from_address)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
TableBlock
|
||||
==========
|
||||
|
||||
The TableBlock module provides an HTML table block type for StreamField. This module uses `handsontable 6.2.2 <https://handsontable.com/>`_ to provide users with the ability to create and edit HTML tables in Wagtail.
|
||||
The TableBlock module provides an HTML table block type for StreamField. This module uses `handsontable 6.2.2 <https://handsontable.com/>`_ to provide users with the ability to create and edit HTML tables in Wagtail. Table blocks provides a caption field for accessibility.
|
||||
|
||||
.. image:: ../../_static/images/screen40_table_block.png
|
||||
|
||||
|
|
@ -83,7 +83,7 @@ Every key in the ``table_options`` dictionary maps to a `handsontable <https://h
|
|||
* `startCols <https://handsontable.com/docs/6.2.2/Options.html#startCols>`_ - The default number of columns for new tables.
|
||||
* `colHeaders <https://handsontable.com/docs/6.2.2/Options.html#colHeaders>`_ - Can be set to ``True`` or ``False``. This setting designates if new tables should be created with column headers. **Note:** this only sets the behaviour for newly created tables. Page editors can override this by checking the the “Column header” checkbox in the table editor in the Wagtail admin.
|
||||
* `rowHeaders <https://handsontable.com/docs/6.2.2/Options.html#rowHeaders>`_ - Operates the same as ``colHeaders`` to designate if new tables should be created with the first column as a row header. Just like ``colHeaders`` this option can be overridden by the page editor in the Wagtail admin.
|
||||
* `contextMenu <https://handsontable.com/docs/6.2.2/Options.html#contextMenu>`_ - Enables or disables the Handsontable right-click menu. By default this is set to ``True``. Alternatively you can provide a list or a dictionary with [specific options](https://handsontable.com/docs/6.2.2/demo-context-menu.html#page-specific).
|
||||
* `contextMenu <https://handsontable.com/docs/6.2.2/Options.html#contextMenu>`_ - Enables or disables the Handsontable right-click menu. By default this is set to ``True``. Alternatively you can provide a list or a dictionary with [specific options](https://handsontable.com/docs/6.2.2/demo-context-menu.html#page-specific).
|
||||
* `editor <https://handsontable.com/docs/6.2.2/Options.html#editor>`_ - Defines the editor used for table cells. The default setting is text.
|
||||
* `stretchH <https://handsontable.com/docs/6.2.2/Options.html#stretchH>`_ - Sets the default horizontal resizing of tables. Options include, 'none', 'last', and 'all'. By default TableBlock uses 'all' for the even resizing of columns.
|
||||
* `height <https://handsontable.com/docs/6.2.2/Options.html#height>`_ - The default height of the grid. By default TableBlock sets the height to ``108`` for the optimal appearance of new tables in the editor. This is optimized for tables with ``startRows`` set to ``3``. If you change the number of ``startRows`` in the configuration, you might need to change the ``height`` setting to improve the default appearance in the editor.
|
||||
|
|
|
|||
16
docs/releases/2.6.1.rst
Normal file
16
docs/releases/2.6.1.rst
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
===========================
|
||||
Wagtail 2.6.1 release notes
|
||||
===========================
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 1
|
||||
|
||||
|
||||
What's new
|
||||
==========
|
||||
|
||||
Bug fixes
|
||||
~~~~~~~~~
|
||||
|
||||
* Prevent Javascript errors caused by unescaped quote characters in translation strings (Matt Westcott)
|
||||
|
|
@ -13,12 +13,22 @@ Wagtail 2.7 is designated a Long Term Support (LTS) release. Long Term Support r
|
|||
What's new
|
||||
==========
|
||||
|
||||
* Upgraded Elasticsearch client library dependency to 6.4.0 or above, 7.0.0 or above is still supported.
|
||||
|
||||
|
||||
Other features
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* Added ``construct_page_listing_buttons`` hook (Michael van Tellingen)
|
||||
* Added more detailed documentation and troubleshooting for installing OpenCV for feature detection (Daniele Procida)
|
||||
* Move and refactor upgrade notification JS (Jonny Scholes)
|
||||
* Remove need for Elasticsearch ``update_all_types`` workaround, upgrade minimum release to 6.4.0 or above (Jonathan Liuti)
|
||||
* Add ability to insert internal anchor links/links with fragment identifiers in Draftail (rich text) fields (Iman Syed)
|
||||
* Added Table Block caption for accessibility (Rahmi Pruitt)
|
||||
* Upgrade django-modelcluster to>=5.0 and upgrade django-taggit to >=1.0 for Django 3.0 support (Matt Westcott)
|
||||
* Revise tests to ensure all pass in Django 3.0 (Matt Westcott)
|
||||
* Add ability for users to change their own name via the account settings page (Kevin Howbrook)
|
||||
|
||||
|
||||
|
||||
Bug fixes
|
||||
|
|
@ -27,7 +37,68 @@ Bug fixes
|
|||
* Added line breaks to long filenames on multiple image / document uploader (Kevin Howbrook)
|
||||
* Added https support for Scribd oEmbed provider (Rodrigo)
|
||||
* Changed StreamField group label color so labels are visible (Catherine Farman)
|
||||
* Prevented images with a very wide aspect ratio from being displayed distorted in the rich text editor (Iman Syed)
|
||||
* Prevent exception when deleting a model with a protected One-to-one relationship (Neal Todd)
|
||||
* Added labels to snippet bulk edit checkboxes for screen reader users (Martey Dodoo)
|
||||
* Middleware responses during page preview are now properly returned to the user (Matt Westcott)
|
||||
* Default text of page links in rich text uses the public page title rather than the admin display title (Andy Chosak)
|
||||
* Specific page permission checks are now enforced when viewing a page revision (Andy Chosak)
|
||||
* ``pageurl`` and ``slugurl`` tags no longer fail when ``request.site`` is ``None`` (Samir Shah)
|
||||
* Output form media on add/edit image forms with custom models (Matt Westcott)
|
||||
* Fixes layout for the clear checkbox in default FileField widget (Mikalai Radchuk)
|
||||
* Remove ASCII conversion from Postgres search backend, to support stemming in non-Latin alphabets (Pavel Denisov)
|
||||
|
||||
|
||||
Upgrade considerations
|
||||
======================
|
||||
|
||||
``Page.dummy_request`` is deprecated
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The internal ``Page.dummy_request`` method (which generates an HTTP request object simulating a real page request, for use in previews) has been deprecated, as it did not correctly handle errors generated during middleware processing. Any code that calls this method to render page previews should be updated to use the new method ``Page.make_preview_request(original_request=None, preview_mode=None)``, which builds the request and calls ``Page.serve_preview`` as a single operation.
|
||||
|
||||
|
||||
``wagtail.admin.utils`` and ``wagtail.admin.decorators`` modules deprecated
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The modules ``wagtail.admin.utils`` and ``wagtail.admin.decorators`` have been deprecated. The helper functions defined here exist primarily for Wagtail's internal use; however, some of them (particularly ``send_mail`` and ``permission_required``) may be found in user code, and import lines will need to be updated. The new locations for these definitions are as follows:
|
||||
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| Definition | Old location | New location |
|
||||
+=================================+==========================+==========================+
|
||||
| any_permission_required | wagtail.admin.utils | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| permission_denied | wagtail.admin.utils | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| permission_required | wagtail.admin.utils | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| PermissionPolicyChecker | wagtail.admin.utils | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| user_has_any_page_permission | wagtail.admin.utils | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| user_passes_test | wagtail.admin.utils | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| users_with_page_permission | wagtail.admin.utils | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| reject_request | wagtail.admin.decorators | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| require_admin_access | wagtail.admin.decorators | wagtail.admin.auth |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| get_available_admin_languages | wagtail.admin.utils | wagtail.admin.locale |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| get_available_admin_time_zones | wagtail.admin.utils | wagtail.admin.locale |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| get_js_translation_strings | wagtail.admin.utils | wagtail.admin.locale |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| WAGTAILADMIN_PROVIDED_LANGUAGES | wagtail.admin.utils | wagtail.admin.locale |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| send_mail | wagtail.admin.utils | wagtail.admin.mail |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| send_notification | wagtail.admin.utils | wagtail.admin.mail |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| get_object_usage | wagtail.admin.utils | wagtail.admin.models |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| popular_tags_for_model | wagtail.admin.utils | wagtail.admin.models |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
| get_site_for_user | wagtail.admin.utils | wagtail.admin.navigation |
|
||||
+---------------------------------+--------------------------+--------------------------+
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Release notes
|
|||
|
||||
upgrading
|
||||
2.7
|
||||
2.6.1
|
||||
2.6
|
||||
2.5.2
|
||||
2.5.1
|
||||
|
|
|
|||
|
|
@ -113,11 +113,12 @@ Prerequisites are the `Elasticsearch`_ service itself and, via pip, the `elastic
|
|||
|
||||
.. code-block:: sh
|
||||
|
||||
pip install "elasticsearch>=6.0.0,<6.3.1" # for Elasticsearch 6.x
|
||||
pip install "elasticsearch>=6.4.0,<7.0.0" # for Elasticsearch 6.x
|
||||
|
||||
.. warning::
|
||||
|
||||
| Version 6.3.1 of the Elasticsearch client library is incompatible with Wagtail. Use 6.3.0 or earlier.
|
||||
| Version 6.3.1 of the Elasticsearch client library is incompatible with Wagtail. Use 6.4.0 or above.
|
||||
|
||||
|
||||
The backend is configured in settings:
|
||||
|
||||
|
|
|
|||
4
setup.py
4
setup.py
|
|
@ -22,8 +22,8 @@ except ImportError:
|
|||
|
||||
install_requires = [
|
||||
"Django>=2.0,<2.3",
|
||||
"django-modelcluster>=4.2,<5.0",
|
||||
"django-taggit>=0.23,<1.0",
|
||||
"django-modelcluster>=5.0,<6.0",
|
||||
"django-taggit>=1.0,<2.0",
|
||||
"django-treebeard>=4.2.0,<5.0",
|
||||
"djangorestframework>=3.7.4,<4.0",
|
||||
"draftjs_exporter>=2.1.5,<3.0",
|
||||
|
|
|
|||
3
tox.ini
3
tox.ini
|
|
@ -36,6 +36,7 @@ basepython =
|
|||
py35: python3.5
|
||||
py36: python3.6
|
||||
py37: python3.7
|
||||
py38: python3.8
|
||||
|
||||
deps =
|
||||
django-sendfile==0.3.6
|
||||
|
|
@ -52,7 +53,7 @@ deps =
|
|||
elasticsearch2: elasticsearch>=2,<3
|
||||
elasticsearch5: elasticsearch>=5,<6
|
||||
elasticsearch5: certifi
|
||||
elasticsearch6: elasticsearch>=6,<6.3.1
|
||||
elasticsearch6: elasticsearch>=6.4.0,<7
|
||||
elasticsearch6: certifi
|
||||
|
||||
setenv =
|
||||
|
|
|
|||
170
wagtail/admin/auth.py
Normal file
170
wagtail/admin/auth.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
from functools import wraps
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.views import redirect_to_login as auth_redirect_to_login
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import activate as activate_tz
|
||||
from django.utils.translation import activate as activate_lang
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.core.models import GroupPagePermission
|
||||
from wagtail.utils import l18n
|
||||
|
||||
|
||||
def users_with_page_permission(page, permission_type, include_superusers=True):
|
||||
# Get user model
|
||||
User = get_user_model()
|
||||
|
||||
# Find GroupPagePermission records of the given type that apply to this page or an ancestor
|
||||
ancestors_and_self = list(page.get_ancestors()) + [page]
|
||||
perm = GroupPagePermission.objects.filter(permission_type=permission_type, page__in=ancestors_and_self)
|
||||
q = Q(groups__page_permissions__in=perm)
|
||||
|
||||
# Include superusers
|
||||
if include_superusers:
|
||||
q |= Q(is_superuser=True)
|
||||
|
||||
return User.objects.filter(is_active=True).filter(q).distinct()
|
||||
|
||||
|
||||
def permission_denied(request):
|
||||
"""Return a standard 'permission denied' response"""
|
||||
if request.is_ajax():
|
||||
raise PermissionDenied
|
||||
|
||||
from wagtail.admin import messages
|
||||
|
||||
messages.error(request, _('Sorry, you do not have permission to access this area.'))
|
||||
return redirect('wagtailadmin_home')
|
||||
|
||||
|
||||
def user_passes_test(test):
|
||||
"""
|
||||
Given a test function that takes a user object and returns a boolean,
|
||||
return a view decorator that denies access to the user if the test returns false.
|
||||
"""
|
||||
def decorator(view_func):
|
||||
# decorator takes the view function, and returns the view wrapped in
|
||||
# a permission check
|
||||
|
||||
@wraps(view_func)
|
||||
def wrapped_view_func(request, *args, **kwargs):
|
||||
if test(request.user):
|
||||
# permission check succeeds; run the view function as normal
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
# permission check failed
|
||||
return permission_denied(request)
|
||||
|
||||
return wrapped_view_func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def permission_required(permission_name):
|
||||
"""
|
||||
Replacement for django.contrib.auth.decorators.permission_required which returns a
|
||||
more meaningful 'permission denied' response than just redirecting to the login page.
|
||||
(The latter doesn't work anyway because Wagtail doesn't define LOGIN_URL...)
|
||||
"""
|
||||
def test(user):
|
||||
return user.has_perm(permission_name)
|
||||
|
||||
# user_passes_test constructs a decorator function specific to the above test function
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
def any_permission_required(*perms):
|
||||
"""
|
||||
Decorator that accepts a list of permission names, and allows the user
|
||||
to pass if they have *any* of the permissions in the list
|
||||
"""
|
||||
def test(user):
|
||||
for perm in perms:
|
||||
if user.has_perm(perm):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
class PermissionPolicyChecker:
|
||||
"""
|
||||
Provides a view decorator that enforces the given permission policy,
|
||||
returning the wagtailadmin 'permission denied' response if permission not granted
|
||||
"""
|
||||
def __init__(self, policy):
|
||||
self.policy = policy
|
||||
|
||||
def require(self, action):
|
||||
def test(user):
|
||||
return self.policy.user_has_permission(user, action)
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
def require_any(self, *actions):
|
||||
def test(user):
|
||||
return self.policy.user_has_any_permission(user, actions)
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
def user_has_any_page_permission(user):
|
||||
"""
|
||||
Check if a user has any permission to add, edit, or otherwise manage any
|
||||
page.
|
||||
"""
|
||||
# Can't do nothin if you're not active.
|
||||
if not user.is_active:
|
||||
return False
|
||||
|
||||
# Superusers can do anything.
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# At least one of the users groups has a GroupPagePermission.
|
||||
# The user can probably do something.
|
||||
if GroupPagePermission.objects.filter(group__in=user.groups.all()).exists():
|
||||
return True
|
||||
|
||||
# Specific permissions for a page type do not mean anything.
|
||||
|
||||
# No luck! This user can not do anything with pages.
|
||||
return False
|
||||
|
||||
|
||||
def reject_request(request):
|
||||
if request.is_ajax():
|
||||
raise PermissionDenied
|
||||
|
||||
return auth_redirect_to_login(
|
||||
request.get_full_path(), login_url=reverse('wagtailadmin_login'))
|
||||
|
||||
|
||||
def require_admin_access(view_func):
|
||||
def decorated_view(request, *args, **kwargs):
|
||||
user = request.user
|
||||
|
||||
if user.is_anonymous:
|
||||
return reject_request(request)
|
||||
|
||||
if user.has_perms(['wagtailadmin.access_admin']):
|
||||
if hasattr(user, 'wagtail_userprofile'):
|
||||
language = user.wagtail_userprofile.get_preferred_language()
|
||||
l18n.set_language(language)
|
||||
activate_lang(language)
|
||||
time_zone = user.wagtail_userprofile.get_current_time_zone()
|
||||
activate_tz(time_zone)
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
if not request.is_ajax():
|
||||
messages.error(request, _('You do not have permission to access the admin'))
|
||||
|
||||
return reject_request(request)
|
||||
|
||||
return decorated_view
|
||||
|
|
@ -274,19 +274,6 @@ class M2MFieldComparison(FieldComparison):
|
|||
|
||||
|
||||
class TagsFieldComparison(M2MFieldComparison):
|
||||
def get_items(self):
|
||||
tags_a = [
|
||||
tag.tag
|
||||
for tag in self.val_a
|
||||
]
|
||||
|
||||
tags_b = [
|
||||
tag.tag
|
||||
for tag in self.val_b
|
||||
]
|
||||
|
||||
return tags_a, tags_b
|
||||
|
||||
def get_item_display(self, tag):
|
||||
return tag.slug
|
||||
|
||||
|
|
|
|||
|
|
@ -1,41 +1,9 @@
|
|||
from django.contrib.auth.views import redirect_to_login as auth_redirect_to_login
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import activate as activate_tz
|
||||
from django.utils.translation import activate as activate_lang
|
||||
from django.utils.translation import ugettext as _
|
||||
import sys
|
||||
from wagtail.utils.deprecation import MovedDefinitionHandler, RemovedInWagtail29Warning
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.utils import l18n
|
||||
MOVED_DEFINITIONS = {
|
||||
'reject_request': 'wagtail.admin.auth',
|
||||
'require_admin_access': 'wagtail.admin.auth',
|
||||
}
|
||||
|
||||
|
||||
def reject_request(request):
|
||||
if request.is_ajax():
|
||||
raise PermissionDenied
|
||||
|
||||
return auth_redirect_to_login(
|
||||
request.get_full_path(), login_url=reverse('wagtailadmin_login'))
|
||||
|
||||
|
||||
def require_admin_access(view_func):
|
||||
def decorated_view(request, *args, **kwargs):
|
||||
user = request.user
|
||||
|
||||
if user.is_anonymous:
|
||||
return reject_request(request)
|
||||
|
||||
if user.has_perms(['wagtailadmin.access_admin']):
|
||||
if hasattr(user, 'wagtail_userprofile'):
|
||||
language = user.wagtail_userprofile.get_preferred_language()
|
||||
l18n.set_language(language)
|
||||
activate_lang(language)
|
||||
time_zone = user.wagtail_userprofile.get_current_time_zone()
|
||||
activate_tz(time_zone)
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
if not request.is_ajax():
|
||||
messages.error(request, _('You do not have permission to access the admin'))
|
||||
|
||||
return reject_request(request)
|
||||
|
||||
return decorated_view
|
||||
sys.modules[__name__] = MovedDefinitionHandler(sys.modules[__name__], MOVED_DEFINITIONS, RemovedInWagtail29Warning)
|
||||
|
|
|
|||
|
|
@ -27,7 +27,12 @@ class URLOrAbsolutePathField(forms.URLField):
|
|||
|
||||
|
||||
class ExternalLinkChooserForm(forms.Form):
|
||||
url = URLOrAbsolutePathField(required=True, label=ugettext_lazy("URL"))
|
||||
url = URLOrAbsolutePathField(required=True, label=ugettext_lazy(""))
|
||||
link_text = forms.CharField(required=False)
|
||||
|
||||
|
||||
class AnchorLinkChooserForm(forms.Form):
|
||||
url = forms.CharField(required=True, label=ugettext_lazy("#"))
|
||||
link_text = forms.CharField(required=False)
|
||||
|
||||
|
||||
|
|
|
|||
116
wagtail/admin/locale.py
Normal file
116
wagtail/admin/locale.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import pytz
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
|
||||
# Wagtail languages with >=90% coverage
|
||||
# This list is manually maintained
|
||||
WAGTAILADMIN_PROVIDED_LANGUAGES = [
|
||||
('ar', ugettext_lazy('Arabic')),
|
||||
('ca', ugettext_lazy('Catalan')),
|
||||
('cs', ugettext_lazy('Czech')),
|
||||
('de', ugettext_lazy('German')),
|
||||
('el', ugettext_lazy('Greek')),
|
||||
('en', ugettext_lazy('English')),
|
||||
('es', ugettext_lazy('Spanish')),
|
||||
('fi', ugettext_lazy('Finnish')),
|
||||
('fr', ugettext_lazy('French')),
|
||||
('gl', ugettext_lazy('Galician')),
|
||||
('hu', ugettext_lazy('Hungarian')),
|
||||
('id-id', ugettext_lazy('Indonesian')),
|
||||
('is-is', ugettext_lazy('Icelandic')),
|
||||
('it', ugettext_lazy('Italian')),
|
||||
('jp', ugettext_lazy('Japanese')),
|
||||
('ko', ugettext_lazy('Korean')),
|
||||
('lt', ugettext_lazy('Lithuanian')),
|
||||
('mn', ugettext_lazy('Mongolian')),
|
||||
('nb', ugettext_lazy('Norwegian Bokmål')),
|
||||
('nl-nl', ugettext_lazy('Netherlands Dutch')),
|
||||
('fa', ugettext_lazy('Persian')),
|
||||
('pl', ugettext_lazy('Polish')),
|
||||
('pt-br', ugettext_lazy('Brazilian Portuguese')),
|
||||
('pt-pt', ugettext_lazy('Portuguese')),
|
||||
('ro', ugettext_lazy('Romanian')),
|
||||
('ru', ugettext_lazy('Russian')),
|
||||
('sv', ugettext_lazy('Swedish')),
|
||||
('sk-sk', ugettext_lazy('Slovak')),
|
||||
('th', ugettext_lazy('Thai')),
|
||||
('uk', ugettext_lazy('Ukrainian')),
|
||||
('zh-hans', ugettext_lazy('Chinese (Simplified)')),
|
||||
('zh-hant', ugettext_lazy('Chinese (Traditional)')),
|
||||
]
|
||||
|
||||
|
||||
# Translatable strings to be made available to Javascript code
|
||||
# as the wagtailConfig.STRINGS object
|
||||
def get_js_translation_strings():
|
||||
return {
|
||||
'DELETE': _('Delete'),
|
||||
'PAGE': _('Page'),
|
||||
'PAGES': _('Pages'),
|
||||
'LOADING': _('Loading…'),
|
||||
'NO_RESULTS': _('No results'),
|
||||
'SERVER_ERROR': _('Server Error'),
|
||||
'SEE_ALL': _('See all'),
|
||||
'CLOSE_EXPLORER': _('Close explorer'),
|
||||
'ALT_TEXT': _('Alt text'),
|
||||
'WRITE_HERE': _('Write here…'),
|
||||
'HORIZONTAL_LINE': _('Horizontal line'),
|
||||
'LINE_BREAK': _('Line break'),
|
||||
'UNDO': _('Undo'),
|
||||
'REDO': _('Redo'),
|
||||
'RELOAD_PAGE': _('Reload the page'),
|
||||
'RELOAD_EDITOR': _('Reload saved content'),
|
||||
'SHOW_LATEST_CONTENT': _('Show latest content'),
|
||||
'SHOW_ERROR': _('Show error'),
|
||||
'EDITOR_CRASH': _('The editor just crashed. Content has been reset to the last saved version.'),
|
||||
'BROKEN_LINK': _('Broken link'),
|
||||
'MISSING_DOCUMENT': _('Missing document'),
|
||||
'CLOSE': _('Close'),
|
||||
'EDIT_PAGE': _('Edit \'{title}\''),
|
||||
'VIEW_CHILD_PAGES_OF_PAGE': _('View child pages of \'{title}\''),
|
||||
'PAGE_EXPLORER': _('Page explorer'),
|
||||
|
||||
'MONTHS': [
|
||||
_('January'),
|
||||
_('February'),
|
||||
_('March'),
|
||||
_('April'),
|
||||
_('May'),
|
||||
_('June'),
|
||||
_('July'),
|
||||
_('August'),
|
||||
_('September'),
|
||||
_('October'),
|
||||
_('November'),
|
||||
_('December')
|
||||
],
|
||||
'WEEKDAYS': [
|
||||
_('Sunday'),
|
||||
_('Monday'),
|
||||
_('Tuesday'),
|
||||
_('Wednesday'),
|
||||
_('Thursday'),
|
||||
_('Friday'),
|
||||
_('Saturday')
|
||||
],
|
||||
'WEEKDAYS_SHORT': [
|
||||
_('Sun'),
|
||||
_('Mon'),
|
||||
_('Tue'),
|
||||
_('Wed'),
|
||||
_('Thu'),
|
||||
_('Fri'),
|
||||
_('Sat')
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def get_available_admin_languages():
|
||||
return getattr(settings, 'WAGTAILADMIN_PERMITTED_LANGUAGES', WAGTAILADMIN_PROVIDED_LANGUAGES)
|
||||
|
||||
|
||||
def get_available_admin_time_zones():
|
||||
return getattr(settings, 'WAGTAIL_USER_TIME_ZONES', pytz.common_timezones)
|
||||
114
wagtail/admin/mail.py
Normal file
114
wagtail/admin/mail.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import get_connection
|
||||
from django.core.mail.message import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import override
|
||||
|
||||
from wagtail.admin.auth import users_with_page_permission
|
||||
from wagtail.core.models import PageRevision
|
||||
from wagtail.users.models import UserProfile
|
||||
|
||||
|
||||
logger = logging.getLogger('wagtail.admin')
|
||||
|
||||
|
||||
def send_mail(subject, message, recipient_list, from_email=None, **kwargs):
|
||||
"""
|
||||
Wrapper around Django's EmailMultiAlternatives as done in send_mail().
|
||||
Custom from_email handling and special Auto-Submitted header.
|
||||
"""
|
||||
if not from_email:
|
||||
if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'):
|
||||
from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL
|
||||
elif hasattr(settings, 'DEFAULT_FROM_EMAIL'):
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
else:
|
||||
from_email = 'webmaster@localhost'
|
||||
|
||||
connection = kwargs.get('connection', False) or get_connection(
|
||||
username=kwargs.get('auth_user', None),
|
||||
password=kwargs.get('auth_password', None),
|
||||
fail_silently=kwargs.get('fail_silently', None),
|
||||
)
|
||||
multi_alt_kwargs = {
|
||||
'connection': connection,
|
||||
'headers': {
|
||||
'Auto-Submitted': 'auto-generated',
|
||||
}
|
||||
}
|
||||
mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, **multi_alt_kwargs)
|
||||
html_message = kwargs.get('html_message', None)
|
||||
if html_message:
|
||||
mail.attach_alternative(html_message, 'text/html')
|
||||
|
||||
return mail.send()
|
||||
|
||||
|
||||
def send_notification(page_revision_id, notification, excluded_user_id):
|
||||
# Get revision
|
||||
revision = PageRevision.objects.get(id=page_revision_id)
|
||||
|
||||
# Get list of recipients
|
||||
if notification == 'submitted':
|
||||
# Get list of publishers
|
||||
include_superusers = getattr(settings, 'WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS', True)
|
||||
recipients = users_with_page_permission(revision.page, 'publish', include_superusers)
|
||||
elif notification in ['rejected', 'approved']:
|
||||
# Get submitter
|
||||
recipients = [revision.user]
|
||||
else:
|
||||
return False
|
||||
|
||||
# Get list of email addresses
|
||||
email_recipients = [
|
||||
recipient for recipient in recipients
|
||||
if recipient.email and recipient.pk != excluded_user_id and getattr(
|
||||
UserProfile.get_for_user(recipient),
|
||||
notification + '_notifications'
|
||||
)
|
||||
]
|
||||
|
||||
# Return if there are no email addresses
|
||||
if not email_recipients:
|
||||
return True
|
||||
|
||||
# Get template
|
||||
template_subject = 'wagtailadmin/notifications/' + notification + '_subject.txt'
|
||||
template_text = 'wagtailadmin/notifications/' + notification + '.txt'
|
||||
template_html = 'wagtailadmin/notifications/' + notification + '.html'
|
||||
|
||||
# Common context to template
|
||||
context = {
|
||||
"revision": revision,
|
||||
"settings": settings,
|
||||
}
|
||||
|
||||
# Send emails
|
||||
sent_count = 0
|
||||
for recipient in email_recipients:
|
||||
try:
|
||||
# update context with this recipient
|
||||
context["user"] = recipient
|
||||
|
||||
# Translate text to the recipient language settings
|
||||
with override(recipient.wagtail_userprofile.get_preferred_language()):
|
||||
# Get email subject and content
|
||||
email_subject = render_to_string(template_subject, context).strip()
|
||||
email_content = render_to_string(template_text, context).strip()
|
||||
|
||||
kwargs = {}
|
||||
if getattr(settings, 'WAGTAILADMIN_NOTIFICATION_USE_HTML', False):
|
||||
kwargs['html_message'] = render_to_string(template_html, context)
|
||||
|
||||
# Send email
|
||||
send_mail(email_subject, email_content, [recipient.email], **kwargs)
|
||||
sent_count += 1
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to send notification email '%s' to %s",
|
||||
email_subject, recipient.email
|
||||
)
|
||||
|
||||
return sent_count == len(email_recipients)
|
||||
|
|
@ -1,5 +1,55 @@
|
|||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Count
|
||||
|
||||
from modelcluster.fields import ParentalKey
|
||||
from taggit.models import Tag
|
||||
|
||||
# The edit_handlers module extends Page with some additional attributes required by
|
||||
# wagtailadmin (namely, base_form_class and get_edit_handler). Importing this within
|
||||
# wagtailadmin.models ensures that this happens in advance of running wagtailadmin's
|
||||
# wagtail admin (namely, base_form_class and get_edit_handler). Importing this within
|
||||
# wagtail.admin.models ensures that this happens in advance of running wagtail.admin's
|
||||
# system checks.
|
||||
from wagtail.admin import edit_handlers # NOQA
|
||||
from wagtail.core.models import Page
|
||||
|
||||
|
||||
def get_object_usage(obj):
|
||||
"Returns a queryset of pages that link to a particular object"
|
||||
|
||||
pages = Page.objects.none()
|
||||
|
||||
# get all the relation objects for obj
|
||||
relations = [f for f in type(obj)._meta.get_fields(include_hidden=True)
|
||||
if (f.one_to_many or f.one_to_one) and f.auto_created]
|
||||
for relation in relations:
|
||||
related_model = relation.related_model
|
||||
|
||||
# if the relation is between obj and a page, get the page
|
||||
if issubclass(related_model, Page):
|
||||
pages |= Page.objects.filter(
|
||||
id__in=related_model._base_manager.filter(**{
|
||||
relation.field.name: obj.id
|
||||
}).values_list('id', flat=True)
|
||||
)
|
||||
else:
|
||||
# if the relation is between obj and an object that has a page as a
|
||||
# property, return the page
|
||||
for f in related_model._meta.fields:
|
||||
if isinstance(f, ParentalKey) and issubclass(f.remote_field.model, Page):
|
||||
pages |= Page.objects.filter(
|
||||
id__in=related_model._base_manager.filter(
|
||||
**{
|
||||
relation.field.name: obj.id
|
||||
}).values_list(f.attname, flat=True)
|
||||
)
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
def popular_tags_for_model(model, count=10):
|
||||
"""Return a queryset of the most frequently used tags used on this model class"""
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
return Tag.objects.filter(
|
||||
taggit_taggeditem_items__content_type=content_type
|
||||
).annotate(
|
||||
item_count=Count('taggit_taggeditem_items')
|
||||
).order_by('-item_count')[:count]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
from django.conf import settings
|
||||
|
||||
from wagtail.core.models import Page
|
||||
|
||||
|
||||
|
|
@ -26,3 +28,19 @@ def get_explorable_root_page(user):
|
|||
root_page = None
|
||||
|
||||
return root_page
|
||||
|
||||
|
||||
def get_site_for_user(user):
|
||||
root_page = get_explorable_root_page(user)
|
||||
if root_page:
|
||||
root_site = root_page.get_site()
|
||||
else:
|
||||
root_site = None
|
||||
real_site_name = None
|
||||
if root_site:
|
||||
real_site_name = root_site.site_name if root_site.site_name else root_site.hostname
|
||||
return {
|
||||
'root_page': root_page,
|
||||
'root_site': root_site,
|
||||
'site_name': real_site_name if real_site_name else settings.WAGTAIL_SITE_NAME,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,8 @@ class HtmlToContentStateHandler(HTMLParser):
|
|||
element_handler.handle_endtag(name, self.state, self.contentstate)
|
||||
|
||||
def handle_data(self, content):
|
||||
# normalise whitespace sequences to a single space
|
||||
# normalise whitespace sequences to a single space unless whitespace is contained in <pre> tag,
|
||||
# in which case, leave it alone
|
||||
# This is in line with https://www.w3.org/TR/html4/struct/text.html#h-9.1
|
||||
content = re.sub(WHITESPACE_RE, ' ', content)
|
||||
|
||||
|
|
@ -341,7 +342,6 @@ class HtmlToContentStateHandler(HTMLParser):
|
|||
content = content.lstrip()
|
||||
elif self.state.leading_whitespace == FORCE_WHITESPACE and not content.startswith(' '):
|
||||
content = ' ' + content
|
||||
|
||||
if content.endswith(' '):
|
||||
# don't output trailing whitespace yet, because we want to discard it if the end
|
||||
# of the block follows. Instead, we'll set leading_whitespace = force so that
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
from django.template.loader import render_to_string
|
||||
|
||||
from wagtail.admin.utils import get_site_for_user, user_has_any_page_permission
|
||||
from wagtail.admin.auth import user_has_any_page_permission
|
||||
from wagtail.admin.navigation import get_site_for_user
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import Page, Site
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {
|
||||
initExplorer,
|
||||
Icon,
|
||||
Portal,
|
||||
initExplorer,
|
||||
initFocusOutline,
|
||||
initSubmenus
|
||||
initSubmenus,
|
||||
initUpgradeNotification,
|
||||
} from 'wagtail-client';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
|
|
@ -35,4 +36,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
|
||||
initFocusOutline();
|
||||
initSubmenus();
|
||||
initUpgradeNotification();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@
|
|||
urlParams = {
|
||||
'allow_external_link': true,
|
||||
'allow_email_link': true,
|
||||
'allow_phone_link': true
|
||||
'allow_phone_link': true,
|
||||
'allow_anchor_link': true,
|
||||
};
|
||||
|
||||
enclosingLink = getEnclosingLink();
|
||||
|
|
@ -61,6 +62,10 @@
|
|||
url = window.chooserUrls.phoneLinkChooser;
|
||||
href = href.replace('tel:', '');
|
||||
urlParams['link_url'] = href;
|
||||
} else if (href.startsWith('#')) {
|
||||
url = window.chooserUrls.anchorLinkChooser;
|
||||
href = href.replace('#', '');
|
||||
urlParams['link_url'] = href;
|
||||
} else if (!linkType) { /* external link */
|
||||
url = window.chooserUrls.externalLinkChooser;
|
||||
urlParams['link_url'] = href;
|
||||
|
|
|
|||
|
|
@ -110,6 +110,18 @@ PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
|
|||
*/
|
||||
$('#id_q', modal.body).trigger('focus');
|
||||
},
|
||||
|
||||
'anchor_link': function(modal, jsonData) {
|
||||
$('p.link-types a', modal.body).on('click', function() {
|
||||
modal.loadUrl(this.href);
|
||||
return false;
|
||||
});
|
||||
|
||||
$('form', modal.body).on('submit', function() {
|
||||
modal.postForm(this.action, $(this).serialize());
|
||||
return false;
|
||||
});
|
||||
},
|
||||
'email_link': function(modal, jsonData) {
|
||||
$('p.link-types a', modal.body).on('click', function() {
|
||||
modal.loadUrl(this.href);
|
||||
|
|
@ -146,5 +158,5 @@ PAGE_CHOOSER_MODAL_ONLOAD_HANDLERS = {
|
|||
'external_link_chosen': function(modal, jsonData) {
|
||||
modal.respond('pageChosen', jsonData['result']);
|
||||
modal.close();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
$(function() {
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* Expected JSON payload:
|
||||
* {
|
||||
* "version" : "1.2.3", // Version number. Can only contain numbers and decimal point.
|
||||
* "url" : "https://wagtail.io" // Absolute URL to page/file containing release notes or actual package. It's up to you.
|
||||
* }
|
||||
*/
|
||||
|
||||
function cmpVersion(a, b) {
|
||||
var i;
|
||||
var cmp;
|
||||
var len;
|
||||
var re = /(\.0)+[^\.]*$/;
|
||||
|
||||
a = (a + '').replace(re, '').split('.');
|
||||
b = (b + '').replace(re, '').split('.');
|
||||
len = Math.min(a.length, b.length);
|
||||
for (i = 0; i < len; i++) {
|
||||
cmp = parseInt(a[i], 10) - parseInt(b[i], 10);
|
||||
if (cmp !== 0) {
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
|
||||
return a.length - b.length;
|
||||
}
|
||||
|
||||
function gtVersion(a, b) {
|
||||
return cmpVersion(a, b) > 0;
|
||||
}
|
||||
|
||||
var releasesUrl = 'https://releases.wagtail.io/latest.txt';
|
||||
var currentVersion = window.wagtailVersion;
|
||||
|
||||
$.getJSON(releasesUrl, function(data) {
|
||||
try {
|
||||
if (data.version && gtVersion(data.version, currentVersion)) {
|
||||
var $container = $('.panel-upgrade-notification')
|
||||
$('.newversion', $container).text(data.version);
|
||||
$('.releasenotes-link', $container).attr('href', data.url);
|
||||
$container.show();
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{% extends "wagtailadmin/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block titletag %}{% trans "Change name" %}{% endblock %}
|
||||
{% block content %}
|
||||
{% trans "Change name" as change_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=change_str %}
|
||||
|
||||
<div class="nice-padding">
|
||||
<form action="{% url 'wagtailadmin_account_change_name' %}" method="POST" novalidate>
|
||||
{% csrf_token %}
|
||||
<ul class="fields">
|
||||
{% for field in form %}
|
||||
{% include "wagtailadmin/shared/field_as_li.html" with field=field %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<input type="submit" value="{% trans 'Change name' %}" class="button" />
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -27,66 +27,7 @@
|
|||
EXTRA_CHILDREN_PARAMETERS: '',
|
||||
};
|
||||
|
||||
wagtailConfig.STRINGS = {
|
||||
DELETE: "{% trans 'Delete' %}",
|
||||
PAGE: "{% trans 'Page' %}",
|
||||
PAGES: "{% trans 'Pages' %}",
|
||||
LOADING: "{% trans 'Loading…' %}",
|
||||
NO_RESULTS: "{% trans 'No results' %}",
|
||||
SERVER_ERROR: "{% trans 'Server Error' %}",
|
||||
SEE_ALL: "{% trans 'See all' %}",
|
||||
CLOSE_EXPLORER: "{% trans 'Close explorer' %}",
|
||||
ALT_TEXT: "{% trans 'Alt text' %}",
|
||||
WRITE_HERE: "{% trans 'Write here…' %}",
|
||||
HORIZONTAL_LINE: "{% trans 'Horizontal line' %}",
|
||||
LINE_BREAK: "{% trans 'Line break' %}",
|
||||
UNDO: "{% trans 'Undo' %}",
|
||||
REDO: "{% trans 'Redo' %}",
|
||||
RELOAD_PAGE: "{% trans 'Reload the page' %}",
|
||||
RELOAD_EDITOR: "{% trans 'Reload saved content' %}",
|
||||
SHOW_LATEST_CONTENT: "{% trans 'Show latest content' %}",
|
||||
SHOW_ERROR: "{% trans 'Show error' %}",
|
||||
EDITOR_CRASH: "{% trans 'The editor just crashed. Content has been reset to the last saved version.' %}",
|
||||
BROKEN_LINK: "{% trans 'Broken link' %}",
|
||||
MISSING_DOCUMENT: "{% trans 'Missing document' %}",
|
||||
CLOSE: "{% trans 'Close' %}",
|
||||
EDIT_PAGE: "{% trans 'Edit \'{title}\'' %}",
|
||||
VIEW_CHILD_PAGES_OF_PAGE: "{% trans 'View child pages of \'{title}\'' %}",
|
||||
PAGE_EXPLORER: "{% trans 'Page explorer' %}",
|
||||
|
||||
MONTHS: [
|
||||
"{% trans 'January' %}",
|
||||
"{% trans 'February' %}",
|
||||
"{% trans 'March' %}",
|
||||
"{% trans 'April' %}",
|
||||
"{% trans 'May' %}",
|
||||
"{% trans 'June' %}",
|
||||
"{% trans 'July' %}",
|
||||
"{% trans 'August' %}",
|
||||
"{% trans 'September' %}",
|
||||
"{% trans 'October' %}",
|
||||
"{% trans 'November' %}",
|
||||
"{% trans 'December' %}"
|
||||
],
|
||||
WEEKDAYS: [
|
||||
"{% trans 'Sunday' %}",
|
||||
"{% trans 'Monday' %}",
|
||||
"{% trans 'Tuesday' %}",
|
||||
"{% trans 'Wednesday' %}",
|
||||
"{% trans 'Thursday' %}",
|
||||
"{% trans 'Friday' %}",
|
||||
"{% trans 'Saturday' %}"
|
||||
],
|
||||
WEEKDAYS_SHORT: [
|
||||
"{% trans 'Sun' %}",
|
||||
"{% trans 'Mon' %}",
|
||||
"{% trans 'Tue' %}",
|
||||
"{% trans 'Wed' %}",
|
||||
"{% trans 'Thu' %}",
|
||||
"{% trans 'Fri' %}",
|
||||
"{% trans 'Sat' %}"
|
||||
]
|
||||
};
|
||||
wagtailConfig.STRINGS = {% js_translation_strings %};
|
||||
|
||||
wagtailConfig.ADMIN_URLS = {
|
||||
PAGES: '{% url "wagtailadmin_explore_root" %}'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{% load i18n wagtailadmin_tags %}
|
||||
{% if allow_external_link or allow_email_link or allow_phone_link or current == 'external' or current == 'email' or current == 'phone' %}
|
||||
{% if allow_external_link or allow_email_link or allow_phone_link or allow_anchor_link or current == 'external' or current == 'email' or current == 'phone' or current == 'anchor' %}
|
||||
<p class="link-types">
|
||||
{% if current == 'internal' %}
|
||||
<b>{% trans "Internal link" %}</b>
|
||||
|
|
@ -24,9 +24,15 @@
|
|||
{% endif %}
|
||||
|
||||
{% if current == 'phone' %}
|
||||
| <b>{% trans "Phone link" %}</b>
|
||||
| <b>{% trans "Phone link" %}</b>
|
||||
{% elif allow_phone_link %}
|
||||
| <a href="{% url 'wagtailadmin_choose_page_phone_link' %}{% querystring p=None parent_page_id=parent_page_id %}">{% trans "Phone link" %}</a>
|
||||
| <a href="{% url 'wagtailadmin_choose_page_phone_link' %}{% querystring p=None parent_page_id=parent_page_id %}">{% trans "Phone link" %}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if current == 'anchor' %}
|
||||
| <b>{% trans "Anchor link" %}</b>
|
||||
{% elif allow_anchor_link %}
|
||||
| <a href="{% url 'wagtailadmin_choose_page_anchor_link' %}{% querystring p=None parent_page_id=parent_page_id %}">{% trans "Anchor link" %}</a>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
{% load i18n wagtailadmin_tags %}
|
||||
{% trans "Add an anchor link" as anchor_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=anchor_str %}
|
||||
|
||||
<div class="nice-padding">
|
||||
{% include 'wagtailadmin/chooser/_link_types.html' with current='anchor' %}
|
||||
|
||||
<form action="{% url 'wagtailadmin_choose_page_anchor_link' %}{% querystring %}" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<ul class="fields">
|
||||
{% for field in form %}
|
||||
{% include "wagtailadmin/shared/field_as_li.html" %}
|
||||
{% endfor %}
|
||||
<li><input type="submit" value="{% trans 'Insert anchor' %}" class="button" /></li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
{% load wagtailcore_tags static %}
|
||||
|
||||
<div class="panel nice-padding panel-upgrade-notification" style="display:none">
|
||||
<div class="help-block help-warning">Wagtail upgrade available. Your version: <strong>{% wagtail_version %}</strong>. New version: <strong class="newversion"></strong>. <a class="releasenotes-link" href="">Read the release notes.</a></div>
|
||||
|
||||
<script>window.wagtailVersion = "{% wagtail_version %}";</script>
|
||||
<script src="{% static 'wagtailadmin/js/upgrade_notify.js' %}" async="true"></script>
|
||||
<div data-upgrade data-wagtail-version="{% wagtail_version %}" class="panel nice-padding panel-upgrade-notification" style="display:none">
|
||||
<div class="help-block help-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>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
'externalLinkChooser': '{% url "wagtailadmin_choose_page_external_link" %}',
|
||||
'emailLinkChooser': '{% url "wagtailadmin_choose_page_email_link" %}',
|
||||
'phoneLinkChooser': '{% url "wagtailadmin_choose_page_phone_link" %}',
|
||||
'anchorLinkChooser': '{% url "wagtailadmin_choose_page_anchor_link" %}',
|
||||
};
|
||||
window.unicodeSlugsEnabled = {% if unicode_slugs_enabled %}true{% else %}false{% endif %};
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Expects a variable 'page', the page instance.
|
|||
|
||||
<div class="title-wrapper">
|
||||
{% if page.can_choose %}
|
||||
<a class="choose-page" href="#{{ page.id|unlocalize }}" data-id="{{ page.id|unlocalize }}" data-title="{{ page.get_admin_display_title }}" data-url="{{ page.url }}" data-parent-id="{{ page.get_parent.id|unlocalize }}" data-edit-url="{% url 'wagtailadmin_pages:edit' page.id %}">{{ page.get_admin_display_title }}</a>
|
||||
<a class="choose-page" href="#{{ page.id|unlocalize }}" data-id="{{ page.id|unlocalize }}" data-title="{{ page.title }}" data-url="{{ page.url }}" data-parent-id="{{ page.get_parent.id|unlocalize }}" data-edit-url="{% url 'wagtailadmin_pages:edit' page.id %}">{{ page.get_admin_display_title }}</a>
|
||||
{% else %}
|
||||
{{ page.get_admin_display_title }}
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import itertools
|
||||
import json
|
||||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
|
|
@ -12,6 +13,7 @@ from django.utils.html import format_html, format_html_join
|
|||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from wagtail.admin.locale import get_js_translation_strings
|
||||
from wagtail.admin.menu import admin_menu
|
||||
from wagtail.admin.navigation import get_explorable_root_page
|
||||
from wagtail.admin.search import admin_search_areas
|
||||
|
|
@ -473,3 +475,8 @@ def avatar_url(user, size=50):
|
|||
return gravatar_url
|
||||
|
||||
return static('wagtailadmin/images/default-user-avatar.png')
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def js_translation_strings():
|
||||
return mark_safe(json.dumps(get_js_translation_strings()))
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import tempfile
|
|||
|
||||
import pytz
|
||||
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
|
|
@ -11,7 +12,7 @@ from django.core import mail
|
|||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from wagtail.admin.utils import (
|
||||
from wagtail.admin.locale import (
|
||||
WAGTAILADMIN_PROVIDED_LANGUAGES, get_available_admin_languages, get_available_admin_time_zones)
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
from wagtail.users.models import UserProfile
|
||||
|
|
@ -309,7 +310,10 @@ class TestAccountSection(TestCase, WagtailTestUtils):
|
|||
|
||||
# Check that a validation error was raised
|
||||
self.assertTrue('new_password2' in response.context['form'].errors.keys())
|
||||
self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2'])
|
||||
if DJANGO_VERSION >= (3, 0):
|
||||
self.assertTrue("The two password fields didn’t match." in response.context['form'].errors['new_password2'])
|
||||
else:
|
||||
self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2'])
|
||||
|
||||
# Check that the password was not changed
|
||||
self.assertTrue(get_user_model().objects.get(pk=self.user.pk).check_password('password'))
|
||||
|
|
@ -407,6 +411,31 @@ class TestAccountSection(TestCase, WagtailTestUtils):
|
|||
# Check that the current language is assumed as English
|
||||
self.assertEqual(profile.get_preferred_language(), "en")
|
||||
|
||||
def test_change_name(self):
|
||||
"""
|
||||
This tests that the change name view responds with a change name page
|
||||
"""
|
||||
# Get change name page
|
||||
response = self.client.get(reverse('wagtailadmin_account_change_name'))
|
||||
|
||||
# Check that the user received a change name page
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/account/change_name.html')
|
||||
|
||||
def test_change_name_post(self):
|
||||
post_data = {
|
||||
'first_name': 'Fox',
|
||||
'last_name': 'Mulder',
|
||||
}
|
||||
response = self.client.post(reverse('wagtailadmin_account_change_name'), post_data)
|
||||
|
||||
# Check that the user was redirected to the account page
|
||||
self.assertRedirects(response, reverse('wagtailadmin_account'))
|
||||
|
||||
# Check that the name was changed
|
||||
self.assertEqual(get_user_model().objects.get(pk=self.user.pk).first_name, post_data['first_name'])
|
||||
self.assertEqual(get_user_model().objects.get(pk=self.user.pk).last_name, post_data['last_name'])
|
||||
|
||||
@override_settings(WAGTAILADMIN_PERMITTED_LANGUAGES=[('en', 'English'), ('es', 'Spanish')])
|
||||
def test_available_admin_languages_with_permitted_languages(self):
|
||||
self.assertListEqual(get_available_admin_languages(), [('en', 'English'), ('es', 'Spanish')])
|
||||
|
|
@ -692,7 +721,12 @@ class TestPasswordReset(TestCase, WagtailTestUtils):
|
|||
self.password_reset_uid = force_text(urlsafe_base64_encode(force_bytes(self.user.pk)))
|
||||
|
||||
# Create url_args
|
||||
self.url_kwargs = dict(uidb64=self.password_reset_uid, token=auth_views.INTERNAL_RESET_URL_TOKEN)
|
||||
if DJANGO_VERSION >= (3, 0):
|
||||
token = auth_views.PasswordResetConfirmView.reset_url_token
|
||||
else:
|
||||
token = auth_views.INTERNAL_RESET_URL_TOKEN
|
||||
|
||||
self.url_kwargs = dict(uidb64=self.password_reset_uid, token=token)
|
||||
|
||||
# Add token to session object
|
||||
s = self.client.session
|
||||
|
|
@ -772,7 +806,11 @@ class TestPasswordReset(TestCase, WagtailTestUtils):
|
|||
|
||||
# Check that a validation error was raised
|
||||
self.assertTrue('new_password2' in response.context['form'].errors.keys())
|
||||
self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2'])
|
||||
|
||||
if DJANGO_VERSION >= (3, 0):
|
||||
self.assertTrue("The two password fields didn’t match." in response.context['form'].errors['new_password2'])
|
||||
else:
|
||||
self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2'])
|
||||
|
||||
# Check that the password was not changed
|
||||
self.assertTrue(get_user_model().objects.get(username='test').check_password('password'))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from django.template import Context, Template
|
|||
from django.test import RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from wagtail.admin.utils import user_has_any_page_permission
|
||||
from wagtail.admin.auth import user_has_any_page_permission
|
||||
from wagtail.core.models import Site
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
|
||||
|
|
|
|||
|
|
@ -208,7 +208,13 @@ class TestChooserBrowseChild(TestCase, WagtailTestUtils):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/chooser/browse.html')
|
||||
|
||||
self.assertInHTML("foobarbaz (simple page)", response.json().get('html'))
|
||||
html = response.json().get('html')
|
||||
self.assertInHTML("foobarbaz (simple page)", html)
|
||||
|
||||
# The data-title attribute should not use the custom admin display title,
|
||||
# because JS code that uses that attribute (e.g. the rich text editor)
|
||||
# should use the real page title.
|
||||
self.assertIn('data-title="foobarbaz"', html)
|
||||
|
||||
def test_parent_with_admin_display_title(self):
|
||||
# Add another child under child_page so it renders a chooser list
|
||||
|
|
@ -566,6 +572,66 @@ class TestChooserExternalLink(TestCase, WagtailTestUtils):
|
|||
self.assertEqual(response_json['result']['title'], "admin")
|
||||
|
||||
|
||||
class TestChooserAnchorLink(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
||||
def get(self, params={}):
|
||||
return self.client.get(reverse('wagtailadmin_choose_page_anchor_link'), params)
|
||||
|
||||
def post(self, post_data={}, url_params={}):
|
||||
url = reverse('wagtailadmin_choose_page_anchor_link')
|
||||
if url_params:
|
||||
url += '?' + urlencode(url_params)
|
||||
return self.client.post(url, post_data)
|
||||
|
||||
def test_simple(self):
|
||||
response = self.get()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailadmin/chooser/anchor_link.html')
|
||||
|
||||
def test_prepopulated_form(self):
|
||||
response = self.get({'link_text': 'Example Anchor Text', 'link_url': 'exampleanchor'})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, 'Example Anchor Text')
|
||||
self.assertContains(response, 'exampleanchor')
|
||||
|
||||
def test_create_link(self):
|
||||
response = self.post({'anchor-link-chooser-url': 'exampleanchor', 'anchor-link-chooser-link_text': 'Example Anchor Text'})
|
||||
result = json.loads(response.content.decode())['result']
|
||||
self.assertEqual(result['url'], "#exampleanchor")
|
||||
self.assertEqual(result['title'], "Example Anchor Text") # When link text is given, it is used
|
||||
self.assertEqual(result['prefer_this_title_as_link_text'], True)
|
||||
|
||||
def test_create_link_without_text(self):
|
||||
response = self.post({'anchor-link-chooser-url': 'exampleanchor'})
|
||||
result = json.loads(response.content.decode())['result']
|
||||
self.assertEqual(result['url'], "#exampleanchor")
|
||||
self.assertEqual(result['title'], "exampleanchor") # When no link text is given, it uses anchor
|
||||
self.assertEqual(result['prefer_this_title_as_link_text'], False)
|
||||
|
||||
def test_notice_changes_to_link_text(self):
|
||||
response = self.post(
|
||||
{'anchor-link-chooser-url': 'exampleanchor2', 'email-link-chooser-link_text': 'Example Text'}, # POST data
|
||||
{'link_url': 'exampleanchor2', 'link_text': 'Example Text'} # GET params - initial data
|
||||
)
|
||||
result = json.loads(response.content.decode())['result']
|
||||
self.assertEqual(result['url'], "#exampleanchor2")
|
||||
self.assertEqual(result['title'], "exampleanchor2")
|
||||
# no change to link text, so prefer the existing link/selection content where available
|
||||
self.assertEqual(result['prefer_this_title_as_link_text'], True)
|
||||
|
||||
response = self.post(
|
||||
{'anchor-link-chooser-url': 'exampleanchor2', 'anchor-link-chooser-link_text': 'Example Anchor Test 2.1'}, # POST data
|
||||
{'link_url': 'exampleanchor', 'link_text': 'Example Anchor Text'} # GET params - initial data
|
||||
)
|
||||
result = json.loads(response.content.decode())['result']
|
||||
self.assertEqual(result['url'], "#exampleanchor2")
|
||||
self.assertEqual(result['title'], "Example Anchor Test 2.1")
|
||||
# link text has changed, so tell the caller to use it
|
||||
self.assertEqual(result['prefer_this_title_as_link_text'], True)
|
||||
|
||||
|
||||
class TestChooserEmailLink(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import os
|
|||
from itertools import chain
|
||||
from unittest import mock
|
||||
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
|
|
@ -1916,7 +1917,7 @@ class TestPageEdit(TestCase, WagtailTestUtils):
|
|||
# Check the HTML response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'tests/simple_page.html')
|
||||
self.assertContains(response, "I've been edited!")
|
||||
self.assertContains(response, "I've been edited!", html=True)
|
||||
|
||||
def test_preview_on_edit_no_session_key(self):
|
||||
preview_url = reverse('wagtailadmin_pages:preview_on_edit',
|
||||
|
|
@ -3002,9 +3003,14 @@ class TestPageCopy(TestCase, WagtailTestUtils):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that a form error was raised
|
||||
self.assertFormError(
|
||||
response, 'form', 'new_slug', "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
|
||||
)
|
||||
if DJANGO_VERSION >= (3, 0):
|
||||
self.assertFormError(
|
||||
response, 'form', 'new_slug', "Enter a valid “slug” consisting of letters, numbers, underscores or hyphens."
|
||||
)
|
||||
else:
|
||||
self.assertFormError(
|
||||
response, 'form', 'new_slug', "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."
|
||||
)
|
||||
|
||||
def test_page_copy_no_publish_permission(self):
|
||||
# Turn user into an editor who can add pages but not publish them
|
||||
|
|
@ -4229,16 +4235,52 @@ class TestRevisions(TestCase, WagtailTestUtils):
|
|||
self.assertContains(response, this_christmas_preview_url)
|
||||
self.assertContains(response, this_christmas_revert_url)
|
||||
|
||||
def test_preview_revision(self):
|
||||
def request_preview_revision(self):
|
||||
last_christmas_preview_url = reverse(
|
||||
'wagtailadmin_pages:revisions_view',
|
||||
args=(self.christmas_event.id, self.last_christmas_revision.id)
|
||||
)
|
||||
response = self.client.get(last_christmas_preview_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
return self.client.get(last_christmas_preview_url)
|
||||
|
||||
def test_preview_revision(self):
|
||||
response = self.request_preview_revision()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Last Christmas I gave you my heart")
|
||||
|
||||
def test_preview_revision_with_no_page_permissions_redirects_to_admin(self):
|
||||
admin_only_user = get_user_model().objects.create_user(
|
||||
username='admin_only',
|
||||
email='admin_only@email.com',
|
||||
password='password'
|
||||
)
|
||||
admin_only_user.user_permissions.add(
|
||||
Permission.objects.get_by_natural_key(
|
||||
codename='access_admin',
|
||||
app_label='wagtailadmin',
|
||||
model='admin'
|
||||
)
|
||||
)
|
||||
|
||||
self.login(user=admin_only_user)
|
||||
response = self.request_preview_revision()
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response['Location'], reverse('wagtailadmin_home'))
|
||||
|
||||
def test_preview_revision_forbidden_without_permission(self):
|
||||
# Alter the editors group so it has no permissions for Christmas page.
|
||||
st_patricks = Page.objects.get(slug='saint-patrick')
|
||||
editors_group = Group.objects.get(name='Site-wide editors')
|
||||
editors_group.page_permissions.update(page_id=st_patricks.id)
|
||||
|
||||
editor = get_user_model().objects.get(username='siteeditor')
|
||||
|
||||
self.login(editor)
|
||||
response = self.request_preview_revision()
|
||||
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_revert_revision(self):
|
||||
last_christmas_preview_url = reverse(
|
||||
'wagtailadmin_pages:revisions_revert',
|
||||
|
|
@ -4318,7 +4360,11 @@ class TestCompareRevisions(TestCase, WagtailTestUtils):
|
|||
response = self.client.get(compare_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, '<span class="deletion">Last Christmas I gave you my heart, but the very next day you gave it away</span><span class="addition">This year, to save me from tears, I'll give it to someone special</span>')
|
||||
self.assertContains(
|
||||
response,
|
||||
'<span class="deletion">Last Christmas I gave you my heart, but the very next day you gave it away</span><span class="addition">This year, to save me from tears, I'll give it to someone special</span>',
|
||||
html=True
|
||||
)
|
||||
|
||||
def test_compare_revisions_earliest(self):
|
||||
compare_url = reverse(
|
||||
|
|
@ -4328,7 +4374,11 @@ class TestCompareRevisions(TestCase, WagtailTestUtils):
|
|||
response = self.client.get(compare_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, '<span class="deletion">Last Christmas I gave you my heart, but the very next day you gave it away</span><span class="addition">This year, to save me from tears, I'll give it to someone special</span>')
|
||||
self.assertContains(
|
||||
response,
|
||||
'<span class="deletion">Last Christmas I gave you my heart, but the very next day you gave it away</span><span class="addition">This year, to save me from tears, I'll give it to someone special</span>',
|
||||
html=True
|
||||
)
|
||||
|
||||
def test_compare_revisions_latest(self):
|
||||
compare_url = reverse(
|
||||
|
|
@ -4338,7 +4388,11 @@ class TestCompareRevisions(TestCase, WagtailTestUtils):
|
|||
response = self.client.get(compare_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, '<span class="deletion">Last Christmas I gave you my heart, but the very next day you gave it away</span><span class="addition">This year, to save me from tears, I'll give it to someone special</span>')
|
||||
self.assertContains(
|
||||
response,
|
||||
'<span class="deletion">Last Christmas I gave you my heart, but the very next day you gave it away</span><span class="addition">This year, to save me from tears, I'll give it to someone special</span>',
|
||||
html=True
|
||||
)
|
||||
|
||||
def test_compare_revisions_live(self):
|
||||
# Mess with the live version, bypassing revisions
|
||||
|
|
@ -4355,7 +4409,11 @@ class TestCompareRevisions(TestCase, WagtailTestUtils):
|
|||
response = self.client.get(compare_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
self.assertContains(response, '<span class="deletion">Last Christmas I gave you my heart, but the very next day you gave it away</span><span class="addition">This year, to save me from tears, I'll just feed it to the dog</span>')
|
||||
self.assertContains(
|
||||
response,
|
||||
'<span class="deletion">Last Christmas I gave you my heart, but the very next day you gave it away</span><span class="addition">This year, to save me from tears, I'll just feed it to the dog</span>',
|
||||
html=True
|
||||
)
|
||||
|
||||
|
||||
class TestCompareRevisionsWithNonModelField(TestCase, WagtailTestUtils):
|
||||
|
|
@ -5275,6 +5333,18 @@ class TestDraftAccess(TestCase, WagtailTestUtils):
|
|||
# User can view
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_middleware_response_is_returned(self):
|
||||
"""
|
||||
If middleware returns a response while serving a page preview, that response should be
|
||||
returned back to the user
|
||||
"""
|
||||
self.login()
|
||||
response = self.client.get(
|
||||
reverse('wagtailadmin_pages:view_draft', args=(self.child_page.id, )),
|
||||
HTTP_USER_AGENT='EvilHacker'
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestPreview(TestCase, WagtailTestUtils):
|
||||
fixtures = ['test.json']
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ from django.urls import reverse, reverse_lazy
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from taggit.models import Tag
|
||||
|
||||
from wagtail.admin.auth import user_has_any_page_permission
|
||||
from wagtail.admin.mail import send_mail
|
||||
from wagtail.admin.menu import MenuItem
|
||||
from wagtail.admin.utils import send_mail, user_has_any_page_permission
|
||||
from wagtail.core.models import Page
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from django.views.generic import TemplateView
|
|||
from django.http import Http404
|
||||
from django.views.defaults import page_not_found
|
||||
|
||||
from wagtail.admin.auth import require_admin_access
|
||||
from wagtail.admin.urls import pages as wagtailadmin_pages_urls
|
||||
from wagtail.admin.urls import collections as wagtailadmin_collections_urls
|
||||
from wagtail.admin.urls import password_reset as wagtailadmin_password_reset_urls
|
||||
|
|
@ -13,7 +14,6 @@ from wagtail.admin.views import account, chooser, home, pages, tags, userbar
|
|||
from wagtail.admin.api import urls as api_urls
|
||||
from wagtail.core import hooks
|
||||
from wagtail.utils.urlpatterns import decorate_urlpatterns
|
||||
from wagtail.admin.decorators import require_admin_access
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
@ -38,6 +38,7 @@ urlpatterns = [
|
|||
url(r'^choose-external-link/$', chooser.external_link, name='wagtailadmin_choose_page_external_link'),
|
||||
url(r'^choose-email-link/$', chooser.email_link, name='wagtailadmin_choose_page_email_link'),
|
||||
url(r'^choose-phone-link/$', chooser.phone_link, name='wagtailadmin_choose_page_phone_link'),
|
||||
url(r'^choose-anchor-link/$', chooser.anchor_link, name='wagtailadmin_choose_page_anchor_link'),
|
||||
|
||||
url(r'^tag-autocomplete/$', tags.autocomplete, name='wagtailadmin_tag_autocomplete'),
|
||||
|
||||
|
|
@ -46,6 +47,7 @@ urlpatterns = [
|
|||
url(r'^account/$', account.account, name='wagtailadmin_account'),
|
||||
url(r'^account/change_password/$', account.change_password, name='wagtailadmin_account_change_password'),
|
||||
url(r'^account/change_email/$', account.change_email, name='wagtailadmin_account_change_email'),
|
||||
url(r'^account/change_name/$', account.change_name, name='wagtailadmin_account_change_name'),
|
||||
url(
|
||||
r'^account/notification_preferences/$',
|
||||
account.notification_preferences,
|
||||
|
|
|
|||
|
|
@ -1,351 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from functools import wraps
|
||||
import pytz
|
||||
import sys
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.mail import get_connection
|
||||
from django.core.mail.message import EmailMultiAlternatives
|
||||
from django.db.models import Count, Q
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import override, ugettext_lazy
|
||||
from modelcluster.fields import ParentalKey
|
||||
from taggit.models import Tag
|
||||
from wagtail.utils.deprecation import MovedDefinitionHandler, RemovedInWagtail29Warning
|
||||
|
||||
from wagtail.admin.navigation import get_explorable_root_page
|
||||
from wagtail.core.models import GroupPagePermission, Page, PageRevision
|
||||
from wagtail.users.models import UserProfile
|
||||
|
||||
logger = logging.getLogger('wagtail.admin')
|
||||
MOVED_DEFINITIONS = {
|
||||
'WAGTAILADMIN_PROVIDED_LANGUAGES': 'wagtail.admin.locale',
|
||||
'get_js_translation_strings': 'wagtail.admin.locale',
|
||||
'get_available_admin_languages': 'wagtail.admin.locale',
|
||||
'get_available_admin_time_zones': 'wagtail.admin.locale',
|
||||
|
||||
# Wagtail languages with >=90% coverage
|
||||
# This list is manually maintained
|
||||
WAGTAILADMIN_PROVIDED_LANGUAGES = [
|
||||
('ar', ugettext_lazy('Arabic')),
|
||||
('ca', ugettext_lazy('Catalan')),
|
||||
('cs', ugettext_lazy('Czech')),
|
||||
('de', ugettext_lazy('German')),
|
||||
('el', ugettext_lazy('Greek')),
|
||||
('en', ugettext_lazy('English')),
|
||||
('es', ugettext_lazy('Spanish')),
|
||||
('fi', ugettext_lazy('Finnish')),
|
||||
('fr', ugettext_lazy('French')),
|
||||
('gl', ugettext_lazy('Galician')),
|
||||
('hu', ugettext_lazy('Hungarian')),
|
||||
('id-id', ugettext_lazy('Indonesian')),
|
||||
('is-is', ugettext_lazy('Icelandic')),
|
||||
('it', ugettext_lazy('Italian')),
|
||||
('jp', ugettext_lazy('Japanese')),
|
||||
('ko', ugettext_lazy('Korean')),
|
||||
('lt', ugettext_lazy('Lithuanian')),
|
||||
('mn', ugettext_lazy('Mongolian')),
|
||||
('nb', ugettext_lazy('Norwegian Bokmål')),
|
||||
('nl-nl', ugettext_lazy('Netherlands Dutch')),
|
||||
('fa', ugettext_lazy('Persian')),
|
||||
('pl', ugettext_lazy('Polish')),
|
||||
('pt-br', ugettext_lazy('Brazilian Portuguese')),
|
||||
('pt-pt', ugettext_lazy('Portuguese')),
|
||||
('ro', ugettext_lazy('Romanian')),
|
||||
('ru', ugettext_lazy('Russian')),
|
||||
('sv', ugettext_lazy('Swedish')),
|
||||
('sk-sk', ugettext_lazy('Slovak')),
|
||||
('th', ugettext_lazy('Thai')),
|
||||
('uk', ugettext_lazy('Ukrainian')),
|
||||
('zh-hans', ugettext_lazy('Chinese (Simplified)')),
|
||||
('zh-hant', ugettext_lazy('Chinese (Traditional)')),
|
||||
]
|
||||
'get_object_usage': 'wagtail.admin.models',
|
||||
'popular_tags_for_model': 'wagtail.admin.models',
|
||||
|
||||
'users_with_page_permission': 'wagtail.admin.auth',
|
||||
'permission_denied': 'wagtail.admin.auth',
|
||||
'user_passes_test': 'wagtail.admin.auth',
|
||||
'permission_required': 'wagtail.admin.auth',
|
||||
'any_permission_required': 'wagtail.admin.auth',
|
||||
'PermissionPolicyChecker': 'wagtail.admin.auth',
|
||||
'user_has_any_page_permission': 'wagtail.admin.auth',
|
||||
|
||||
def get_available_admin_languages():
|
||||
return getattr(settings, 'WAGTAILADMIN_PERMITTED_LANGUAGES', WAGTAILADMIN_PROVIDED_LANGUAGES)
|
||||
'send_mail': 'wagtail.admin.mail',
|
||||
'send_notification': 'wagtail.admin.mail',
|
||||
|
||||
'get_site_for_user': 'wagtail.admin.navigation',
|
||||
}
|
||||
|
||||
def get_available_admin_time_zones():
|
||||
return getattr(settings, 'WAGTAIL_USER_TIME_ZONES', pytz.common_timezones)
|
||||
|
||||
|
||||
def get_object_usage(obj):
|
||||
"Returns a queryset of pages that link to a particular object"
|
||||
|
||||
pages = Page.objects.none()
|
||||
|
||||
# get all the relation objects for obj
|
||||
relations = [f for f in type(obj)._meta.get_fields(include_hidden=True)
|
||||
if (f.one_to_many or f.one_to_one) and f.auto_created]
|
||||
for relation in relations:
|
||||
related_model = relation.related_model
|
||||
|
||||
# if the relation is between obj and a page, get the page
|
||||
if issubclass(related_model, Page):
|
||||
pages |= Page.objects.filter(
|
||||
id__in=related_model._base_manager.filter(**{
|
||||
relation.field.name: obj.id
|
||||
}).values_list('id', flat=True)
|
||||
)
|
||||
else:
|
||||
# if the relation is between obj and an object that has a page as a
|
||||
# property, return the page
|
||||
for f in related_model._meta.fields:
|
||||
if isinstance(f, ParentalKey) and issubclass(f.remote_field.model, Page):
|
||||
pages |= Page.objects.filter(
|
||||
id__in=related_model._base_manager.filter(
|
||||
**{
|
||||
relation.field.name: obj.id
|
||||
}).values_list(f.attname, flat=True)
|
||||
)
|
||||
|
||||
return pages
|
||||
|
||||
|
||||
def popular_tags_for_model(model, count=10):
|
||||
"""Return a queryset of the most frequently used tags used on this model class"""
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
return Tag.objects.filter(
|
||||
taggit_taggeditem_items__content_type=content_type
|
||||
).annotate(
|
||||
item_count=Count('taggit_taggeditem_items')
|
||||
).order_by('-item_count')[:count]
|
||||
|
||||
|
||||
def users_with_page_permission(page, permission_type, include_superusers=True):
|
||||
# Get user model
|
||||
User = get_user_model()
|
||||
|
||||
# Find GroupPagePermission records of the given type that apply to this page or an ancestor
|
||||
ancestors_and_self = list(page.get_ancestors()) + [page]
|
||||
perm = GroupPagePermission.objects.filter(permission_type=permission_type, page__in=ancestors_and_self)
|
||||
q = Q(groups__page_permissions__in=perm)
|
||||
|
||||
# Include superusers
|
||||
if include_superusers:
|
||||
q |= Q(is_superuser=True)
|
||||
|
||||
return User.objects.filter(is_active=True).filter(q).distinct()
|
||||
|
||||
|
||||
def permission_denied(request):
|
||||
"""Return a standard 'permission denied' response"""
|
||||
if request.is_ajax():
|
||||
raise PermissionDenied
|
||||
|
||||
from wagtail.admin import messages
|
||||
|
||||
messages.error(request, _('Sorry, you do not have permission to access this area.'))
|
||||
return redirect('wagtailadmin_home')
|
||||
|
||||
|
||||
def user_passes_test(test):
|
||||
"""
|
||||
Given a test function that takes a user object and returns a boolean,
|
||||
return a view decorator that denies access to the user if the test returns false.
|
||||
"""
|
||||
def decorator(view_func):
|
||||
# decorator takes the view function, and returns the view wrapped in
|
||||
# a permission check
|
||||
|
||||
@wraps(view_func)
|
||||
def wrapped_view_func(request, *args, **kwargs):
|
||||
if test(request.user):
|
||||
# permission check succeeds; run the view function as normal
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
# permission check failed
|
||||
return permission_denied(request)
|
||||
|
||||
return wrapped_view_func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def permission_required(permission_name):
|
||||
"""
|
||||
Replacement for django.contrib.auth.decorators.permission_required which returns a
|
||||
more meaningful 'permission denied' response than just redirecting to the login page.
|
||||
(The latter doesn't work anyway because Wagtail doesn't define LOGIN_URL...)
|
||||
"""
|
||||
def test(user):
|
||||
return user.has_perm(permission_name)
|
||||
|
||||
# user_passes_test constructs a decorator function specific to the above test function
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
def any_permission_required(*perms):
|
||||
"""
|
||||
Decorator that accepts a list of permission names, and allows the user
|
||||
to pass if they have *any* of the permissions in the list
|
||||
"""
|
||||
def test(user):
|
||||
for perm in perms:
|
||||
if user.has_perm(perm):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
class PermissionPolicyChecker:
|
||||
"""
|
||||
Provides a view decorator that enforces the given permission policy,
|
||||
returning the wagtailadmin 'permission denied' response if permission not granted
|
||||
"""
|
||||
def __init__(self, policy):
|
||||
self.policy = policy
|
||||
|
||||
def require(self, action):
|
||||
def test(user):
|
||||
return self.policy.user_has_permission(user, action)
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
def require_any(self, *actions):
|
||||
def test(user):
|
||||
return self.policy.user_has_any_permission(user, actions)
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
def send_mail(subject, message, recipient_list, from_email=None, **kwargs):
|
||||
"""
|
||||
Wrapper around Django's EmailMultiAlternatives as done in send_mail().
|
||||
Custom from_email handling and special Auto-Submitted header.
|
||||
"""
|
||||
if not from_email:
|
||||
if hasattr(settings, 'WAGTAILADMIN_NOTIFICATION_FROM_EMAIL'):
|
||||
from_email = settings.WAGTAILADMIN_NOTIFICATION_FROM_EMAIL
|
||||
elif hasattr(settings, 'DEFAULT_FROM_EMAIL'):
|
||||
from_email = settings.DEFAULT_FROM_EMAIL
|
||||
else:
|
||||
from_email = 'webmaster@localhost'
|
||||
|
||||
connection = kwargs.get('connection', False) or get_connection(
|
||||
username=kwargs.get('auth_user', None),
|
||||
password=kwargs.get('auth_password', None),
|
||||
fail_silently=kwargs.get('fail_silently', None),
|
||||
)
|
||||
multi_alt_kwargs = {
|
||||
'connection': connection,
|
||||
'headers': {
|
||||
'Auto-Submitted': 'auto-generated',
|
||||
}
|
||||
}
|
||||
mail = EmailMultiAlternatives(subject, message, from_email, recipient_list, **multi_alt_kwargs)
|
||||
html_message = kwargs.get('html_message', None)
|
||||
if html_message:
|
||||
mail.attach_alternative(html_message, 'text/html')
|
||||
|
||||
return mail.send()
|
||||
|
||||
|
||||
def send_notification(page_revision_id, notification, excluded_user_id):
|
||||
# Get revision
|
||||
revision = PageRevision.objects.get(id=page_revision_id)
|
||||
|
||||
# Get list of recipients
|
||||
if notification == 'submitted':
|
||||
# Get list of publishers
|
||||
include_superusers = getattr(settings, 'WAGTAILADMIN_NOTIFICATION_INCLUDE_SUPERUSERS', True)
|
||||
recipients = users_with_page_permission(revision.page, 'publish', include_superusers)
|
||||
elif notification in ['rejected', 'approved']:
|
||||
# Get submitter
|
||||
recipients = [revision.user]
|
||||
else:
|
||||
return False
|
||||
|
||||
# Get list of email addresses
|
||||
email_recipients = [
|
||||
recipient for recipient in recipients
|
||||
if recipient.email and recipient.pk != excluded_user_id and getattr(
|
||||
UserProfile.get_for_user(recipient),
|
||||
notification + '_notifications'
|
||||
)
|
||||
]
|
||||
|
||||
# Return if there are no email addresses
|
||||
if not email_recipients:
|
||||
return True
|
||||
|
||||
# Get template
|
||||
template_subject = 'wagtailadmin/notifications/' + notification + '_subject.txt'
|
||||
template_text = 'wagtailadmin/notifications/' + notification + '.txt'
|
||||
template_html = 'wagtailadmin/notifications/' + notification + '.html'
|
||||
|
||||
# Common context to template
|
||||
context = {
|
||||
"revision": revision,
|
||||
"settings": settings,
|
||||
}
|
||||
|
||||
# Send emails
|
||||
sent_count = 0
|
||||
for recipient in email_recipients:
|
||||
try:
|
||||
# update context with this recipient
|
||||
context["user"] = recipient
|
||||
|
||||
# Translate text to the recipient language settings
|
||||
with override(recipient.wagtail_userprofile.get_preferred_language()):
|
||||
# Get email subject and content
|
||||
email_subject = render_to_string(template_subject, context).strip()
|
||||
email_content = render_to_string(template_text, context).strip()
|
||||
|
||||
kwargs = {}
|
||||
if getattr(settings, 'WAGTAILADMIN_NOTIFICATION_USE_HTML', False):
|
||||
kwargs['html_message'] = render_to_string(template_html, context)
|
||||
|
||||
# Send email
|
||||
send_mail(email_subject, email_content, [recipient.email], **kwargs)
|
||||
sent_count += 1
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"Failed to send notification email '%s' to %s",
|
||||
email_subject, recipient.email
|
||||
)
|
||||
|
||||
return sent_count == len(email_recipients)
|
||||
|
||||
|
||||
def user_has_any_page_permission(user):
|
||||
"""
|
||||
Check if a user has any permission to add, edit, or otherwise manage any
|
||||
page.
|
||||
"""
|
||||
# Can't do nothin if you're not active.
|
||||
if not user.is_active:
|
||||
return False
|
||||
|
||||
# Superusers can do anything.
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
# At least one of the users groups has a GroupPagePermission.
|
||||
# The user can probably do something.
|
||||
if GroupPagePermission.objects.filter(group__in=user.groups.all()).exists():
|
||||
return True
|
||||
|
||||
# Specific permissions for a page type do not mean anything.
|
||||
|
||||
# No luck! This user can not do anything with pages.
|
||||
return False
|
||||
|
||||
|
||||
def get_site_for_user(user):
|
||||
root_page = get_explorable_root_page(user)
|
||||
if root_page:
|
||||
root_site = root_page.get_site()
|
||||
else:
|
||||
root_site = None
|
||||
real_site_name = None
|
||||
if root_site:
|
||||
real_site_name = root_site.site_name if root_site.site_name else root_site.hostname
|
||||
return {
|
||||
'root_page': root_page,
|
||||
'root_site': root_site,
|
||||
'site_name': real_site_name if real_site_name else settings.WAGTAIL_SITE_NAME,
|
||||
}
|
||||
sys.modules[__name__] = MovedDefinitionHandler(sys.modules[__name__], MOVED_DEFINITIONS, RemovedInWagtail29Warning)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from django.utils.translation import activate
|
|||
from wagtail.admin.forms.auth import LoginForm, PasswordResetForm
|
||||
from wagtail.core import hooks
|
||||
from wagtail.users.forms import (
|
||||
AvatarPreferencesForm, CurrentTimeZoneForm, EmailForm, NotificationPreferencesForm, PreferredLanguageForm)
|
||||
AvatarPreferencesForm, CurrentTimeZoneForm, EmailForm, NameForm, NotificationPreferencesForm, PreferredLanguageForm)
|
||||
from wagtail.users.models import UserProfile
|
||||
from wagtail.utils.loading import get_custom_form
|
||||
|
||||
|
|
@ -95,6 +95,22 @@ def change_email(request):
|
|||
})
|
||||
|
||||
|
||||
def change_name(request):
|
||||
if request.method == 'POST':
|
||||
form = NameForm(request.POST, instance=request.user)
|
||||
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Your name has been changed successfully!"))
|
||||
return redirect('wagtailadmin_account')
|
||||
else:
|
||||
form = NameForm(instance=request.user)
|
||||
|
||||
return render(request, 'wagtailadmin/account/change_name.html', {
|
||||
'form': form,
|
||||
})
|
||||
|
||||
|
||||
class PasswordResetEnabledViewMixin:
|
||||
"""
|
||||
Class based view mixin that disables the view if password reset is disabled by one of the following settings:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ from django.http import Http404
|
|||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from wagtail.admin.forms.choosers import (
|
||||
EmailLinkChooserForm, ExternalLinkChooserForm, PhoneLinkChooserForm)
|
||||
AnchorLinkChooserForm, EmailLinkChooserForm, ExternalLinkChooserForm, PhoneLinkChooserForm)
|
||||
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.modal_workflow import render_modal_workflow
|
||||
from wagtail.core import hooks
|
||||
|
|
@ -13,7 +14,7 @@ from wagtail.core.utils import resolve_model_string
|
|||
|
||||
def shared_context(request, extra_context=None):
|
||||
context = {
|
||||
# parent_page ID is passed as a GET parameter on the external_link and email_link views
|
||||
# parent_page ID is passed as a GET parameter on the external_link, anchor_link and mail_link views
|
||||
# so that it's remembered when browsing from 'Internal link' to another link type
|
||||
# and back again. On the 'browse' / 'internal link' view this will be overridden to be
|
||||
# sourced from the standard URL path parameter instead.
|
||||
|
|
@ -21,6 +22,7 @@ def shared_context(request, extra_context=None):
|
|||
'allow_external_link': request.GET.get('allow_external_link'),
|
||||
'allow_email_link': request.GET.get('allow_email_link'),
|
||||
'allow_phone_link': request.GET.get('allow_phone_link'),
|
||||
'allow_anchor_link': request.GET.get('allow_anchor_link'),
|
||||
}
|
||||
if extra_context:
|
||||
context.update(extra_context)
|
||||
|
|
@ -224,6 +226,37 @@ def external_link(request):
|
|||
)
|
||||
|
||||
|
||||
def anchor_link(request):
|
||||
initial_data = {
|
||||
'link_text': request.GET.get('link_text', ''),
|
||||
'url': request.GET.get('link_url', ''),
|
||||
}
|
||||
|
||||
if request.method == 'POST':
|
||||
form = AnchorLinkChooserForm(request.POST, initial=initial_data, prefix='anchor-link-chooser')
|
||||
|
||||
if form.is_valid():
|
||||
result = {
|
||||
'url': '#' + form.cleaned_data['url'],
|
||||
'title': form.cleaned_data['link_text'].strip() or form.cleaned_data['url'],
|
||||
'prefer_this_title_as_link_text': ('link_text' in form.changed_data),
|
||||
}
|
||||
return render_modal_workflow(
|
||||
request, None, None,
|
||||
None, json_data={'step': 'external_link_chosen', 'result': result}
|
||||
)
|
||||
else:
|
||||
form = AnchorLinkChooserForm(initial=initial_data, prefix='anchor-link-chooser')
|
||||
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
'wagtailadmin/chooser/anchor_link.html', None,
|
||||
shared_context(request, {
|
||||
'form': form,
|
||||
}), json_data={'step': 'anchor_link'}
|
||||
)
|
||||
|
||||
|
||||
def email_link(request):
|
||||
initial_data = {
|
||||
'link_text': request.GET.get('link_text', ''),
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from django.views.generic.edit import BaseCreateView, BaseDeleteView, BaseUpdate
|
|||
from django.views.generic.list import BaseListView
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.utils import permission_denied
|
||||
from wagtail.admin.auth import permission_denied
|
||||
|
||||
|
||||
class PermissionCheckedMixin:
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ from django.http import Http404
|
|||
from django.shortcuts import render
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from wagtail.admin.navigation import get_site_for_user
|
||||
from wagtail.admin.site_summary import SiteSummaryPanel
|
||||
from wagtail.admin.utils import get_site_for_user
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import Page, PageRevision, UserPagePermissionsProxy
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ from django.views.generic import View
|
|||
|
||||
from wagtail.admin import messages, signals
|
||||
from wagtail.admin.action_menu import PageActionMenu
|
||||
from wagtail.admin.auth import user_has_any_page_permission, user_passes_test
|
||||
from wagtail.admin.forms.pages import CopyForm
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.mail import send_notification
|
||||
from wagtail.admin.navigation import get_explorable_root_page
|
||||
from wagtail.admin.utils import send_notification, user_has_any_page_permission, user_passes_test
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import Page, PageRevision, UserPagePermissionsProxy
|
||||
from wagtail.search.query import MATCH_ALL
|
||||
|
|
@ -586,7 +587,7 @@ def view_draft(request, page_id):
|
|||
perms = page.permissions_for_user(request.user)
|
||||
if not (perms.can_publish() or perms.can_edit()):
|
||||
raise PermissionDenied
|
||||
return page.serve_preview(page.dummy_request(request), page.default_preview_mode)
|
||||
return page.make_preview_request(request, page.default_preview_mode)
|
||||
|
||||
|
||||
class PreviewOnEdit(View):
|
||||
|
|
@ -646,8 +647,7 @@ class PreviewOnEdit(View):
|
|||
|
||||
form.save(commit=False)
|
||||
preview_mode = request.GET.get('mode', page.default_preview_mode)
|
||||
return page.serve_preview(page.dummy_request(request),
|
||||
preview_mode)
|
||||
return page.make_preview_request(request, preview_mode)
|
||||
|
||||
|
||||
class PreviewOnCreate(PreviewOnEdit):
|
||||
|
|
@ -1055,11 +1055,9 @@ def preview_for_moderation(request, revision_id):
|
|||
|
||||
page = revision.as_page_object()
|
||||
|
||||
request.revision_id = revision_id
|
||||
|
||||
# pass in the real user request rather than page.dummy_request(), so that request.user
|
||||
# and request.revision_id will be picked up by the wagtail user bar
|
||||
return page.serve_preview(request, page.default_preview_mode)
|
||||
return page.make_preview_request(request, page.default_preview_mode, extra_request_attrs={
|
||||
'revision_id': revision_id
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
|
|
@ -1177,10 +1175,15 @@ def revisions_revert(request, page_id, revision_id):
|
|||
@user_passes_test(user_has_any_page_permission)
|
||||
def revisions_view(request, page_id, revision_id):
|
||||
page = get_object_or_404(Page, id=page_id).specific
|
||||
|
||||
perms = page.permissions_for_user(request.user)
|
||||
if not (perms.can_publish() or perms.can_edit()):
|
||||
raise PermissionDenied
|
||||
|
||||
revision = get_object_or_404(page.revisions, id=revision_id)
|
||||
revision_page = revision.as_page_object()
|
||||
|
||||
return revision_page.serve_preview(page.dummy_request(request), page.default_preview_mode)
|
||||
return revision_page.make_preview_request(request, page.default_preview_mode)
|
||||
|
||||
|
||||
def revisions_compare(request, page_id, revision_id_a, revision_id_b):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ from django.utils.translation import ugettext
|
|||
from draftjs_exporter.dom import DOM
|
||||
|
||||
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
|
||||
from wagtail.admin.auth import user_has_any_page_permission
|
||||
from wagtail.admin.locale import get_available_admin_languages, get_available_admin_time_zones
|
||||
from wagtail.admin.menu import MenuItem, SubmenuMenuItem, settings_menu
|
||||
from wagtail.admin.navigation import get_explorable_root_page
|
||||
from wagtail.admin.rich_text import (
|
||||
|
|
@ -16,9 +18,6 @@ from wagtail.admin.rich_text.converters.html_to_contentstate import (
|
|||
BlockElementHandler, ExternalLinkElementHandler, HorizontalRuleHandler,
|
||||
InlineStyleElementHandler, ListElementHandler, ListItemElementHandler, PageLinkElementHandler)
|
||||
from wagtail.admin.search import SearchArea
|
||||
from wagtail.admin.utils import (
|
||||
get_available_admin_languages, get_available_admin_time_zones,
|
||||
user_has_any_page_permission)
|
||||
from wagtail.admin.views.account import password_management_enabled
|
||||
from wagtail.admin.viewsets import viewsets
|
||||
from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook, PageListingButton
|
||||
|
|
@ -203,7 +202,7 @@ def register_account_set_profile_picture(request):
|
|||
return {
|
||||
'url': reverse('wagtailadmin_account_change_avatar'),
|
||||
'label': _('Set profile picture'),
|
||||
'help_text': _("Change your profile picture")
|
||||
'help_text': _("Change your profile picture.")
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -257,6 +256,15 @@ def register_account_current_time_zone(request):
|
|||
}
|
||||
|
||||
|
||||
@hooks.register('register_account_menu_item')
|
||||
def register_account_change_name(request):
|
||||
return {
|
||||
'url': reverse('wagtailadmin_account_change_name'),
|
||||
'label': _('Change name'),
|
||||
'help_text': _('Change your first and last name on your account.'),
|
||||
}
|
||||
|
||||
|
||||
@hooks.register('register_rich_text_features')
|
||||
def register_core_features(features):
|
||||
# Hallo.js
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from unidecode import unidecode
|
||||
|
||||
from wagtail.admin.edit_handlers import FieldPanel
|
||||
from wagtail.admin.utils import send_mail
|
||||
from wagtail.admin.mail import send_mail
|
||||
from wagtail.core.models import Orderable, Page
|
||||
|
||||
from .forms import FormBuilder, WagtailAdminFormPageForm
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from django import VERSION as DJANGO_VERSION
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.test import TestCase
|
||||
|
|
@ -65,13 +66,21 @@ class TestExcludeFromExplorer(TestCase, WagtailTestUtils):
|
|||
def test_attribute_effects_explorer(self):
|
||||
# The two VenuePages should appear in the venuepage list
|
||||
response = self.client.get('/admin/modeladmintest/venuepage/')
|
||||
self.assertContains(response, "Santa's Grotto")
|
||||
self.assertContains(response, "Santa's Workshop")
|
||||
if DJANGO_VERSION >= (3, 0):
|
||||
self.assertContains(response, "Santa's Grotto")
|
||||
self.assertContains(response, "Santa's Workshop")
|
||||
else:
|
||||
self.assertContains(response, "Santa's Grotto")
|
||||
self.assertContains(response, "Santa's Workshop")
|
||||
|
||||
# But when viewing the children of 'Christmas' event in explorer
|
||||
response = self.client.get('/admin/pages/4/')
|
||||
self.assertNotContains(response, "Santa's Grotto")
|
||||
self.assertNotContains(response, "Santa's Workshop")
|
||||
if DJANGO_VERSION >= (3, 0):
|
||||
self.assertNotContains(response, "Santa's Grotto")
|
||||
self.assertNotContains(response, "Santa's Workshop")
|
||||
else:
|
||||
self.assertNotContains(response, "Santa's Grotto")
|
||||
self.assertNotContains(response, "Santa's Workshop")
|
||||
|
||||
# But the other test page should...
|
||||
self.assertContains(response, "Claim your free present!")
|
||||
|
|
|
|||
|
|
@ -439,6 +439,23 @@ class TestDeleteViewWithProtectedRelation(TestCase, WagtailTestUtils):
|
|||
self.assertFalse(Author.objects.filter(id=4).exists())
|
||||
|
||||
|
||||
def test_post_with_1to1_dependent_object(self):
|
||||
response = self.post(5)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(
|
||||
response,
|
||||
"'Harper Lee' is currently referenced by other objects"
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"<li><b>Solo Book:</b> To Kill a Mockingbird</li>"
|
||||
)
|
||||
|
||||
# Author not deleted
|
||||
self.assertTrue(Author.objects.filter(id=5).exists())
|
||||
|
||||
|
||||
class TestDeleteViewModelReprPrimary(TestCase, WagtailTestUtils):
|
||||
fixtures = ['modeladmintest_test.json']
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ from django.contrib.admin.options import IncorrectLookupParameters
|
|||
from django.contrib.admin.utils import (
|
||||
get_fields_from_path, label_for_field, lookup_needs_distinct, prepare_lookup_value, quote, unquote)
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import ImproperlyConfigured, PermissionDenied, SuspiciousOperation
|
||||
from django.core.exceptions import (
|
||||
ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied, SuspiciousOperation)
|
||||
from django.core.paginator import InvalidPage, Paginator
|
||||
from django.db import models
|
||||
from django.db.models.fields import FieldDoesNotExist
|
||||
from django.db.models.fields.related import ManyToManyField
|
||||
from django.db.models.fields.related import ManyToManyField, OneToOneRel
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.defaultfilters import filesizeformat
|
||||
from django.utils.decorators import method_decorator
|
||||
|
|
@ -738,9 +739,17 @@ class DeleteView(InstanceSpecificView):
|
|||
obj.field, ManyToManyField))
|
||||
for rel in fields:
|
||||
if rel.on_delete == models.PROTECT:
|
||||
qs = getattr(self.instance, rel.get_accessor_name())
|
||||
for obj in qs.all():
|
||||
linked_objects.append(obj)
|
||||
if isinstance(rel, OneToOneRel):
|
||||
try:
|
||||
obj = getattr(self.instance, rel.get_accessor_name())
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
else:
|
||||
linked_objects.append(obj)
|
||||
else:
|
||||
qs = getattr(self.instance, rel.get_accessor_name())
|
||||
for obj in qs.all():
|
||||
linked_objects.append(obj)
|
||||
context = self.get_context_data(
|
||||
protected_error=True,
|
||||
linked_objects=linked_objects
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from .models import RawSearchQuery as PostgresRawSearchQuery
|
|||
from .models import IndexEntry
|
||||
from .utils import (
|
||||
get_content_type_pk, get_descendants_content_types_pks, get_postgresql_connections,
|
||||
get_sql_weights, get_weight, unidecode)
|
||||
get_sql_weights, get_weight)
|
||||
|
||||
EMPTY_VECTOR = SearchVector(Value(''))
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ class Index:
|
|||
def prepare_field(self, obj, field):
|
||||
if isinstance(field, SearchField):
|
||||
yield (field, get_weight(field.boost),
|
||||
unidecode(self.prepare_value(field.get_value(obj))))
|
||||
self.prepare_value(field.get_value(obj)))
|
||||
elif isinstance(field, RelatedFields):
|
||||
sub_obj = field.get_value(obj)
|
||||
if sub_obj is None:
|
||||
|
|
@ -227,16 +227,13 @@ class PostgresSearchQueryCompiler(BaseSearchQueryCompiler):
|
|||
and field.field_name == field_lookup:
|
||||
return self.get_search_field(sub_field_name, field.fields)
|
||||
|
||||
def prepare_word(self, word):
|
||||
return unidecode(word)
|
||||
|
||||
def build_tsquery_content(self, query, group=False):
|
||||
if isinstance(query, PlainText):
|
||||
query_formats = []
|
||||
query_params = []
|
||||
for word in query.query_string.split():
|
||||
query_formats.append(self.TSQUERY_WORD_FORMAT)
|
||||
query_params.append(self.prepare_word(word))
|
||||
query_params.append(word)
|
||||
operator = self.TSQUERY_OPERATORS[query.operator]
|
||||
query_format = operator.join(query_formats)
|
||||
if group and len(query_formats) > 1:
|
||||
|
|
|
|||
44
wagtail/contrib/postgres_search/tests/test_stemming.py
Normal file
44
wagtail/contrib/postgres_search/tests/test_stemming.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import unittest
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.test import TestCase
|
||||
|
||||
from wagtail.search.backends import get_search_backend
|
||||
from wagtail.tests.search import models
|
||||
|
||||
|
||||
class TestPostgresStemming(TestCase):
|
||||
def setUp(self):
|
||||
backend_name = "wagtail.contrib.postgres_search.backend"
|
||||
for conf in settings.WAGTAILSEARCH_BACKENDS.values():
|
||||
if conf['BACKEND'] == backend_name:
|
||||
break
|
||||
else:
|
||||
raise unittest.SkipTest("Only for %s" % backend_name)
|
||||
|
||||
self.backend = get_search_backend(backend_name)
|
||||
|
||||
def test_ru_stemming(self):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
"SET default_text_search_config TO 'pg_catalog.russian'"
|
||||
)
|
||||
|
||||
ru_book = models.Book.objects.create(
|
||||
title="Голубое сало", publication_date="1999-05-01",
|
||||
number_of_pages=352
|
||||
)
|
||||
self.backend.add(ru_book)
|
||||
|
||||
results = self.backend.search("Голубое", models.Book)
|
||||
self.assertEqual(list(results), [ru_book])
|
||||
|
||||
|
||||
results = self.backend.search("Голубая", models.Book)
|
||||
self.assertEqual(list(results), [ru_book])
|
||||
|
||||
results = self.backend.search("Голубой", models.Book)
|
||||
self.assertEqual(list(results), [ru_book])
|
||||
|
||||
ru_book.delete()
|
||||
|
|
@ -5,13 +5,6 @@ from django.db import connections
|
|||
|
||||
from wagtail.search.index import Indexed, RelatedFields, SearchField
|
||||
|
||||
try:
|
||||
# Only use the GPLv2 licensed unidecode if it's installed.
|
||||
from unidecode import unidecode
|
||||
except ImportError:
|
||||
def unidecode(value):
|
||||
return value
|
||||
|
||||
|
||||
def get_postgresql_connections():
|
||||
return [connection for connection in connections.all()
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ from django.utils.translation import ugettext as _
|
|||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.auth import PermissionPolicyChecker, permission_denied
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.utils import PermissionPolicyChecker, permission_denied
|
||||
from wagtail.contrib.redirects import models
|
||||
from wagtail.contrib.redirects.forms import RedirectForm
|
||||
from wagtail.contrib.redirects.permissions import permission_policy
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from django.utils.translation import ugettext as _
|
|||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.auth import any_permission_required, permission_required
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.utils import any_permission_required, permission_required
|
||||
from wagtail.contrib.search_promotions import forms
|
||||
from wagtail.search import forms as search_forms
|
||||
from wagtail.search.models import Query
|
||||
|
|
|
|||
|
|
@ -43,7 +43,12 @@ class TableInput(forms.HiddenInput):
|
|||
|
||||
def get_context(self, name, value, attrs=None):
|
||||
context = super().get_context(name, value, attrs)
|
||||
table_caption = ''
|
||||
if value and value != 'null':
|
||||
table_caption = json.loads(value).get('table_caption', '')
|
||||
context['widget']['table_options_json'] = json.dumps(self.table_options)
|
||||
context['widget']['table_caption'] = table_caption
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
|
@ -59,6 +64,7 @@ class TableBlock(FieldBlock):
|
|||
"""
|
||||
self.table_options = self.get_table_options(table_options=table_options)
|
||||
self.field_options = {'required': required, 'help_text': help_text}
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@cached_property
|
||||
|
|
@ -97,6 +103,7 @@ class TableBlock(FieldBlock):
|
|||
'table_header': table_header,
|
||||
'first_col_is_header': first_col_is_header,
|
||||
'html_renderer': self.is_html_renderer(),
|
||||
'table_caption': value.get('table_caption'),
|
||||
'data': value['data'][1:] if table_header else value.get('data', [])
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,11 @@ function initTable(id, tableOptions) {
|
|||
var containerId = id + '-handsontable-container';
|
||||
var tableHeaderCheckboxId = id + '-handsontable-header';
|
||||
var colHeaderCheckboxId = id + '-handsontable-col-header';
|
||||
var tableCaptionId = id + '-handsontable-col-caption';
|
||||
var hiddenStreamInput = $('#' + id);
|
||||
var tableHeaderCheckbox = $('#' + tableHeaderCheckboxId);
|
||||
var colHeaderCheckbox = $('#' + colHeaderCheckboxId);
|
||||
var tableCaption = $('#' + tableCaptionId);
|
||||
var hot;
|
||||
var defaultOptions;
|
||||
var finalOptions = {};
|
||||
|
|
@ -18,6 +20,7 @@ function initTable(id, tableOptions) {
|
|||
var structureEvent;
|
||||
var dataForForm = null;
|
||||
var isInitialized = false;
|
||||
|
||||
var getWidth = function() {
|
||||
return $('.widget-table_input').closest('.sequence-member-inner').width();
|
||||
};
|
||||
|
|
@ -55,6 +58,9 @@ function initTable(id, tableOptions) {
|
|||
if (dataForForm.hasOwnProperty('first_col_is_header')) {
|
||||
colHeaderCheckbox.prop('checked', dataForForm.first_col_is_header);
|
||||
}
|
||||
if (dataForForm.hasOwnProperty('table_caption')) {
|
||||
tableCaption.prop('value', dataForForm.table_caption);
|
||||
}
|
||||
}
|
||||
|
||||
if (!tableOptions.hasOwnProperty('width') || !tableOptions.hasOwnProperty('height')) {
|
||||
|
|
@ -88,7 +94,8 @@ function initTable(id, tableOptions) {
|
|||
data: hot.getData(),
|
||||
cell: getCellsClassnames(),
|
||||
first_row_is_table_header: tableHeaderCheckbox.prop('checked'),
|
||||
first_col_is_header: colHeaderCheckbox.prop('checked')
|
||||
first_col_is_header: colHeaderCheckbox.prop('checked'),
|
||||
table_caption: tableCaption.val()
|
||||
}));
|
||||
};
|
||||
|
||||
|
|
@ -123,6 +130,10 @@ function initTable(id, tableOptions) {
|
|||
persist();
|
||||
});
|
||||
|
||||
tableCaption.on('change', function() {
|
||||
persist();
|
||||
});
|
||||
|
||||
defaultOptions = {
|
||||
afterChange: cellEvent,
|
||||
afterCreateCol: structureEvent,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
{% load table_block_tags %}
|
||||
|
||||
<table>
|
||||
{% if table_caption %}
|
||||
<caption>{{ table_caption }}</caption>
|
||||
{% endif %}
|
||||
{% if table_header %}
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="field">
|
||||
<label for="{{ widget.attrs.id }}-handsontable-col-caption">{% trans 'Table caption' %}</label>
|
||||
<div class="field-content">
|
||||
<div class="input">
|
||||
<input type="text" id="{{ widget.attrs.id }}-handsontable-col-caption" name="handsontable-col-caption" value="{{ widget.table_caption }}"/>
|
||||
</div>
|
||||
<p class="help">{% trans 'A heading that identifies the overall topic of the table, and is useful for screen reader users' %}</p>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div id="{{ widget.attrs.id }}-handsontable-container"></div>
|
||||
{% include 'django/forms/widgets/hidden.html' %}
|
||||
<script>initTable("{{ widget.attrs.id|escapejs }}", {{ widget.table_options_json|safe }});</script>
|
||||
|
|
|
|||
|
|
@ -241,6 +241,30 @@ class TestTableBlock(TestCase):
|
|||
self.assertIn("<div>A fascinating table.</div>", result)
|
||||
|
||||
|
||||
def test_table_block_caption_render(self):
|
||||
"""
|
||||
Test a generic render with caption.
|
||||
"""
|
||||
value = {'table_caption': 'caption', 'first_row_is_table_header': False,
|
||||
'first_col_is_header': False,
|
||||
'data': [['Test 1', 'Test 2', 'Test 3'], [None, None, None],
|
||||
[None, None, None]]}
|
||||
block = TableBlock()
|
||||
result = block.render(value)
|
||||
expected = """
|
||||
<table>
|
||||
<caption>caption</caption>
|
||||
<tbody>
|
||||
<tr><td>Test 1</td><td>Test 2</td><td>Test 3</td></tr>
|
||||
<tr><td></td><td></td><td></td></tr>
|
||||
<tr><td></td><td></td><td></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
"""
|
||||
self.assertHTMLEqual(result, expected)
|
||||
self.assertIn('Test 2', result)
|
||||
|
||||
|
||||
class TestTableBlockForm(WagtailTestUtils, SimpleTestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import logging
|
|||
from collections import defaultdict
|
||||
from io import StringIO
|
||||
from urllib.parse import urlparse
|
||||
from warnings import warn
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
|
|
@ -32,6 +33,8 @@ from wagtail.core.sites import get_site_for_hostname
|
|||
from wagtail.core.url_routing import RouteResult
|
||||
from wagtail.core.utils import WAGTAIL_APPEND_SLASH, camelcase_to_underscore, resolve_model_string
|
||||
from wagtail.search import index
|
||||
from wagtail.utils.deprecation import RemovedInWagtail29Warning
|
||||
|
||||
|
||||
logger = logging.getLogger('wagtail.core')
|
||||
|
||||
|
|
@ -1216,16 +1219,51 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
|
|||
user_perms = UserPagePermissionsProxy(user)
|
||||
return user_perms.for_page(self)
|
||||
|
||||
def dummy_request(self, original_request=None, **meta):
|
||||
def make_preview_request(self, original_request=None, preview_mode=None, extra_request_attrs=None):
|
||||
"""
|
||||
Construct a HttpRequest object that is, as far as possible, representative of ones that would
|
||||
receive this page as a response. Used for previewing / moderation and any other place where we
|
||||
Simulate a request to this page, by constructing a fake HttpRequest object that is (as far
|
||||
as possible) representative of a real request to this page's front-end URL, and invoking
|
||||
serve_preview with that request (and the given preview_mode).
|
||||
|
||||
Used for previewing / moderation and any other place where we
|
||||
want to display a view of this page in the admin interface without going through the regular
|
||||
page routing logic.
|
||||
|
||||
If you pass in a real request object as original_request, additional information (e.g. client IP, cookies)
|
||||
will be included in the dummy request.
|
||||
"""
|
||||
dummy_meta = self._get_dummy_headers(original_request)
|
||||
request = WSGIRequest(dummy_meta)
|
||||
|
||||
# Add a flag to let middleware know that this is a dummy request.
|
||||
request.is_dummy = True
|
||||
|
||||
if extra_request_attrs:
|
||||
for k, v in extra_request_attrs.items():
|
||||
setattr(request, k, v)
|
||||
|
||||
page = self
|
||||
|
||||
# Build a custom django.core.handlers.BaseHandler subclass that invokes serve_preview as
|
||||
# the eventual view function called at the end of the middleware chain, rather than going
|
||||
# through the URL resolver
|
||||
class Handler(BaseHandler):
|
||||
def _get_response(self, request):
|
||||
response = page.serve_preview(request, preview_mode)
|
||||
if hasattr(response, 'render') and callable(response.render):
|
||||
response = response.render()
|
||||
return response
|
||||
|
||||
# Invoke this custom handler.
|
||||
handler = Handler()
|
||||
handler.load_middleware()
|
||||
return handler.get_response(request)
|
||||
|
||||
def _get_dummy_headers(self, original_request=None):
|
||||
"""
|
||||
Return a dict of META information to be included in a faked HttpRequest object to pass to
|
||||
serve_preview.
|
||||
"""
|
||||
url = self.full_url
|
||||
if url:
|
||||
url_info = urlparse(url)
|
||||
|
|
@ -1279,6 +1317,16 @@ class Page(AbstractPage, index.Indexed, ClusterableModel, metaclass=PageBase):
|
|||
if header in original_request.META:
|
||||
dummy_values[header] = original_request.META[header]
|
||||
|
||||
return dummy_values
|
||||
|
||||
def dummy_request(self, original_request=None, **meta):
|
||||
warn(
|
||||
"Page.dummy_request is deprecated. Use Page.make_preview_request instead",
|
||||
category=RemovedInWagtail29Warning
|
||||
)
|
||||
|
||||
dummy_values = self._get_dummy_headers(original_request)
|
||||
|
||||
# Add additional custom metadata sent by the caller.
|
||||
dummy_values.update(**meta)
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ class LinkRewriter:
|
|||
link_type = 'external'
|
||||
elif href.startswith('mailto:'):
|
||||
link_type = 'email'
|
||||
elif href.startswith('#'):
|
||||
link_type = 'anchor'
|
||||
|
||||
if not link_type:
|
||||
# otherwise return ordinary links without a linktype unchanged
|
||||
|
|
@ -74,7 +76,7 @@ class LinkRewriter:
|
|||
try:
|
||||
rule = self.link_rules[link_type]
|
||||
except KeyError:
|
||||
if link_type in ['email', 'external']:
|
||||
if link_type in ['email', 'external', 'anchor']:
|
||||
# If no rule is registered for supported types
|
||||
# return ordinary links without a linktype unchanged
|
||||
return match.group(0)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ def pageurl(context, page, fallback=None):
|
|||
# request.site not available in the current context; fall back on page.url
|
||||
return page.url
|
||||
|
||||
if current_site is None:
|
||||
# request.site is set to None; fall back on page.url
|
||||
return page.url
|
||||
|
||||
# Pass page.relative_url the request object, which may contain a cached copy of
|
||||
# Site.get_site_root_paths()
|
||||
# This avoids page.relative_url having to make a database/cache fetch for this list
|
||||
|
|
@ -48,13 +52,15 @@ def slugurl(context, slug):
|
|||
that matches the slug on any site.
|
||||
"""
|
||||
|
||||
page = None
|
||||
try:
|
||||
current_site = context['request'].site
|
||||
except (KeyError, AttributeError):
|
||||
# No site object found - allow the fallback below to take place.
|
||||
page = None
|
||||
pass
|
||||
else:
|
||||
page = Page.objects.in_site(current_site).filter(slug=slug).first()
|
||||
if current_site is not None:
|
||||
page = Page.objects.in_site(current_site).filter(slug=slug).first()
|
||||
|
||||
# If no page is found, fall back to searching the whole tree.
|
||||
if page is None:
|
||||
|
|
|
|||
|
|
@ -1444,13 +1444,14 @@ class TestIssue2024(TestCase):
|
|||
self.assertEqual(event_index.content_type, ContentType.objects.get_for_model(Page))
|
||||
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=['localhost'])
|
||||
class TestDummyRequest(TestCase):
|
||||
class TestMakePreviewRequest(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
||||
def test_dummy_request_for_accessible_page(self):
|
||||
def test_make_preview_request_for_accessible_page(self):
|
||||
event_index = Page.objects.get(url_path='/home/events/')
|
||||
request = event_index.dummy_request()
|
||||
response = event_index.make_preview_request()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
request = response.context_data['request']
|
||||
|
||||
# request should have the correct path and hostname for this page
|
||||
self.assertEqual(request.path, '/events/')
|
||||
|
|
@ -1471,11 +1472,13 @@ class TestDummyRequest(TestCase):
|
|||
self.assertIn('wsgi.multiprocess', request.META)
|
||||
self.assertIn('wsgi.run_once', request.META)
|
||||
|
||||
def test_dummy_request_for_accessible_page_https(self):
|
||||
def test_make_preview_request_for_accessible_page_https(self):
|
||||
Site.objects.update(port=443)
|
||||
|
||||
event_index = Page.objects.get(url_path='/home/events/')
|
||||
request = event_index.dummy_request()
|
||||
response = event_index.make_preview_request()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
request = response.context_data['request']
|
||||
|
||||
# request should have the correct path and hostname for this page
|
||||
self.assertEqual(request.path, '/events/')
|
||||
|
|
@ -1496,11 +1499,13 @@ class TestDummyRequest(TestCase):
|
|||
self.assertIn('wsgi.multiprocess', request.META)
|
||||
self.assertIn('wsgi.run_once', request.META)
|
||||
|
||||
def test_dummy_request_for_accessible_page_non_standard_port(self):
|
||||
def test_make_preview_request_for_accessible_page_non_standard_port(self):
|
||||
Site.objects.update(port=8888)
|
||||
|
||||
event_index = Page.objects.get(url_path='/home/events/')
|
||||
request = event_index.dummy_request()
|
||||
response = event_index.make_preview_request()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
request = response.context_data['request']
|
||||
|
||||
# request should have the correct path and hostname for this page
|
||||
self.assertEqual(request.path, '/events/')
|
||||
|
|
@ -1521,7 +1526,7 @@ class TestDummyRequest(TestCase):
|
|||
self.assertIn('wsgi.multiprocess', request.META)
|
||||
self.assertIn('wsgi.run_once', request.META)
|
||||
|
||||
def test_dummy_request_for_accessible_page_with_original_request(self):
|
||||
def test_make_preview_request_for_accessible_page_with_original_request(self):
|
||||
event_index = Page.objects.get(url_path='/home/events/')
|
||||
original_headers = {
|
||||
'REMOTE_ADDR': '192.168.0.1',
|
||||
|
|
@ -1532,7 +1537,9 @@ class TestDummyRequest(TestCase):
|
|||
}
|
||||
factory = RequestFactory(**original_headers)
|
||||
original_request = factory.get('/home/events/')
|
||||
request = event_index.dummy_request(original_request)
|
||||
response = event_index.make_preview_request(original_request)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
request = response.context_data['request']
|
||||
|
||||
# request should have the all the special headers we set in original_request
|
||||
self.assertEqual(request.META['REMOTE_ADDR'], original_request.META['REMOTE_ADDR'])
|
||||
|
|
@ -1557,9 +1564,11 @@ class TestDummyRequest(TestCase):
|
|||
self.assertIn('wsgi.run_once', request.META)
|
||||
|
||||
@override_settings(ALLOWED_HOSTS=['production.example.com'])
|
||||
def test_dummy_request_for_inaccessible_page_should_use_valid_host(self):
|
||||
def test_make_preview_request_for_inaccessible_page_should_use_valid_host(self):
|
||||
root_page = Page.objects.get(url_path='/')
|
||||
request = root_page.dummy_request()
|
||||
response = root_page.make_preview_request()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
request = response.context_data['request']
|
||||
|
||||
# in the absence of an actual Site record where we can access this page,
|
||||
# dummy_request should still provide a hostname that Django's host header
|
||||
|
|
@ -1569,7 +1578,9 @@ class TestDummyRequest(TestCase):
|
|||
@override_settings(ALLOWED_HOSTS=['*'])
|
||||
def test_dummy_request_for_inaccessible_page_with_wildcard_allowed_hosts(self):
|
||||
root_page = Page.objects.get(url_path='/')
|
||||
request = root_page.dummy_request()
|
||||
response = root_page.make_preview_request()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
request = response.context_data['request']
|
||||
|
||||
# '*' is not a valid hostname, so ensure that we replace it with something sensible
|
||||
self.assertNotEqual(request.META['HTTP_HOST'], '*')
|
||||
|
|
|
|||
|
|
@ -108,11 +108,13 @@ class TestLinkRewriterTagReplacing(TestCase):
|
|||
self.assertEqual(page_type_link, '<a href="/article/3">')
|
||||
|
||||
# but it should also be able to handle other supported
|
||||
# link types (email, external) even if no rules is provided
|
||||
# link types (email, external, anchor) even if no rules is provided
|
||||
external_type_link = rewriter('<a href="https://wagtail.io/">')
|
||||
self.assertEqual(external_type_link, '<a href="https://wagtail.io/">')
|
||||
email_type_link = rewriter('<a href="mailto:test@wagtail.io">')
|
||||
self.assertEqual(email_type_link, '<a href="mailto:test@wagtail.io">')
|
||||
anchor_type_link = rewriter('<a href="#test">')
|
||||
self.assertEqual(anchor_type_link, '<a href="#test">')
|
||||
|
||||
# As well as link which don't have any linktypes
|
||||
link_without_linktype = rewriter('<a data-link="https://wagtail.io">')
|
||||
|
|
@ -131,6 +133,7 @@ class TestLinkRewriterTagReplacing(TestCase):
|
|||
'page': lambda attrs: '<a href="/article/{}">'.format(attrs['id']),
|
||||
'external': lambda attrs: '<a rel="nofollow" href="{}">'.format(attrs['href']),
|
||||
'email': lambda attrs: '<a data-email="true" href="{}">'.format(attrs['href']),
|
||||
'anchor': lambda attrs: '<a data-anchor="true" href="{}">'.format(attrs['href']),
|
||||
'custom': lambda attrs: '<a data-phone="true" href="{}">'.format(attrs['href']),
|
||||
}
|
||||
rewriter = LinkRewriter(rules)
|
||||
|
|
@ -146,6 +149,8 @@ class TestLinkRewriterTagReplacing(TestCase):
|
|||
self.assertEqual(external_type_link_http, '<a rel="nofollow" href="http://wagtail.io/">')
|
||||
email_type_link = rewriter('<a href="mailto:test@wagtail.io">')
|
||||
self.assertEqual(email_type_link, '<a data-email="true" href="mailto:test@wagtail.io">')
|
||||
anchor_type_link = rewriter('<a href="#test">')
|
||||
self.assertEqual(anchor_type_link, '<a data-anchor="true" href="#test">')
|
||||
|
||||
# But not the unsupported ones.
|
||||
link_with_no_linktype = rewriter('<a href="tel:+4917640206387">')
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@ class TestPageUrlTags(TestCase):
|
|||
result = tpl.render(template.Context({'page': page, 'request': HttpRequest()}))
|
||||
self.assertIn('<a href="/events/">Events</a>', result)
|
||||
|
||||
def test_pageurl_with_null_site_in_request(self):
|
||||
page = Page.objects.get(url_path='/home/events/')
|
||||
tpl = template.Template('''{% load wagtailcore_tags %}<a href="{% pageurl page %}">{{ page.title }}</a>''')
|
||||
|
||||
# 'request' object in context, but site is None
|
||||
request = HttpRequest()
|
||||
request.site = None
|
||||
result = tpl.render(template.Context({'page': page, 'request': request}))
|
||||
self.assertIn('<a href="/events/">Events</a>', result)
|
||||
|
||||
def test_bad_pageurl(self):
|
||||
tpl = template.Template('''{% load wagtailcore_tags %}<a href="{% pageurl page %}">{{ page.title }}</a>''')
|
||||
|
||||
|
|
@ -96,6 +106,13 @@ class TestPageUrlTags(TestCase):
|
|||
result = slugurl(template.Context({'request': HttpRequest()}), 'events')
|
||||
self.assertEqual(result, '/events/')
|
||||
|
||||
def test_slugurl_with_null_site_in_request(self):
|
||||
# 'request' object in context, but site is None
|
||||
request = HttpRequest()
|
||||
request.site = None
|
||||
result = slugurl(template.Context({'request': request}), 'events')
|
||||
self.assertEqual(result, '/events/')
|
||||
|
||||
|
||||
class TestSiteRootPathsCache(TestCase):
|
||||
fixtures = ['test.json']
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from django.urls import reverse
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from wagtail.admin.utils import get_object_usage
|
||||
from wagtail.admin.models import get_object_usage
|
||||
from wagtail.core.models import CollectionMember
|
||||
from wagtail.search import index
|
||||
from wagtail.search.queryset import SearchableQuerySetMixin
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from django.shortcuts import get_object_or_404, render
|
|||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from wagtail.admin.auth import PermissionPolicyChecker
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.modal_workflow import render_modal_workflow
|
||||
from wagtail.admin.utils import PermissionPolicyChecker
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import Collection
|
||||
from wagtail.documents.forms import get_document_form
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ from django.utils.translation import ugettext as _
|
|||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.auth import PermissionPolicyChecker, permission_denied
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.utils import PermissionPolicyChecker, permission_denied, popular_tags_for_model
|
||||
from wagtail.admin.models import popular_tags_for_model
|
||||
from wagtail.core.models import Collection
|
||||
from wagtail.documents.forms import get_document_form
|
||||
from wagtail.documents.models import get_document_model
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from django.utils.encoding import force_text
|
|||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from wagtail.admin.utils import PermissionPolicyChecker
|
||||
from wagtail.admin.auth import PermissionPolicyChecker
|
||||
from wagtail.core.models import Collection
|
||||
from wagtail.search.backends import get_search_backends
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ from django.utils.translation import ugettext, ungettext
|
|||
|
||||
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
|
||||
from wagtail.admin.menu import MenuItem
|
||||
from wagtail.admin.navigation import get_site_for_user
|
||||
from wagtail.admin.rich_text import HalloPlugin
|
||||
from wagtail.admin.search import SearchArea
|
||||
from wagtail.admin.site_summary import SummaryItem
|
||||
from wagtail.admin.utils import get_site_for_user
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import BaseViewRestriction
|
||||
from wagtail.core.wagtail_hooks import require_wagtail_login
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from taggit.managers import TaggableManager
|
|||
from unidecode import unidecode
|
||||
from willow.image import Image as WillowImage
|
||||
|
||||
from wagtail.admin.utils import get_object_usage
|
||||
from wagtail.admin.models import get_object_usage
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import CollectionMember
|
||||
from wagtail.images.exceptions import InvalidFilterSpecError
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
{% trans "Choose an image" as choose_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=choose_str merged=1 tabbed=1 icon="image" %}
|
||||
|
||||
{{ uploadform.media.js }}
|
||||
{{ uploadform.media.css }}
|
||||
|
||||
{% if uploadform %}
|
||||
<ul class="tab-nav merged">
|
||||
<li class="{% if not uploadform.errors %}active{% endif %}"><a href="#search" >{% trans "Search" %}</a></li>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@
|
|||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
|
||||
{{ form.media.js }}
|
||||
|
||||
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
|
||||
<script>
|
||||
$(function() {
|
||||
|
|
@ -16,6 +18,11 @@
|
|||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
{{ form.media.css }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "Add image" as add_str %}
|
||||
{% include "wagtailadmin/shared/header.html" with title=add_str icon="image" %}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
|
||||
{{ form.media.css }}
|
||||
|
||||
<!-- Focal point chooser -->
|
||||
<link rel="stylesheet" href="{% static 'wagtailimages/css/vendor/jquery.Jcrop.min.css' %}" type="text/css">
|
||||
<link rel="stylesheet" href="{% static 'wagtailimages/css/focal-point-chooser.css' %}" type="text/css">
|
||||
|
|
@ -12,6 +14,8 @@
|
|||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
|
||||
{{ form.media.js }}
|
||||
|
||||
{% url 'wagtailadmin_tag_autocomplete' as autocomplete_url %}
|
||||
<script>
|
||||
$(function() {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
|
||||
{{ form_media.css }}
|
||||
|
||||
<link rel="stylesheet" href="{% static 'wagtailimages/css/add-multiple.css' %}" type="text/css" />
|
||||
{% endblock %}
|
||||
|
||||
|
|
@ -75,6 +77,8 @@
|
|||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
|
||||
{{ form_media.js }}
|
||||
|
||||
<!-- this exact order of plugins is vital -->
|
||||
<script src="{% static 'wagtailimages/js/vendor/load-image.min.js' %}"></script>
|
||||
<script src="{% static 'wagtailimages/js/vendor/canvas-to-blob.min.js' %}"></script>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from django.utils.http import RFC3986_SUBDELIMS, urlquote
|
|||
|
||||
from wagtail.core.models import Collection, GroupCollectionPermission
|
||||
from wagtail.images.views.serve import generate_signature
|
||||
from wagtail.tests.testapp.models import CustomImage
|
||||
from wagtail.tests.utils import WagtailTestUtils
|
||||
|
||||
from .utils import Image, get_test_image_file
|
||||
|
|
@ -108,6 +109,9 @@ class TestImageAddView(TestCase, WagtailTestUtils):
|
|||
# Ensure the form supports file uploads
|
||||
self.assertContains(response, 'enctype="multipart/form-data"')
|
||||
|
||||
# draftail should NOT be a standard JS include on this page
|
||||
self.assertNotContains(response, 'wagtailadmin/js/draftail.js')
|
||||
|
||||
def test_get_with_collections(self):
|
||||
root_collection = Collection.get_first_root_node()
|
||||
root_collection.add_child(name="Evil plans")
|
||||
|
|
@ -119,6 +123,21 @@ class TestImageAddView(TestCase, WagtailTestUtils):
|
|||
self.assertContains(response, '<label for="id_collection">')
|
||||
self.assertContains(response, "Evil plans")
|
||||
|
||||
@override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImage')
|
||||
def test_get_with_custom_image_model(self):
|
||||
response = self.get()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailimages/images/add.html')
|
||||
|
||||
# Ensure the form supports file uploads
|
||||
self.assertContains(response, 'enctype="multipart/form-data"')
|
||||
|
||||
# custom fields should be included
|
||||
self.assertContains(response, 'name="fancy_caption"')
|
||||
|
||||
# form media should be imported
|
||||
self.assertContains(response, 'wagtailadmin/js/draftail.js')
|
||||
|
||||
def test_add(self):
|
||||
response = self.post({
|
||||
'title': "Test image",
|
||||
|
|
@ -325,6 +344,11 @@ class TestImageEditView(TestCase, WagtailTestUtils):
|
|||
# Ensure the form supports file uploads
|
||||
self.assertContains(response, 'enctype="multipart/form-data"')
|
||||
|
||||
# draftail should NOT be a standard JS include on this page
|
||||
# (see TestImageEditViewWithCustomImageModel - this confirms that form media
|
||||
# definitions are being respected)
|
||||
self.assertNotContains(response, 'wagtailadmin/js/draftail.js')
|
||||
|
||||
@override_settings(WAGTAIL_USAGE_COUNT_ENABLED=True)
|
||||
def test_with_usage_count(self):
|
||||
response = self.get()
|
||||
|
|
@ -514,6 +538,34 @@ class TestImageEditView(TestCase, WagtailTestUtils):
|
|||
self.assertContains(response, 'data-original-width="1024"')
|
||||
|
||||
|
||||
@override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImage')
|
||||
class TestImageEditViewWithCustomImageModel(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
||||
# Create an image to edit
|
||||
self.image = CustomImage.objects.create(
|
||||
title="Test image",
|
||||
file=get_test_image_file(),
|
||||
)
|
||||
|
||||
self.storage = self.image.file.storage
|
||||
|
||||
def get(self, params={}):
|
||||
return self.client.get(reverse('wagtailimages:edit', args=(self.image.id,)), params)
|
||||
|
||||
def test_get_with_custom_image_model(self):
|
||||
response = self.get()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailimages/images/edit.html')
|
||||
|
||||
# Ensure the form supports file uploads
|
||||
self.assertContains(response, 'enctype="multipart/form-data"')
|
||||
|
||||
# form media should be imported
|
||||
self.assertContains(response, 'wagtailadmin/js/draftail.js')
|
||||
|
||||
|
||||
class TestImageDeleteView(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
|
@ -571,6 +623,23 @@ class TestImageChooserView(TestCase, WagtailTestUtils):
|
|||
self.assertEqual(response_json['step'], 'chooser')
|
||||
self.assertTemplateUsed(response, 'wagtailimages/chooser/chooser.html')
|
||||
|
||||
# draftail should NOT be a standard JS include on this page
|
||||
self.assertNotIn('wagtailadmin/js/draftail.js', response_json['html'])
|
||||
|
||||
@override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImage')
|
||||
def test_with_custom_image_model(self):
|
||||
response = self.get()
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertEqual(response_json['step'], 'chooser')
|
||||
self.assertTemplateUsed(response, 'wagtailimages/chooser/chooser.html')
|
||||
|
||||
# custom form fields should be present
|
||||
self.assertIn('name="image-chooser-upload-fancy_caption"', response_json['html'])
|
||||
|
||||
# form media imports should appear on the page
|
||||
self.assertIn('wagtailadmin/js/draftail.js', response_json['html'])
|
||||
|
||||
def test_search(self):
|
||||
response = self.get({'q': "Hello"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
|
@ -898,6 +967,11 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailimages/multiple/add.html')
|
||||
|
||||
# draftail should NOT be a standard JS include on this page
|
||||
# (see TestMultipleImageUploaderWithCustomImageModel - this confirms that form media
|
||||
# definitions are being respected)
|
||||
self.assertNotContains(response, 'wagtailadmin/js/draftail.js')
|
||||
|
||||
@override_settings(WAGTAILIMAGES_MAX_UPLOAD_SIZE=1000)
|
||||
def test_add_max_file_size_context_variables(self):
|
||||
response = self.client.get(reverse('wagtailimages:add_multiple'))
|
||||
|
|
@ -1095,6 +1169,123 @@ class TestMultipleImageUploader(TestCase, WagtailTestUtils):
|
|||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
||||
@override_settings(WAGTAILIMAGES_IMAGE_MODEL='tests.CustomImage')
|
||||
class TestMultipleImageUploaderWithCustomImageModel(TestCase, WagtailTestUtils):
|
||||
"""
|
||||
This tests the multiple image upload views located in wagtailimages/views/multiple.py
|
||||
with a custom image model
|
||||
"""
|
||||
def setUp(self):
|
||||
self.login()
|
||||
|
||||
# Create an image for running tests on
|
||||
self.image = CustomImage.objects.create(
|
||||
title="Test image",
|
||||
file=get_test_image_file(),
|
||||
)
|
||||
|
||||
def test_add(self):
|
||||
"""
|
||||
This tests that the add view responds correctly on a GET request
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.get(reverse('wagtailimages:add_multiple'))
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTemplateUsed(response, 'wagtailimages/multiple/add.html')
|
||||
|
||||
# response should include form media for the image edit form
|
||||
self.assertContains(response, 'wagtailadmin/js/draftail.js')
|
||||
|
||||
def test_add_post(self):
|
||||
"""
|
||||
This tests that a POST request to the add view saves the image and returns an edit form
|
||||
"""
|
||||
response = self.client.post(reverse('wagtailimages:add_multiple'), {
|
||||
'files[]': SimpleUploadedFile('test.png', get_test_image_file().file.getvalue()),
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
self.assertTemplateUsed(response, 'wagtailimages/multiple/edit_form.html')
|
||||
|
||||
# Check image
|
||||
self.assertIn('image', response.context)
|
||||
self.assertEqual(response.context['image'].title, 'test.png')
|
||||
self.assertTrue(response.context['image'].file_size)
|
||||
self.assertTrue(response.context['image'].file_hash)
|
||||
|
||||
# Check form
|
||||
self.assertIn('form', response.context)
|
||||
self.assertEqual(response.context['form'].initial['title'], 'test.png')
|
||||
self.assertIn('caption', response.context['form'].fields)
|
||||
self.assertNotIn('not_editable_field', response.context['form'].fields)
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn('image_id', response_json)
|
||||
self.assertIn('form', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertEqual(response_json['image_id'], response.context['image'].id)
|
||||
self.assertTrue(response_json['success'])
|
||||
|
||||
def test_edit_post(self):
|
||||
"""
|
||||
This tests that a POST request to the edit view edits the image
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(reverse('wagtailimages:edit_multiple', args=(self.image.id, )), {
|
||||
('image-%d-title' % self.image.id): "New title!",
|
||||
('image-%d-tags' % self.image.id): "",
|
||||
('image-%d-caption' % self.image.id): "a boot stamping on a human face, forever",
|
||||
}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn('image_id', response_json)
|
||||
self.assertNotIn('form', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertEqual(response_json['image_id'], self.image.id)
|
||||
self.assertTrue(response_json['success'])
|
||||
|
||||
# check that image has been updated
|
||||
new_image = CustomImage.objects.get(id=self.image.id)
|
||||
self.assertEqual(new_image.title, "New title!")
|
||||
self.assertEqual(new_image.caption, "a boot stamping on a human face, forever")
|
||||
|
||||
def test_delete_post(self):
|
||||
"""
|
||||
This tests that a POST request to the delete view deletes the image
|
||||
"""
|
||||
# Send request
|
||||
response = self.client.post(reverse(
|
||||
'wagtailimages:delete_multiple', args=(self.image.id, )
|
||||
), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
# Check response
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response['Content-Type'], 'application/json')
|
||||
|
||||
# Make sure the image is deleted
|
||||
self.assertFalse(Image.objects.filter(id=self.image.id).exists())
|
||||
|
||||
# Check JSON
|
||||
response_json = json.loads(response.content.decode())
|
||||
self.assertIn('image_id', response_json)
|
||||
self.assertIn('success', response_json)
|
||||
self.assertEqual(response_json['image_id'], self.image.id)
|
||||
self.assertTrue(response_json['success'])
|
||||
|
||||
# check that image has been deleted
|
||||
self.assertEqual(CustomImage.objects.filter(id=self.image.id).count(), 0)
|
||||
|
||||
|
||||
class TestURLGeneratorView(TestCase, WagtailTestUtils):
|
||||
def setUp(self):
|
||||
# Create an image for running tests on
|
||||
|
|
|
|||
|
|
@ -569,6 +569,7 @@ class TestGetImageForm(TestCase, WagtailTestUtils):
|
|||
'focal_point_width',
|
||||
'focal_point_height',
|
||||
'caption',
|
||||
'fancy_caption',
|
||||
])
|
||||
|
||||
def test_file_field(self):
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ from django.shortcuts import get_object_or_404, render
|
|||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from wagtail.admin.auth import PermissionPolicyChecker
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.modal_workflow import render_modal_workflow
|
||||
from wagtail.admin.utils import PermissionPolicyChecker, popular_tags_for_model
|
||||
from wagtail.admin.models import popular_tags_for_model
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import Collection
|
||||
from wagtail.images import get_image_model
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ from django.utils.translation import ugettext as _
|
|||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.auth import PermissionPolicyChecker, permission_denied
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.utils import PermissionPolicyChecker, permission_denied, popular_tags_for_model
|
||||
from wagtail.admin.models import popular_tags_for_model
|
||||
from wagtail.core.models import Collection, Site
|
||||
from wagtail.images import get_image_model
|
||||
from wagtail.images.exceptions import InvalidFilterSpecError
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from django.utils.encoding import force_text
|
|||
from django.views.decorators.http import require_POST
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
|
||||
from wagtail.admin.utils import PermissionPolicyChecker
|
||||
from wagtail.admin.auth import PermissionPolicyChecker
|
||||
from wagtail.core.models import Collection
|
||||
from wagtail.images import get_image_model
|
||||
from wagtail.images.fields import ALLOWED_EXTENSIONS
|
||||
|
|
@ -93,16 +93,19 @@ def add(request):
|
|||
'error_message': '\n'.join(['\n'.join([force_text(i) for i in v]) for k, v in form.errors.items()]),
|
||||
})
|
||||
else:
|
||||
# Instantiate a dummy copy of the form that we can retrieve validation messages and media from;
|
||||
# actual rendering of forms will happen on AJAX POST rather than here
|
||||
form = ImageForm(user=request.user)
|
||||
|
||||
return render(request, 'wagtailimages/multiple/add.html', {
|
||||
'max_filesize': form.fields['file'].max_upload_size,
|
||||
'help_text': form.fields['file'].help_text,
|
||||
'allowed_extensions': ALLOWED_EXTENSIONS,
|
||||
'error_max_file_size': form.fields['file'].error_messages['file_too_large_unknown_size'],
|
||||
'error_accepted_file_types': form.fields['file'].error_messages['invalid_image'],
|
||||
'collections': collections_to_choose,
|
||||
})
|
||||
return render(request, 'wagtailimages/multiple/add.html', {
|
||||
'max_filesize': form.fields['file'].max_upload_size,
|
||||
'help_text': form.fields['file'].help_text,
|
||||
'allowed_extensions': ALLOWED_EXTENSIONS,
|
||||
'error_max_file_size': form.fields['file'].error_messages['file_too_large_unknown_size'],
|
||||
'error_accepted_file_types': form.fields['file'].error_messages['invalid_image'],
|
||||
'collections': collections_to_choose,
|
||||
'form_media': form.media,
|
||||
})
|
||||
|
||||
|
||||
@require_POST
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ from django.utils.translation import ugettext, ungettext
|
|||
|
||||
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
|
||||
from wagtail.admin.menu import MenuItem
|
||||
from wagtail.admin.navigation import get_site_for_user
|
||||
from wagtail.admin.rich_text import HalloPlugin
|
||||
from wagtail.admin.search import SearchArea
|
||||
from wagtail.admin.site_summary import SummaryItem
|
||||
from wagtail.admin.utils import get_site_for_user
|
||||
from wagtail.core import hooks
|
||||
from wagtail.images import admin_urls, get_image_model, image_operations
|
||||
from wagtail.images.api.admin.endpoints import ImagesAdminAPIEndpoint
|
||||
|
|
|
|||
|
|
@ -225,9 +225,9 @@ class BaseField:
|
|||
if hasattr(field, 'get_searchable_content'):
|
||||
value = field.get_searchable_content(value)
|
||||
elif isinstance(field, TaggableManager):
|
||||
# Special case for tags fields. Convert QuerySet of TaggedItems into QuerySet of Tags
|
||||
Tag = field.remote_field.model
|
||||
value = Tag.objects.filter(id__in=value.values_list('tag_id', flat=True))
|
||||
# As of django-taggit 1.0, value_from_object returns a list of Tag objects,
|
||||
# which matches what we want
|
||||
pass
|
||||
elif isinstance(field, RelatedField):
|
||||
# The type of the ForeignKey may have a get_searchable_content method that we should
|
||||
# call. Firstly we need to find the field its referencing but it may be referencing
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from django.core import checks
|
|||
from django.urls import reverse
|
||||
|
||||
from wagtail.admin.checks import check_panels_in_model
|
||||
from wagtail.admin.utils import get_object_usage
|
||||
from wagtail.admin.models import get_object_usage
|
||||
|
||||
SNIPPET_MODELS = []
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue