mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-12 01:03:11 +00:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
91c7947791
48 changed files with 680 additions and 489 deletions
11
.drone.yml
11
.drone.yml
|
|
@ -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
2
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
*.swp
|
||||
*.pyc
|
||||
.DS_Store
|
||||
/.coverage
|
||||
|
|
@ -6,3 +7,4 @@
|
|||
/wagtail.egg-info/
|
||||
/docs/_build/
|
||||
/.tox/
|
||||
/venv
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ Release notes
|
|||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
roadmap
|
||||
0.9
|
||||
0.8.6
|
||||
0.8.5
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
4
setup.py
4
setup.py
|
|
@ -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",
|
||||
|
|
|
|||
2
tox.ini
2
tox.ini
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
|
|
@ -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_))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
@ -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,
|
||||
),
|
||||
]
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
23
wagtail/wagtaildocs/rich_text.py
Normal file
23
wagtail/wagtaildocs/rich_text.py
Normal 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>"
|
||||
|
|
@ -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/">')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
31
wagtail/wagtailembeds/rich_text.py
Normal file
31
wagtail/wagtailembeds/rich_text.py
Normal 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'])
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
||||
|
|
|
|||
45
wagtail/wagtailimages/rich_text.py
Normal file
45
wagtail/wagtailimages/rich_text.py
Normal 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>"
|
||||
60
wagtail/wagtailimages/tests/test_rich_text.py
Normal file
60
wagtail/wagtailimages/tests/test_rich_text.py
Normal 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, '')
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue