Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Dave Cranwell 2015-03-26 10:52:59 +00:00
commit 91c7947791
48 changed files with 680 additions and 489 deletions

View file

@ -1,11 +0,0 @@
image: kaedroho/django-base
env:
- DATABASE_HOST=postgres
- ELASTICSEARCH_URL=http://elasticsearch:9200/
script:
- pip3.4 install mock python-dateutil pytz elasticsearch
- python3.4 setup.py install
- python3.4 runtests.py
services:
- postgres
- dockerfile/elasticsearch

2
.gitignore vendored
View file

@ -1,3 +1,4 @@
*.swp
*.pyc
.DS_Store
/.coverage
@ -6,3 +7,4 @@
/wagtail.egg-info/
/docs/_build/
/.tox/
/venv

View file

@ -31,6 +31,11 @@ Changelog
* The `document_served` signal now correctly passes the Document class as `sender` and the document as `instance`
* Image/Document edit page no longer throws OSError when the original image is missing
* Page classes can specify an edit_handler property to override the default Content / Promote / Settings tabbed interface
* The Page model now records the date/time that a page was first published, as the field `first_published_at`
* Increased the maximum length of a page slug from 50 to 255 characters
* Plain text fields in the page editor now use auto-expanding text areas
* Date / time pickers now consistently use times without seconds, to prevent Javascript behaviour glitches when focusing / unfocusing fields
* Added hooks `register_rich_text_embed_handler` and `register_rich_text_link_handler` for customising link / embed handling within rich text fields
0.8.6 (10.03.2015)

View file

@ -52,12 +52,12 @@ Available at `wagtail.readthedocs.org <http://wagtail.readthedocs.org/>`_ and al
Need Support?
~~~~~~~~~~~~~~~
Ask your questions on our `Google Group <https://groups.google.com/forum/#!forum/wagtail>`_.
Ask your questions on our `Wagtail support group <https://groups.google.com/forum/#!forum/wagtail>`_.
Compatibility
~~~~~~~~~~~~~
Wagtail supports Django 1.7.0+ on Python 2.7, 3.3 and 3.4.
Wagtail supports Django 1.7.1+ on Python 2.7, 3.3 and 3.4.
Wagtail's dependencies are summarised at `requirements.io <https://requires.io/github/torchbox/wagtail/requirements>`_.
@ -71,3 +71,5 @@ We suggest you start by checking the `Help develop me! <https://github.com/torch
Send us a useful pull request and we'll post you a `t-shirt <https://twitter.com/WagtailCMS/status/432166799464210432/photo/1>`_.
We also welcome `translations <http://wagtail.readthedocs.org/en/latest/howto/contributing.html#translations>`_ for Wagtail's interface.
We run a separate Wagtail developers group here: https://groups.google.com/forum/#!forum/wagtail-developers please not that this is not for support requests.

View file

@ -8,7 +8,7 @@ Before you start
You can get basic Wagtail setup installed on your machine with only a few prerequisites. See the full `Dependencies`_ list below.
There are various optional components that will improve the performance and feature set of Wagtail. Our recommended software stack includes the PostgreSQL database, ElasticSearch (for free-text searching), the OpenCV library (for image feature detection), and Redis (as a cache and message queue backend). This would be a lot to install in one go. For this reason we provide a virtual machine image to use with `Vagrant <http://www.vagrantup.com/>`__, with all of these components ready installed.
There are various optional components that will improve the performance and feature set of Wagtail. Our recommended software stack includes the PostgreSQL database, Elasticsearch (for free-text searching), the OpenCV library (for image feature detection), and Redis (as a cache and message queue backend). This would be a lot to install in one go. For this reason we provide a virtual machine image to use with `Vagrant <http://www.vagrantup.com/>`__, with all of these components ready installed.
Whether you just want to try out the demo site, or you're ready to dive in and create a Wagtail site with all bells and whistles enabled, we strongly recommend the Vagrant approach. Nevertheless, if you're the sort of person who balks at the idea of downloading a whole operating system just to run a web app, we've got you covered too. Start from `Install Python`_.
@ -16,26 +16,9 @@ Whether you just want to try out the demo site, or you're ready to dive in and c
Dependencies
============
Barebones
---------
Wagtail is based on the Django web framework and various other Python libraries. For the full list of absolutely required libraries, see `setup.py <https://github.com/torchbox/wagtail/blob/master/setup.py>`__.
The basic Wagtail installation is pure Python. No build tools are required on the host machine.
For the full list of absolutely required libraries, see `setup.py <https://github.com/torchbox/wagtail/blob/master/setup.py>`__.
If you are installing Wagtail differently (e.g. from the Git repository), you must run ``python setup.py install`` from the repository root. The above command will install all Wagtail dependencies.
Administration UI
-----------------
.. warning::
The administrative interface requires django-libsass and Pillow. The project template bundled with Wagtail includes them (see :doc:`creating_your_project`). You must add the above libraries if you are adding Wagtail to an existing project, unless you will be using it
in a purely framework fashion without visiting wagtailadmin or hooking it up to the urlconf.
* django-libsass>=0.2
* Pillow>=2.6.1
Both django-libsass and Pillow have native-code components that require further attention:
Most of Wagtail's dependencies are pure Python and will install automatically using ``pip``, but there are a few native-code components that require further attention:
* libsass-python (for compiling SASS stylesheets) - requires a C++ compiler and the Python development headers.
* Pillow (for image processing) - additionally requires libjpeg and zlib.
@ -78,6 +61,8 @@ The quickest way to install Wagtail is using pip. To get the latest stable versi
pip install wagtail
If you are installing Wagtail differently (e.g. from the Git repository), you must run ``python setup.py install`` from the repository root. The above command will install all Wagtail dependencies.
To check that Wagtail can be seen by Python, type ``python`` in your shell then try to import ``wagtail`` from the prompt:
.. code-block:: python
@ -107,12 +92,12 @@ To enable Postgres for your project, uncomment the ``psycopg2`` line from your p
This assumes that your PostgreSQL instance is configured to allow you to connect as the 'postgres' user - if not, you'll need to adjust the ``createdb`` line and the database settings in settings/base.py accordingly.
ElasticSearch
Elasticsearch
-------------
Wagtail integrates with ElasticSearch to provide full-text searching of your content, both within the Wagtail interface and on your site's front-end. If ElasticSearch is not available, Wagtail will fall back to much more basic search functionality using database queries. ElasticSearch is pre-installed as part of the Vagrant virtual machine image; non-Vagrant users can use the `debian.sh <https://github.com/torchbox/wagtail/blob/master/scripts/install/debian.sh>`__ or `ubuntu.sh <https://github.com/torchbox/wagtail/blob/master/scripts/install/ubuntu.sh>`__ installation scripts as a guide.
Wagtail integrates with Elasticsearch to provide full-text searching of your content, both within the Wagtail interface and on your site's front-end. If Elasticsearch is not available, Wagtail will fall back to much more basic search functionality using database queries. Elasticsearch is pre-installed as part of the Vagrant virtual machine image; non-Vagrant users can use the `debian.sh <https://github.com/torchbox/wagtail/blob/master/scripts/install/debian.sh>`__ or `ubuntu.sh <https://github.com/torchbox/wagtail/blob/master/scripts/install/ubuntu.sh>`__ installation scripts as a guide.
To enable ElasticSearch for your project, uncomment the ``elasticsearch`` line from your project's requirements.txt, and in ``myprojectname/settings/base.py``, uncomment the WAGTAILSEARCH_BACKENDS section. Then run::
To enable Elasticsearch for your project, uncomment the ``elasticsearch`` line from your project's requirements.txt, and in ``myprojectname/settings/base.py``, uncomment the WAGTAILSEARCH_BACKENDS section. Then run::
pip install -r requirements.txt
./manage.py update_index

View file

@ -18,6 +18,70 @@ Coding guidelines
* Python 2 and 3 compatibility. All contributions should support Python 2 and 3 and we recommend using the `six <https://pythonhosted.org/six/>`_ compatibility library (use the pip version installed as a dependency, not the version bundled with Django).
* Tests. Wagtail has a suite of tests, which we are committed to improving and expanding. We run continuous integration at `travis-ci.org/torchbox/wagtail <https://travis-ci.org/torchbox/wagtail>`_ to ensure that no commits or pull requests introduce test failures. If your contributions add functionality to Wagtail, please include the additional tests to cover it; if your contributions alter existing functionality, please update the relevant tests accordingly.
Running the unit tests
~~~~~~~~~~~~~~~~~~~~~~
In order to run Wagtail's test suite, you will need to install some dependencies first. We recommend installing these into a virtual environment.
**Setting up the virtual environment**
If you are using Python 3.3 or above, run the following commands in your shell
at the root of the Wagtail repo::
pyvenv venv
source venv/bin/activate
python setup.py develop
pip install -r requirements-dev.txt
For Python 2, you will need to install the ``virtualenv`` package and replace
the first line above with:
virtualenv venv
**Running the tests**
With your virtual environment active, run the following command to run all the
tests::
python runtests.py
**Running only some of the tests**
At the time of writing, Wagtail has nearly 1000 tests which takes a while to
run. You can run tests for only one part of Wagtail by passing in the path as
an argument to ``runtests.py``::
python runtests.py wagtail.wagtailcore
**Testing against PostgreSQL**
By default, Wagtail tests against SQLite. If you need to test against a
different database, set the ``DATABASE_ENGINE`` environment variable to the
name of the Django database backend to test against::
DATABASE_ENGINE=django.db.backends.postgresql_psycopg2 python runtests.py
This will create a new database called ``test_wagtail`` in PostgreSQL and run
the tests against it.
If you need to use a different user, password or host. Use the ``PGUSER``, ``PGPASSWORD`` and ``PGHOST`` environment variables.
**Testing Elasticsearch**
To test Elasticsearch, you need to have the ``elasticsearch`` package installed.
Once installed, Wagtail will attempt to connect to a local instance of
Elasticsearch (``http://localhost:9200``) and use the index ``test_wagtail``.
If your Elasticsearch instance is located somewhere else, you can set the
``ELASTICSEARCH_URL`` environment variable to point to its location::
ELASTICSEARCH_URL=http://my-elasticsearch-instance:9200 python runtests.py
If you no longer want Wagtail to test against Elasticsearch, uninstall the
``elasticsearch`` package.
Styleguide
~~~~~~~~~~

View file

@ -13,14 +13,14 @@ We have tried to minimise external dependencies for a working installation of Wa
Cache
-----
We recommend `Redis <http://redis.io/>`_ as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: ``sudo apt-get install redis-server``), add ``django-redis-cache`` to your requirements.txt, and enable it as a cache backend::
We recommend `Redis <http://redis.io/>`_ as a fast, persistent cache. Install Redis through your package manager (on Debian or Ubuntu: ``sudo apt-get install redis-server``), add ``django-redis`` to your requirements.txt, and enable it as a cache backend::
CACHES = {
'default': {
'BACKEND': 'redis_cache.cache.RedisCache',
'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': '127.0.0.1:6379',
'OPTIONS': {
'CLIENT_CLASS': 'redis_cache.client.DefaultClient',
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
}
}
}

View file

@ -63,11 +63,9 @@ Apps (settings.py)
'django.contrib.messages',
'django.contrib.staticfiles',
'south',
'compressor',
'taggit',
'modelcluster',
'django.contrib.admin',
'wagtail.wagtailcore',
'wagtail.wagtailadmin',
@ -89,11 +87,6 @@ Wagtail requires several Django app modules, third-party apps, and defines sever
Third-Party Apps
----------------
``south``
Used for database migrations. See `South Documentation`_.
.. _South Documentation: http://south.readthedocs.org/en/latest/
``compressor``
Static asset combiner and minifier for Django. Compressor also enables for the use of preprocessors. See `Compressor Documentation`_.
@ -109,9 +102,6 @@ Third-Party Apps
.. _the django-modelcluster github project page: https://github.com/torchbox/django-modelcluster
``django.contrib.admin``
The Django admin module. While Wagtail will eventually provide a sites-editing interface, the Django admin is included for now to provide that functionality.
Wagtail Apps
------------
@ -270,13 +260,13 @@ URL Patterns
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls
from wagtail.wagtailsearch import urls as wagtailsearch_urls
urlpatterns = patterns('',
url(r'^django-admin/', include(admin.site.urls)),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^search/', include(wagtailsearch_frontend_urls)),
url(r'^search/', include(wagtailsearch_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# Optional urlconf for including your own vanilla Django urls/views
@ -438,11 +428,9 @@ settings.py
'django.contrib.messages',
'django.contrib.staticfiles',
'south',
'compressor',
'taggit',
'modelcluster',
'django.contrib.admin',
'wagtail.wagtailcore',
'wagtail.wagtailadmin',
@ -538,14 +526,14 @@ urls.py
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.wagtailsearch.urls import frontend as wagtailsearch_frontend_urls
from wagtail.wagtailsearch import urls as wagtailsearch__urls
urlpatterns = patterns('',
url(r'^django-admin/', include(admin.site.urls)),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^search/', include(wagtailsearch_frontend_urls)),
url(r'^search/', include(wagtailsearch_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# For anything not caught by a more specific rule above, hand over to

View file

@ -22,6 +22,9 @@ Core
----
* Added validation to prevent pages being created with only whitespace characters in their title fields
* The Page model now records the date/time that a page was first published, as the field ``first_published_at``
* Increased the maximum length of a page slug from 50 to 255 characters
* Added hooks ``register_rich_text_embed_handler`` and ``register_rich_text_link_handler`` for customising link / embed handling within rich text fields
Admin
@ -29,12 +32,13 @@ Admin
**UI**
* Improvements to the layout of the admin menu footer
* Improvements to the layout of the left-hand menu footer
* Added thousands separator for counters on dashboard
* Added contextual links to admin notification messages
* When copying pages, it is now possible to specify a place to copy to
* Added pagination to the snippets listing and chooser
* Page / document / image / snippet choosers now include a link to edit the chosen item
* Plain text fields in the page editor now use auto-expanding text areas
**Page editor**
@ -60,6 +64,7 @@ Admin
* Removed the need to add permission check on admin views (now automated)
* Reversing ``django.contrib.auth.admin.login`` will no longer lead to Wagtails login view (making it easier to have front end views)
* Added cache-control headers to all admin views. This allows Varnish/Squid/CDN to run on vanilla settings in front of a Wagtail site
* Date / time pickers now consistently use times without seconds, to prevent Javascript behaviour glitches when focusing / unfocusing fields
Project template
@ -105,6 +110,15 @@ It is no longer necessary to pass the base model as a parameter, so this declara
The old format is now deprecated; all existing ``InlinePanel`` declarations should be updated to the new format.
You no longer need ``LOGIN_URL`` and ``LOGIN_REDIRECT_URL`` to point to Wagtail admin.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are upgrading from an older version of Wagtail, you probably want to remove these from your project settings.
Prevously, these two settings needed to be set to ``wagtailadmin_login`` and ``wagtailadmin_dashboard``
respectively or Wagtail would become very tricky to log in to. This is no longer the case and Wagtail
should work fine without them.
Login/Password reset views renamed
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -113,9 +127,6 @@ This is no longer possible. Update any references to ``wagtailadmin_login``.
Password reset view name has changed from ``password_reset`` to ``wagtailadmin_password_reset``.
You no longer need ``LOGIN_URL`` and ``LOGIN_REDIRECT_URL`` to point to Wagtail admin.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Javascript includes in admin backend have been moved
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -4,7 +4,6 @@ Release notes
.. toctree::
:maxdepth: 1
roadmap
0.9
0.8.6
0.8.5

View file

@ -1,28 +0,0 @@
Roadmap
-------
The story so far
~~~~~~~~~~~~~~~~
Wagtail was developed for the Royal College of Art in 2013, and launched as an open source project in February 2014. The changes since its launch are recorded in the codebase:
https://raw.github.com/torchbox/wagtail/master/CHANGELOG.txt
In summary:
* February 2014: Reduced dependencies, basic documentation, translations, tests
What's next
~~~~~~~~~~~
The `issue list <https://github.com/torchbox/wagtail/issues>`_ gives a detailed view of the immediate tasks, but our current broad priorities are:
* Better documentation: simple setup guides for all levels of user, a manual for editors and administrators, in-depth intstructions for Django developers.
* Block-level editing UI (see `Sir Trevor <http://madebymany.github.io/sir-trevor-js/>`_)
* Support for an HTML content type
* Simple inline stats
You decide
~~~~~~~~~~
Please help us focus on the right things by raising issues for new features, or joining the discussion on existing issues.

View file

@ -1,6 +1,9 @@
# For coverage and PEP8 linting
coverage>=3.7.0
flake8>=2.2.0
# Required for running the tests
mock>=1.0.0
python-dateutil>=2.2
pytz>=2014.7
Pillow>=2.7.0
# For coverage and PEP8 linting
coverage>=3.7.0
flake8>=2.2.0

View file

@ -28,11 +28,13 @@ PY3 = sys.version_info[0] == 3
install_requires = [
"Django>=1.7.0,<1.8",
"Django>=1.7.1,<1.8",
"django-compressor>=1.4",
"django-libsass>=0.2",
"django-modelcluster>=0.5",
"django-taggit==0.12.3",
"django-treebeard==3.0",
"Pillow>=2.6.1",
"beautifulsoup4>=4.3.2",
"html5lib==0.999",
"Unidecode>=0.04.14",

View file

@ -21,7 +21,7 @@ base =
coverage
dj17 =
Django>=1.7,<1.8
Django>=1.7.1,<1.8
dj18 =
https://github.com/django/django/archive/stable/1.8.x.zip#egg=django

View file

@ -1,13 +1,11 @@
# Minimal requirements
Django>=1.7,<1.8
Django>=1.7.1,<1.8
wagtail==0.8.6
django-libsass>=0.2
Pillow>=2.6.1
# Recommended components (require additional setup):
# psycopg2==2.5.2
# elasticsearch==1.1.1
# Recommended components to improve performance in production:
# django-redis-cache==0.13.0
# django-redis==3.8.2
# django-celery==3.1.10

View file

@ -11,13 +11,8 @@ MEDIA_URL = '/media/'
DATABASES = {
'default': {
'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.postgresql_psycopg2'),
'NAME': os.environ.get('DATABASE_NAME', 'wagtaildemo'),
'TEST_NAME': os.environ.get('DATABASE_NAME', 'test_wagtaildemo'),
'USER': os.environ.get('DATABASE_USER', 'postgres'),
'PASSWORD': os.environ.get('DATABASE_PASS', None),
'HOST': os.environ.get('DATABASE_HOST', None),
'PORT': os.environ.get('DATABASE_PORT', None),
'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.sqlite3'),
'NAME': os.environ.get('DATABASE_NAME', 'wagtail'),
}
}

View file

@ -25,26 +25,43 @@ from wagtail.wagtailcore.utils import camelcase_to_underscore, resolve_model_str
from wagtail.utils.deprecation import RemovedInWagtail11Warning
# Form field properties to override whenever we encounter a model field
# that matches one of these types - including subclasses
FORM_FIELD_OVERRIDES = {
models.DateField: {'widget': widgets.AdminDateInput},
models.TimeField: {'widget': widgets.AdminTimeInput},
models.DateTimeField: {'widget': widgets.AdminDateTimeInput},
TaggableManager: {'widget': widgets.AdminTagWidget}
TaggableManager: {'widget': widgets.AdminTagWidget},
}
# Form field properties to override whenever we encounter a model field
# that matches one of these types exactly, ignoring subclasses.
# (This allows us to override the widget for models.TextField, but leave
# the RichTextField widget alone)
DIRECT_FORM_FIELD_OVERRIDES = {
models.TextField: {'widget': widgets.AdminAutoHeightTextInput},
}
# Callback to allow us to override the default form fields provided for each model field.
def formfield_for_dbfield(db_field, **kwargs):
# snarfed from django/contrib/admin/options.py
# adapted from django/contrib/admin/options.py
overrides = None
# If we've got overrides for the formfield defined, use 'em. **kwargs
# passed to formfield_for_dbfield override the defaults.
for klass in db_field.__class__.mro():
if klass in FORM_FIELD_OVERRIDES:
kwargs = dict(copy.deepcopy(FORM_FIELD_OVERRIDES[klass]), **kwargs)
return db_field.formfield(**kwargs)
if db_field.__class__ in DIRECT_FORM_FIELD_OVERRIDES:
overrides = DIRECT_FORM_FIELD_OVERRIDES[db_field.__class__]
else:
for klass in db_field.__class__.mro():
if klass in FORM_FIELD_OVERRIDES:
overrides = FORM_FIELD_OVERRIDES[klass]
break
if overrides:
kwargs = dict(copy.deepcopy(overrides), **kwargs)
# For any other type of field, just call its formfield() method.
return db_field.formfield(**kwargs)

View file

@ -104,7 +104,7 @@ function initDateTimeChooser(id) {
if (window.dateTimePickerTranslations) {
$('#' + id).datetimepicker({
closeOnDateSelect: true,
format: 'Y-m-d H:i:s',
format: 'Y-m-d H:i',
scrollInput:false,
i18n: {
lang: window.dateTimePickerTranslations
@ -113,7 +113,7 @@ function initDateTimeChooser(id) {
});
} else {
$('#' + id).datetimepicker({
format: 'Y-m-d H:i:s',
format: 'Y-m-d H:i',
});
}
}

View file

@ -267,7 +267,8 @@
}
.struct-block .widget-text_input > label,
.struct-block .widget-textarea > label{
.struct-block .widget-textarea > label,
.struct-block .widget-admin_auto_height_text_input > label{
display:none;
}

View file

@ -18,7 +18,7 @@
</head>
<body class="{% block bodyclass %}{% endblock %} {% if messages %}has-messages{% endif %}">
<!--[if lt IE 9]>
<p class="capabilitymessage">{% blocktrans %}You are using an <strong>outdated</strong> browser not supported by this software. Please <a href="http://browsehappy.com/">upgrade your browser</a> or <a href="http://www.google.com/chromeframe/?redirect=true">activate Google Chrome Frame</a>.{% endblocktrans %}</p>
<p class="capabilitymessage">{% blocktrans %}You are using an <strong>outdated</strong> browser not supported by this software. Please <a href="http://browsehappy.com/">upgrade your browser</a>.{% endblocktrans %}</p>
<![endif]-->
<noscript class="capabilitymessage">{% trans 'Javascript is required to use Wagtail, but it is currently disabled' %}</noscript>

View file

@ -16,10 +16,11 @@ from wagtail.wagtailadmin.edit_handlers import (
InlinePanel,
)
from wagtail.wagtailadmin.widgets import AdminPageChooser, AdminDateInput
from wagtail.wagtailadmin.widgets import AdminPageChooser, AdminDateInput, AdminAutoHeightTextInput
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailcore.models import Page, Site
from wagtail.tests.models import PageChooserModel, EventPageChooserModel, EventPage, EventPageSpeaker
from wagtail.wagtailcore.fields import RichTextArea
from wagtail.tests.models import PageChooserModel, EventPageChooserModel, EventPage, EventPageSpeaker, SimplePage
from wagtail.tests.utils import WagtailTestUtils
from wagtail.utils.deprecation import RemovedInWagtail11Warning
@ -43,6 +44,22 @@ class TestGetFormForModel(TestCase):
self.assertIn('speakers', form.formsets)
self.assertIn('related_links', form.formsets)
def test_direct_form_field_overrides(self):
# Test that field overrides defined through DIRECT_FORM_FIELD_OVERRIDES
# are applied
SimplePageForm = get_form_for_model(SimplePage)
simple_form = SimplePageForm()
# plain TextFields should use AdminAutoHeightTextInput as the widget
self.assertEqual(type(simple_form.fields['content'].widget), AdminAutoHeightTextInput)
# This override should NOT be applied to subclasses of TextField such as
# RichTextField - they should retain their default widgets
EventPageForm = get_form_for_model(EventPage)
event_form = EventPageForm()
self.assertEqual(type(event_form.fields['body'].widget), RichTextArea)
def test_get_form_for_model_with_specific_fields(self):
EventPageForm = get_form_for_model(EventPage, fields=['date_from'], formsets=['speakers'])
form = EventPageForm()

View file

@ -1,5 +1,6 @@
from datetime import timedelta
import unittest
import mock
from django.test import TestCase
from django.core.urlresolvers import reverse
@ -212,6 +213,7 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertEqual(page.title, post_data['title'])
self.assertIsInstance(page, SimplePage)
self.assertFalse(page.live)
self.assertFalse(page.first_published_at)
# treebeard should report no consistency problems with the tree
self.assertFalse(any(Page.find_problems()), 'treebeard found consistency problems')
@ -273,12 +275,8 @@ class TestPageCreation(TestCase, WagtailTestUtils):
def test_create_simplepage_post_publish(self):
# Connect a mock signal handler to page_published signal
signal_fired = [False]
signal_page = [None]
def page_published_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_published.connect(page_published_handler)
mock_handler = mock.MagicMock()
page_published.connect(mock_handler)
# Post
post_data = {
@ -298,11 +296,15 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertEqual(page.title, post_data['title'])
self.assertIsInstance(page, SimplePage)
self.assertTrue(page.live)
self.assertTrue(page.first_published_at)
# Check that the page_published signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], page)
self.assertEqual(signal_page[0], signal_page[0].specific)
self.assertEqual(mock_handler.call_count, 1)
mock_call = mock_handler.mock_calls[0][2]
self.assertEqual(mock_call['sender'], page.specific_class)
self.assertEqual(mock_call['instance'], page)
self.assertIsInstance(mock_call['instance'], page.specific_class)
# treebeard should report no consistency problems with the tree
self.assertFalse(any(Page.find_problems()), 'treebeard found consistency problems')
@ -333,6 +335,7 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertTrue(PageRevision.objects.filter(page=page).exclude(approved_go_live_at__isnull=True).exists())
# But Page won't be live
self.assertFalse(page.live)
self.assertFalse(page.first_published_at)
self.assertTrue(page.status_string, "scheduled")
def test_create_simplepage_post_submit(self):
@ -357,6 +360,7 @@ class TestPageCreation(TestCase, WagtailTestUtils):
self.assertEqual(page.title, post_data['title'])
self.assertIsInstance(page, SimplePage)
self.assertFalse(page.live)
self.assertFalse(page.first_published_at)
# The latest revision for the page should now be in moderation
self.assertTrue(page.get_latest_revision().submitted_for_moderation)
@ -438,12 +442,13 @@ class TestPageEdit(TestCase, WagtailTestUtils):
self.root_page = Page.objects.get(id=2)
# Add child page
self.child_page = SimplePage()
self.child_page.title = "Hello world!"
self.child_page.slug = "hello-world"
self.child_page.live = True
self.root_page.add_child(instance=self.child_page)
self.child_page.save_revision()
child_page = SimplePage(
title="Hello world!",
slug="hello-world",
)
self.root_page.add_child(instance=child_page)
child_page.save_revision().publish()
self.child_page = SimplePage.objects.get(id=child_page.id)
# Add event page (to test edit handlers)
self.event_page = EventPage()
@ -572,18 +577,17 @@ class TestPageEdit(TestCase, WagtailTestUtils):
def test_page_edit_post_publish(self):
# Connect a mock signal handler to page_published signal
signal_fired = [False]
signal_page = [None]
def page_published_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_published.connect(page_published_handler)
mock_handler = mock.MagicMock()
page_published.connect(mock_handler)
# Set has_unpublished_changes=True on the existing record to confirm that the publish action
# is resetting it (and not just leaving it alone)
self.child_page.has_unpublished_changes = True
self.child_page.save()
# Save current value of first_published_at so we can check that it doesn't change
first_published_at = SimplePage.objects.get(id=self.child_page.id).first_published_at
# Tests publish from edit page
post_data = {
'title': "I've been edited!",
@ -601,13 +605,19 @@ class TestPageEdit(TestCase, WagtailTestUtils):
self.assertEqual(child_page_new.title, post_data['title'])
# Check that the page_published signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], child_page_new)
self.assertEqual(signal_page[0], signal_page[0].specific)
self.assertEqual(mock_handler.call_count, 1)
mock_call = mock_handler.mock_calls[0][2]
self.assertEqual(mock_call['sender'], child_page_new.specific_class)
self.assertEqual(mock_call['instance'], child_page_new)
self.assertIsInstance(mock_call['instance'], child_page_new.specific_class)
# The page shouldn't have "has_unpublished_changes" flag set
self.assertFalse(child_page_new.has_unpublished_changes)
# first_published_at should not change as it was already set
self.assertEqual(first_published_at, child_page_new.first_published_at)
def test_edit_post_publish_scheduled(self):
go_live_at = timezone.now() + timedelta(days=1)
expire_at = timezone.now() + timedelta(days=2)
@ -902,12 +912,8 @@ class TestPageDelete(TestCase, WagtailTestUtils):
def test_page_delete_post(self):
# Connect a mock signal handler to page_unpublished signal
signal_fired = [False]
signal_page = [None]
def page_unpublished_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_unpublished.connect(page_unpublished_handler)
mock_handler = mock.MagicMock()
page_unpublished.connect(mock_handler)
# Post
response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )))
@ -922,9 +928,12 @@ class TestPageDelete(TestCase, WagtailTestUtils):
self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0)
# Check that the page_unpublished signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], self.child_page)
self.assertEqual(signal_page[0], signal_page[0].specific)
self.assertEqual(mock_handler.call_count, 1)
mock_call = mock_handler.mock_calls[0][2]
self.assertEqual(mock_call['sender'], self.child_page.specific_class)
self.assertEqual(mock_call['instance'], self.child_page)
self.assertIsInstance(mock_call['instance'], self.child_page.specific_class)
def test_page_delete_notlive_post(self):
# Same as above, but this makes sure the page_unpublished signal is not fired
@ -935,10 +944,8 @@ class TestPageDelete(TestCase, WagtailTestUtils):
self.child_page.save()
# Connect a mock signal handler to page_unpublished signal
signal_fired = [False]
def page_unpublished_handler(sender, instance, **kwargs):
signal_fired[0] = True
page_unpublished.connect(page_unpublished_handler)
mock_handler = mock.MagicMock()
page_unpublished.connect(mock_handler)
# Post
response = self.client.post(reverse('wagtailadmin_pages_delete', args=(self.child_page.id, )))
@ -953,7 +960,7 @@ class TestPageDelete(TestCase, WagtailTestUtils):
self.assertEqual(Page.objects.filter(path__startswith=self.root_page.path, slug='hello-world').count(), 0)
# Check that the page_unpublished signal was not fired
self.assertFalse(signal_fired[0])
self.assertEqual(mock_handler.call_count, 0)
def test_subpage_deletion(self):
# Connect mock signal handlers to page_unpublished, pre_delete and post_delete signals
@ -1472,12 +1479,8 @@ class TestPageUnpublish(TestCase, WagtailTestUtils):
This posts to the unpublish view and checks that the page was unpublished
"""
# Connect a mock signal handler to page_unpublished signal
signal_fired = [False]
signal_page = [None]
def page_unpublished_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_unpublished.connect(page_unpublished_handler)
mock_handler = mock.MagicMock()
page_unpublished.connect(mock_handler)
# Post to the unpublish page
response = self.client.post(reverse('wagtailadmin_pages_unpublish', args=(self.page.id, )))
@ -1489,9 +1492,12 @@ class TestPageUnpublish(TestCase, WagtailTestUtils):
self.assertFalse(SimplePage.objects.get(id=self.page.id).live)
# Check that the page_unpublished signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], self.page)
self.assertEqual(signal_page[0], signal_page[0].specific)
self.assertEqual(mock_handler.call_count, 1)
mock_call = mock_handler.mock_calls[0][2]
self.assertEqual(mock_call['sender'], self.page.specific_class)
self.assertEqual(mock_call['instance'], self.page)
self.assertIsInstance(mock_call['instance'], self.page.specific_class)
class TestApproveRejectModeration(TestCase, WagtailTestUtils):
@ -1522,12 +1528,8 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
This posts to the approve moderation view and checks that the page was approved
"""
# Connect a mock signal handler to page_published signal
signal_fired = [False]
signal_page = [None]
def page_published_handler(sender, instance, **kwargs):
signal_fired[0] = True
signal_page[0] = instance
page_published.connect(page_published_handler)
mock_handler = mock.MagicMock()
page_published.connect(mock_handler)
# Post
response = self.client.post(reverse('wagtailadmin_pages_approve_moderation', args=(self.revision.id, )))
@ -1542,9 +1544,12 @@ class TestApproveRejectModeration(TestCase, WagtailTestUtils):
self.assertFalse(page.has_unpublished_changes, "Approving moderation failed to set has_unpublished_changes=False")
# Check that the page_published signal was fired
self.assertTrue(signal_fired[0])
self.assertEqual(signal_page[0], self.page)
self.assertEqual(signal_page[0], signal_page[0].specific)
self.assertEqual(mock_handler.call_count, 1)
mock_call = mock_handler.mock_calls[0][2]
self.assertEqual(mock_call['sender'], self.page.specific_class)
self.assertEqual(mock_call['instance'], self.page)
self.assertIsInstance(mock_call['instance'], self.page.specific_class)
def test_approve_moderation_when_later_revision_exists(self):
self.page.title = "Goodbye world!"

View file

@ -15,17 +15,41 @@ from wagtail.wagtailcore.models import Page
from taggit.forms import TagWidget
class AdminAutoHeightTextInput(WidgetWithScript, widgets.Textarea):
def __init__(self, attrs=None):
# Use more appropriate rows default, given autoheight will alter this anyway
default_attrs = {'rows': '1'}
if attrs:
default_attrs.update(attrs)
super(AdminAutoHeightTextInput, self).__init__(default_attrs)
def render_js_init(self, id_, name, value):
return '$("#{0}").autosize();'.format(id_)
class AdminDateInput(WidgetWithScript, widgets.DateInput):
# Set a default date format to match the one that our JS date picker expects -
# it can still be overridden explicitly, but this way it won't be affected by
# the DATE_INPUT_FORMATS setting
def __init__(self, attrs=None, format='%Y-%m-%d'):
super(AdminDateInput, self).__init__(attrs=attrs, format=format)
def render_js_init(self, id_, name, value):
return 'initDateChooser({0});'.format(json.dumps(id_))
class AdminTimeInput(WidgetWithScript, widgets.TimeInput):
def __init__(self, attrs=None, format='%H:%M'):
super(AdminTimeInput, self).__init__(attrs=attrs, format=format)
def render_js_init(self, id_, name, value):
return 'initTimeChooser({0});'.format(json.dumps(id_))
class AdminDateTimeInput(WidgetWithScript, widgets.DateTimeInput):
def __init__(self, attrs=None, format='%Y-%m-%d %H:%M'):
super(AdminDateTimeInput, self).__init__(attrs=attrs, format=format)
def render_js_init(self, id_, name, value):
return 'initDateTimeChooser({0});'.format(json.dumps(id_))

View file

@ -68,13 +68,18 @@ class CharBlock(FieldBlock):
class TextBlock(FieldBlock):
def __init__(self, required=True, help_text=None, max_length=None, min_length=None, **kwargs):
self.field = forms.CharField(
widget=forms.Textarea(),
required=required, help_text=help_text,
max_length=max_length, min_length=min_length)
def __init__(self, required=True, help_text=None, rows=1, max_length=None, min_length=None, **kwargs):
self.field_options = {'required': required, 'help_text': help_text, 'max_length': max_length, 'min_length': min_length}
self.rows = rows
super(TextBlock, self).__init__(**kwargs)
@cached_property
def field(self):
from wagtail.wagtailadmin.widgets import AdminAutoHeightTextInput
field_kwargs = {'widget': AdminAutoHeightTextInput(attrs={'rows':self.rows})}
field_kwargs.update(self.field_options)
return forms.CharField(**field_kwargs)
def get_searchable_content(self, value):
return [force_text(value)]

View file

@ -6,7 +6,7 @@ from django import forms
from django.core.exceptions import ValidationError
from django.forms.utils import ErrorList
from django.template.loader import render_to_string
from django.utils.encoding import python_2_unicode_compatible
from django.utils.encoding import python_2_unicode_compatible, force_text
from django.utils.html import format_html_join
from django.utils.safestring import mark_safe
@ -192,7 +192,7 @@ class BaseStreamBlock(Block):
def render_basic(self, value):
return format_html_join('\n', '<div class="block-{1}">{0}</div>',
[(child, child.block_type) for child in value]
[(force_text(child), child.block_type) for child in value]
)
def get_searchable_content(self, value):

View file

@ -74,15 +74,13 @@ class BaseStructBlock(Block):
for child_rendering in child_renderings
])
# Can these be rendered with a template?
if self.label:
return format_html('<div class="struct-block"><label>{0}</label> <ul>{1}</ul></div>', self.label, list_items)
else:
return format_html('<div class="struct-block"><ul>{0}</ul></div>', list_items)
def value_from_datadict(self, data, files, prefix):
return dict([
return StructValue(self, [
(name, block.value_from_datadict(data, files, '%s-%s' % (prefix, name)))
for name, block in self.child_blocks.items()
])

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0010_change_page_owner_to_null_on_delete'),
]
operations = [
migrations.AddField(
model_name='page',
name='first_published_at',
field=models.DateTimeField(editable=False, null=True),
preserve_default=True,
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0011_page_first_published_at'),
]
operations = [
migrations.AlterField(
model_name='page',
name='slug',
field=models.SlugField(help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255),
preserve_default=True,
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0012_extend_page_slug_field'),
]
operations = [
migrations.AlterField(
model_name='page',
name='expire_at',
field=models.DateTimeField(help_text='Please add a date-time in the form YYYY-MM-DD hh:mm.', null=True, verbose_name='Expiry date/time', blank=True),
preserve_default=True,
),
migrations.AlterField(
model_name='page',
name='go_live_at',
field=models.DateTimeField(help_text='Please add a date-time in the form YYYY-MM-DD hh:mm.', null=True, verbose_name='Go live date/time', blank=True),
preserve_default=True,
),
]

View file

@ -265,7 +265,7 @@ class PageBase(models.base.ModelBase):
@python_2_unicode_compatible
class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed)):
title = models.CharField(max_length=255, help_text=_("The page title as you'd like it to be seen by the public"))
slug = models.SlugField(help_text=_("The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/"))
slug = models.SlugField(max_length=255, help_text=_("The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/"))
# TODO: enforce uniqueness on slug field per parent (will have to be done at the Django
# level rather than db, since there is no explicit parent relation in the db)
content_type = models.ForeignKey('contenttypes.ContentType', related_name='pages')
@ -278,12 +278,13 @@ class Page(six.with_metaclass(PageBase, MP_Node, ClusterableModel, index.Indexed
show_in_menus = models.BooleanField(default=False, help_text=_("Whether a link to this page will appear in automatically generated menus"))
search_description = models.TextField(blank=True)
go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm:ss."), blank=True, null=True)
expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm:ss."), blank=True, null=True)
go_live_at = models.DateTimeField(verbose_name=_("Go live date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
expire_at = models.DateTimeField(verbose_name=_("Expiry date/time"), help_text=_("Please add a date-time in the form YYYY-MM-DD hh:mm."), blank=True, null=True)
expired = models.BooleanField(default=False, editable=False)
locked = models.BooleanField(default=False, editable=False)
first_published_at = models.DateTimeField(null=True, editable=False)
latest_revision_created_at = models.DateTimeField(null=True, editable=False)
search_fields = (
@ -1099,6 +1100,7 @@ class PageRevision(models.Model):
obj.owner = self.page.owner
obj.locked = self.page.locked
obj.latest_revision_created_at = self.page.latest_revision_created_at
obj.first_published_at = self.page.first_published_at
return obj
@ -1139,6 +1141,11 @@ class PageRevision(models.Model):
# If page goes live clear the approved_go_live_at of all revisions
page.revisions.update(approved_go_live_at=None)
page.expired = False # When a page is published it can't be expired
# Set first_published_at if the page is being published now
if page.live and page.first_published_at is None:
page.first_published_at = timezone.now()
page.save()
self.submitted_for_moderation = False
page.revisions.update(submitted_for_moderation=False)

View file

@ -4,15 +4,6 @@ from django.utils.html import escape
from wagtail.wagtailcore.whitelist import Whitelister
from wagtail.wagtailcore.models import Page
from wagtail.wagtaildocs.models import Document
# FIXME: we don't really want to import wagtailimages within core.
# For that matter, we probably don't want core to be concerned about translating
# HTML for the benefit of the hallo.js editor...
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.formats import get_image_format
from wagtail.wagtailcore import hooks
@ -22,80 +13,6 @@ from wagtail.wagtailcore import hooks
# elsewhere in the database and is liable to change - from real HTML representation
# to DB representation and back again.
class ImageEmbedHandler(object):
"""
ImageEmbedHandler will be invoked whenever we encounter an element in HTML content
with an attribute of data-embedtype="image". The resulting element in the database
representation will be:
<embed embedtype="image" id="42" format="thumb" alt="some custom alt text">
"""
@staticmethod
def get_db_attributes(tag):
"""
Given a tag that we've identified as an image embed (because it has a
data-embedtype="image" attribute), return a dict of the attributes we should
have on the resulting <embed> element.
"""
return {
'id': tag['data-id'],
'format': tag['data-format'],
'alt': tag['data-alt'],
}
@staticmethod
def expand_db_attributes(attrs, for_editor):
"""
Given a dict of attributes from the <embed> tag, return the real HTML
representation.
"""
Image = get_image_model()
try:
image = Image.objects.get(id=attrs['id'])
format = get_image_format(attrs['format'])
if for_editor:
try:
return format.image_to_editor_html(image, attrs['alt'])
except:
return ''
else:
return format.image_to_html(image, attrs['alt'])
except Image.DoesNotExist:
return "<img>"
class MediaEmbedHandler(object):
"""
MediaEmbedHandler will be invoked whenever we encounter an element in HTML content
with an attribute of data-embedtype="media". The resulting element in the database
representation will be:
<embed embedtype="media" url="http://vimeo.com/XXXXX">
"""
@staticmethod
def get_db_attributes(tag):
"""
Given a tag that we've identified as a media embed (because it has a
data-embedtype="media" attribute), return a dict of the attributes we should
have on the resulting <embed> element.
"""
return {
'url': tag['data-url'],
}
@staticmethod
def expand_db_attributes(attrs, for_editor):
"""
Given a dict of attributes from the <embed> tag, return the real HTML
representation.
"""
from wagtail.wagtailembeds import format
if for_editor:
return format.embed_to_editor_html(attrs['url'])
else:
return format.embed_to_frontend_html(attrs['url'])
class PageLinkHandler(object):
"""
PageLinkHandler will be invoked whenever we encounter an <a> element in HTML content
@ -127,35 +44,38 @@ class PageLinkHandler(object):
return "<a>"
class DocumentLinkHandler(object):
@staticmethod
def get_db_attributes(tag):
return {'id': tag['data-id']}
@staticmethod
def expand_db_attributes(attrs, for_editor):
try:
doc = Document.objects.get(id=attrs['id'])
if for_editor:
editor_attrs = 'data-linktype="document" data-id="%d" ' % doc.id
else:
editor_attrs = ''
return '<a %shref="%s">' % (editor_attrs, escape(doc.url))
except Document.DoesNotExist:
return "<a>"
EMBED_HANDLERS = {
'image': ImageEmbedHandler,
'media': MediaEmbedHandler,
}
EMBED_HANDLERS = {}
LINK_HANDLERS = {
'page': PageLinkHandler,
'document': DocumentLinkHandler,
}
has_loaded_embed_handlers = False
has_loaded_link_handlers = False
def get_embed_handler(embed_type):
global EMBED_HANDLERS, has_loaded_embed_handlers
if not has_loaded_embed_handlers:
for hook in hooks.get_hooks('register_rich_text_embed_handler'):
handler_name, handler = hook()
EMBED_HANDLERS[handler_name] = handler
has_loaded_embed_handlers = True
return EMBED_HANDLERS[embed_type]
def get_link_handler(link_type):
global LINK_HANDLERS, has_loaded_link_handlers
if not has_loaded_link_handlers:
for hook in hooks.get_hooks('register_rich_text_link_handler'):
handler_name, handler = hook()
LINK_HANDLERS[handler_name] = handler
has_loaded_link_handlers = True
return LINK_HANDLERS[link_type]
class DbWhitelister(Whitelister):
"""
@ -189,7 +109,7 @@ class DbWhitelister(Whitelister):
if 'data-embedtype' in tag.attrs:
embed_type = tag['data-embedtype']
# fetch the appropriate embed handler for this embedtype
embed_handler = EMBED_HANDLERS[embed_type]
embed_handler = get_embed_handler(embed_type)
embed_attrs = embed_handler.get_db_attributes(tag)
embed_attrs['embedtype'] = embed_type
@ -202,7 +122,7 @@ class DbWhitelister(Whitelister):
cls.clean_node(doc, child)
link_type = tag['data-linktype']
link_handler = LINK_HANDLERS[link_type]
link_handler = get_link_handler(link_type)
link_attrs = link_handler.get_db_attributes(tag)
link_attrs['linktype'] = link_type
tag.attrs.clear()
@ -238,12 +158,12 @@ def expand_db_html(html, for_editor=False):
if 'linktype' not in attrs:
# return unchanged
return m.group(0)
handler = LINK_HANDLERS[attrs['linktype']]
handler = get_link_handler(attrs['linktype'])
return handler.expand_db_attributes(attrs, for_editor)
def replace_embed_tag(m):
attrs = extract_attrs(m.group(1))
handler = EMBED_HANDLERS[attrs['embedtype']]
handler = get_embed_handler(attrs['embedtype'])
return handler.expand_db_attributes(attrs, for_editor)
html = FIND_A_TAG.sub(replace_a_tag, html)

View file

@ -438,6 +438,23 @@ class TestStructBlock(unittest.TestCase):
self.assertEqual(content, ["Wagtail site"])
def test_value_from_datadict(self):
block = blocks.StructBlock([
('title', blocks.CharBlock()),
('link', blocks.URLBlock()),
])
struct_val = block.value_from_datadict({
'mylink-title': "Torchbox",
'mylink-link': "http://www.torchbox.com"
}, {}, 'mylink')
self.assertEqual(struct_val['title'], "Torchbox")
self.assertEqual(struct_val['link'], "http://www.torchbox.com")
self.assertTrue(isinstance(struct_val, blocks.StructValue))
self.assertTrue(isinstance(struct_val.bound_blocks['link'].block, blocks.URLBlock))
class TestListBlock(unittest.TestCase):
def test_initialise_with_class(self):
@ -663,7 +680,7 @@ class TestStreamBlock(unittest.TestCase):
def render_article(self, data):
class ArticleBlock(blocks.StreamBlock):
heading = blocks.CharBlock()
paragraph = blocks.CharBlock()
paragraph = blocks.RichTextBlock()
block = ArticleBlock()
value = block.to_python(data)
@ -678,7 +695,7 @@ class TestStreamBlock(unittest.TestCase):
},
{
'type': 'paragraph',
'value': 'My first paragraph',
'value': 'My <i>first</i> paragraph',
},
{
'type': 'paragraph',
@ -687,8 +704,8 @@ class TestStreamBlock(unittest.TestCase):
])
self.assertIn('<div class="block-heading">My title</div>', html)
self.assertIn('<div class="block-paragraph">My first paragraph</div>', html)
self.assertIn('<div class="block-paragraph">My second paragraph</div>', html)
self.assertIn('<div class="block-paragraph"><div class="rich-text">My <i>first</i> paragraph</div></div>', html)
self.assertIn('<div class="block-paragraph"><div class="rich-text">My second paragraph</div></div>', html)
def test_render_unknown_type(self):
# This can happen if a developer removes a type from their StreamBlock
@ -704,7 +721,7 @@ class TestStreamBlock(unittest.TestCase):
])
self.assertNotIn('foo', html)
self.assertNotIn('Hello', html)
self.assertIn('<div class="block-paragraph">My first paragraph</div>', html)
self.assertIn('<div class="block-paragraph"><div class="rich-text">My first paragraph</div></div>', html)
def render_form(self):
class ArticleBlock(blocks.StreamBlock):

View file

@ -188,6 +188,7 @@ class TestPublishScheduledPagesCommand(TestCase):
p = Page.objects.get(slug='hello-world')
self.assertTrue(p.live)
self.assertTrue(p.first_published_at)
self.assertFalse(p.has_unpublished_changes)
self.assertFalse(PageRevision.objects.filter(page=p).exclude(approved_go_live_at__isnull=True).exists())

View file

@ -463,7 +463,7 @@ class TestCopyPage(TestCase):
# Set the created_at of the revision to a time in the past
revision = christmas_event.get_latest_revision()
revision.created_at = datetime.datetime(2014, 1, 1)
revision.created_at = datetime.datetime(2014, 1, 1, 0, 0, 0, tzinfo=pytz.utc)
revision.save()
# Copy it

View file

@ -3,10 +3,7 @@ from mock import patch
from django.test import TestCase
from wagtail.wagtailcore.rich_text import (
ImageEmbedHandler,
MediaEmbedHandler,
PageLinkHandler,
DocumentLinkHandler,
DbWhitelister,
extract_attrs,
expand_db_html
@ -14,112 +11,6 @@ from wagtail.wagtailcore.rich_text import (
from bs4 import BeautifulSoup
class TestImageEmbedHandler(TestCase):
fixtures = ['wagtail/tests/fixtures/test.json']
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<b data-id="test-id" data-format="test-format" data-alt="test-alt">foo</b>'
)
tag = soup.b
result = ImageEmbedHandler.get_db_attributes(tag)
self.assertEqual(result,
{'alt': 'test-alt',
'id': 'test-id',
'format': 'test-format'})
def test_expand_db_attributes_page_does_not_exist(self):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 0},
False
)
self.assertEqual(result, '<img>')
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_not_for_editor(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'alt': 'test-alt',
'format': 'left'},
False
)
self.assertIn('<img class="richtext-image left"', result)
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_for_editor(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'alt': 'test-alt',
'format': 'left'},
True
)
self.assertIn('<img data-embedtype="image" data-id="1" data-format="left" data-alt="test-alt" class="richtext-image left"', result)
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_for_editor_throws_exception(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'format': 'left'},
True
)
self.assertEqual(result, '')
class TestMediaEmbedHandler(TestCase):
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<b data-url="test-url">foo</b>'
)
tag = soup.b
result = MediaEmbedHandler.get_db_attributes(tag)
self.assertEqual(result,
{'url': 'test-url'})
@patch('wagtail.wagtailembeds.embeds.oembed')
def test_expand_db_attributes_for_editor(self, oembed):
oembed.return_value = {
'title': 'test title',
'author_name': 'test author name',
'provider_name': 'test provider name',
'type': 'test type',
'thumbnail_url': 'test thumbnail url',
'width': 'test width',
'height': 'test height',
'html': 'test html'
}
result = MediaEmbedHandler.expand_db_attributes(
{'url': 'http://www.youtube.com/watch/'},
True
)
self.assertIn('<div class="embed-placeholder" contenteditable="false" data-embedtype="media" data-url="http://www.youtube.com/watch/">', result)
self.assertIn('<h3>test title</h3>', result)
self.assertIn('<p>URL: http://www.youtube.com/watch/</p>', result)
self.assertIn('<p>Provider: test provider name</p>', result)
self.assertIn('<p>Author: test author name</p>', result)
self.assertIn('<img src="test thumbnail url" alt="test title">', result)
@patch('wagtail.wagtailembeds.embeds.oembed')
def test_expand_db_attributes_not_for_editor(self, oembed):
oembed.return_value = {
'title': 'test title',
'author_name': 'test author name',
'provider_name': 'test provider name',
'type': 'test type',
'thumbnail_url': 'test thumbnail url',
'width': 'test width',
'height': 'test height',
'html': 'test html'
}
result = MediaEmbedHandler.expand_db_attributes(
{'url': 'http://www.youtube.com/watch/'},
False
)
self.assertIn('test html', result)
class TestPageLinkHandler(TestCase):
fixtures = ['wagtail/tests/fixtures/test.json']
@ -155,42 +46,6 @@ class TestPageLinkHandler(TestCase):
self.assertEqual(result, '<a href="None">')
class TestDocumentLinkHandler(TestCase):
fixtures = ['wagtail/tests/fixtures/test.json']
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<a data-id="test-id">foo</a>'
)
tag = soup.a
result = DocumentLinkHandler.get_db_attributes(tag)
self.assertEqual(result,
{'id': 'test-id'})
def test_expand_db_attributes_document_does_not_exist(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 0},
False
)
self.assertEqual(result, '<a>')
def test_expand_db_attributes_for_editor(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 1},
True
)
self.assertEqual(result,
'<a data-linktype="document" data-id="1" href="/documents/1/">')
def test_expand_db_attributes_not_for_editor(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 1},
False
)
self.assertEqual(result,
'<a href="/documents/1/">')
class TestDbWhiteLister(TestCase):
def test_clean_tag_node_div(self):
soup = BeautifulSoup(

View file

@ -0,0 +1,23 @@
from django.utils.html import escape
from wagtail.wagtaildocs.models import Document
class DocumentLinkHandler(object):
@staticmethod
def get_db_attributes(tag):
return {'id': tag['data-id']}
@staticmethod
def expand_db_attributes(attrs, for_editor):
try:
doc = Document.objects.get(id=attrs['id'])
if for_editor:
editor_attrs = 'data-linktype="document" data-id="%d" ' % doc.id
else:
editor_attrs = ''
return '<a %shref="%s">' % (editor_attrs, escape(doc.url))
except Document.DoesNotExist:
return "<a>"

View file

@ -1,6 +1,7 @@
from six import b
import unittest
import mock
from bs4 import BeautifulSoup
from django.test import TestCase
from django.contrib.auth import get_user_model
@ -16,6 +17,7 @@ from wagtail.tests.models import EventPage, EventPageRelatedLink
from wagtail.wagtaildocs.models import Document
from wagtail.wagtaildocs import models
from wagtail.wagtaildocs.rich_text import DocumentLinkHandler
class TestDocumentPermissions(TestCase):
@ -576,3 +578,39 @@ class TestServeView(TestCase):
def test_with_incorrect_filename(self):
response = self.client.get(reverse('wagtaildocs_serve', args=(self.document.id, 'incorrectfilename')))
self.assertEqual(response.status_code, 404)
class TestDocumentRichTextLinkHandler(TestCase):
fixtures = ['wagtail/tests/fixtures/test.json']
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<a data-id="test-id">foo</a>'
)
tag = soup.a
result = DocumentLinkHandler.get_db_attributes(tag)
self.assertEqual(result,
{'id': 'test-id'})
def test_expand_db_attributes_document_does_not_exist(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 0},
False
)
self.assertEqual(result, '<a>')
def test_expand_db_attributes_for_editor(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 1},
True
)
self.assertEqual(result,
'<a data-linktype="document" data-id="1" href="/documents/1/">')
def test_expand_db_attributes_not_for_editor(self):
result = DocumentLinkHandler.expand_db_attributes(
{'id': 1},
False
)
self.assertEqual(result,
'<a href="/documents/1/">')

View file

@ -10,6 +10,7 @@ from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtaildocs import admin_urls
from wagtail.wagtaildocs.rich_text import DocumentLinkHandler
@hooks.register('register_admin_urls')
@ -53,3 +54,8 @@ def register_permissions():
document_content_type = ContentType.objects.get(app_label='wagtaildocs', model='document')
document_permissions = Permission.objects.filter(content_type = document_content_type)
return document_permissions
@hooks.register('register_rich_text_link_handler')
def register_document_link_handler():
return ('document', DocumentLinkHandler)

View file

@ -0,0 +1,31 @@
from wagtail.wagtailembeds import format
class MediaEmbedHandler(object):
"""
MediaEmbedHandler will be invoked whenever we encounter an element in HTML content
with an attribute of data-embedtype="media". The resulting element in the database
representation will be:
<embed embedtype="media" url="http://vimeo.com/XXXXX">
"""
@staticmethod
def get_db_attributes(tag):
"""
Given a tag that we've identified as a media embed (because it has a
data-embedtype="media" attribute), return a dict of the attributes we should
have on the resulting <embed> element.
"""
return {
'url': tag['data-url'],
}
@staticmethod
def expand_db_attributes(attrs, for_editor):
"""
Given a dict of attributes from the <embed> tag, return the real HTML
representation.
"""
if for_editor:
return format.embed_to_editor_html(attrs['url'])
else:
return format.embed_to_frontend_html(attrs['url'])

View file

@ -2,8 +2,10 @@ import six.moves.urllib.request
from six.moves.urllib.error import URLError
from mock import patch
import warnings
import unittest
from bs4 import BeautifulSoup
from wagtail.wagtailembeds.rich_text import MediaEmbedHandler
try:
import embedly
@ -320,3 +322,55 @@ class TestEmbedBlock(TestCase):
# Check that the embed was in the returned HTML
self.assertIn('<h1>Hello world!</h1>', html)
class TestMediaEmbedHandler(TestCase):
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<b data-url="test-url">foo</b>'
)
tag = soup.b
result = MediaEmbedHandler.get_db_attributes(tag)
self.assertEqual(result,
{'url': 'test-url'})
@patch('wagtail.wagtailembeds.embeds.oembed')
def test_expand_db_attributes_for_editor(self, oembed):
oembed.return_value = {
'title': 'test title',
'author_name': 'test author name',
'provider_name': 'test provider name',
'type': 'test type',
'thumbnail_url': 'test thumbnail url',
'width': 'test width',
'height': 'test height',
'html': 'test html'
}
result = MediaEmbedHandler.expand_db_attributes(
{'url': 'http://www.youtube.com/watch/'},
True
)
self.assertIn('<div class="embed-placeholder" contenteditable="false" data-embedtype="media" data-url="http://www.youtube.com/watch/">', result)
self.assertIn('<h3>test title</h3>', result)
self.assertIn('<p>URL: http://www.youtube.com/watch/</p>', result)
self.assertIn('<p>Provider: test provider name</p>', result)
self.assertIn('<p>Author: test author name</p>', result)
self.assertIn('<img src="test thumbnail url" alt="test title">', result)
@patch('wagtail.wagtailembeds.embeds.oembed')
def test_expand_db_attributes_not_for_editor(self, oembed):
oembed.return_value = {
'title': 'test title',
'author_name': 'test author name',
'provider_name': 'test provider name',
'type': 'test type',
'thumbnail_url': 'test thumbnail url',
'width': 'test width',
'height': 'test height',
'html': 'test html'
}
result = MediaEmbedHandler.expand_db_attributes(
{'url': 'http://www.youtube.com/watch/'},
False
)
self.assertIn('test html', result)

View file

@ -5,6 +5,7 @@ from django.utils.html import format_html
from wagtail.wagtailcore import hooks
from wagtail.wagtailembeds import urls
from wagtail.wagtailembeds.rich_text import MediaEmbedHandler
@hooks.register('register_admin_urls')
@ -27,3 +28,8 @@ def editor_js():
'wagtailembeds/js/hallo-plugins/hallo-wagtailembeds.js',
urlresolvers.reverse('wagtailembeds_chooser')
)
@hooks.register('register_rich_text_embed_handler')
def register_media_embed_handler():
return ('media', MediaEmbedHandler)

View file

@ -79,29 +79,6 @@ class TestFormSubmission(TestCase):
self.assertIn("baz", submission[0].form_data)
class TestPageModes(TestCase):
fixtures = ['test.json']
def setUp(self):
self.form_page = Page.objects.get(url_path='/home/contact-us/').specific
def test_form(self):
response = self.form_page.serve_preview(self.form_page.dummy_request(), 'form')
# Check response
self.assertContains(response, """<label for="id_your-email">Your email</label>""")
self.assertTemplateUsed(response, 'tests/form_page.html')
self.assertTemplateNotUsed(response, 'tests/form_page_landing.html')
def test_landing(self):
response = self.form_page.serve_preview(self.form_page.dummy_request(), 'landing')
# Check response
self.assertContains(response, "Thank you for your feedback.")
self.assertTemplateNotUsed(response, 'tests/form_page.html')
self.assertTemplateUsed(response, 'tests/form_page_landing.html')
class TestFormBuilder(TestCase):
fixtures = ['test.json']

View file

@ -0,0 +1,45 @@
from wagtail.wagtailimages.models import get_image_model
from wagtail.wagtailimages.formats import get_image_format
class ImageEmbedHandler(object):
"""
ImageEmbedHandler will be invoked whenever we encounter an element in HTML content
with an attribute of data-embedtype="image". The resulting element in the database
representation will be:
<embed embedtype="image" id="42" format="thumb" alt="some custom alt text">
"""
@staticmethod
def get_db_attributes(tag):
"""
Given a tag that we've identified as an image embed (because it has a
data-embedtype="image" attribute), return a dict of the attributes we should
have on the resulting <embed> element.
"""
return {
'id': tag['data-id'],
'format': tag['data-format'],
'alt': tag['data-alt'],
}
@staticmethod
def expand_db_attributes(attrs, for_editor):
"""
Given a dict of attributes from the <embed> tag, return the real HTML
representation.
"""
Image = get_image_model()
try:
image = Image.objects.get(id=attrs['id'])
format = get_image_format(attrs['format'])
if for_editor:
try:
return format.image_to_editor_html(image, attrs['alt'])
except:
return ''
else:
return format.image_to_html(image, attrs['alt'])
except Image.DoesNotExist:
return "<img>"

View file

@ -0,0 +1,60 @@
from django.test import TestCase
from bs4 import BeautifulSoup
from mock import patch
from wagtail.wagtailimages.rich_text import ImageEmbedHandler
class TestImageEmbedHandler(TestCase):
fixtures = ['wagtail/tests/fixtures/test.json']
def test_get_db_attributes(self):
soup = BeautifulSoup(
'<b data-id="test-id" data-format="test-format" data-alt="test-alt">foo</b>'
)
tag = soup.b
result = ImageEmbedHandler.get_db_attributes(tag)
self.assertEqual(result,
{'alt': 'test-alt',
'id': 'test-id',
'format': 'test-format'})
def test_expand_db_attributes_page_does_not_exist(self):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 0},
False
)
self.assertEqual(result, '<img>')
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_not_for_editor(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'alt': 'test-alt',
'format': 'left'},
False
)
self.assertIn('<img class="richtext-image left"', result)
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_for_editor(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'alt': 'test-alt',
'format': 'left'},
True
)
self.assertIn('<img data-embedtype="image" data-id="1" data-format="left" data-alt="test-alt" class="richtext-image left"', result)
@patch('wagtail.wagtailimages.models.Image')
@patch('django.core.files.File')
def test_expand_db_attributes_for_editor_throws_exception(self, mock_file, mock_image):
result = ImageEmbedHandler.expand_db_attributes(
{'id': 1,
'format': 'left'},
True
)
self.assertEqual(result, '')

View file

@ -1,6 +1,7 @@
from wsgiref.util import FileWrapper
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.core.servers.basehttp import FileWrapper
from django.core.exceptions import PermissionDenied
from wagtail.wagtailimages.models import get_image_model, Filter

View file

@ -11,6 +11,7 @@ from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.menu import MenuItem
from wagtail.wagtailimages import admin_urls, image_operations
from wagtail.wagtailimages.rich_text import ImageEmbedHandler
@hooks.register('register_admin_urls')
@ -108,3 +109,8 @@ def register_image_operations():
('width', image_operations.WidthHeightOperation),
('height', image_operations.WidthHeightOperation),
]
@hooks.register('register_rich_text_embed_handler')
def register_image_embed_handler():
return ('image', ImageEmbedHandler)

View file

@ -3,6 +3,7 @@ from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.forms.models import inlineformset_factory
from wagtail.wagtailcore import hooks
from wagtail.wagtailadmin.widgets import AdminPageChooser
@ -213,6 +214,15 @@ class BaseGroupPagePermissionFormSet(forms.models.BaseInlineFormSet):
return empty_form
GroupPagePermissionFormSet = inlineformset_factory(
Group,
GroupPagePermission,
formset=BaseGroupPagePermissionFormSet,
extra=0,
fields=('page', 'permission_type'),
)
class NotificationPreferencesForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(NotificationPreferencesForm, self).__init__(*args, **kwargs)

View file

@ -5,12 +5,10 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.views.decorators.vary import vary_on_headers
from django.forms.models import inlineformset_factory
from wagtail.wagtailadmin import messages
from wagtail.wagtailadmin.forms import SearchForm
from wagtail.wagtailusers.forms import GroupForm, BaseGroupPagePermissionFormSet
from wagtail.wagtailcore.models import GroupPagePermission
from wagtail.wagtailusers.forms import GroupForm, GroupPagePermissionFormSet
def user_has_group_model_perm(user):
@ -79,12 +77,6 @@ def index(request):
@permission_required('auth.add_group')
def create(request):
GroupPagePermissionFormSet = inlineformset_factory(
Group,
GroupPagePermission,
formset=BaseGroupPagePermissionFormSet,
extra=0
)
if request.POST:
form = GroupForm(request.POST)
formset = GroupPagePermissionFormSet(request.POST)
@ -111,12 +103,6 @@ def create(request):
@permission_required('auth.change_group')
def edit(request, group_id):
group = get_object_or_404(Group, id=group_id)
GroupPagePermissionFormSet = inlineformset_factory(
Group,
GroupPagePermission,
formset=BaseGroupPagePermissionFormSet,
extra=0
)
if request.POST:
form = GroupForm(request.POST, instance=group)
formset = GroupPagePermissionFormSet(request.POST, instance=group)