Skip to content

Commit

Permalink
Add live preview capabilities (#6)
Browse files Browse the repository at this point in the history
- Add live preview capabilities
- Better error handling
- Garbage collect before creating new preview
- Fix live preview logic
- Update README
  • Loading branch information
zerolab authored Aug 20, 2019
1 parent 544c5f6 commit f9c47cc
Show file tree
Hide file tree
Showing 24 changed files with 616 additions and 15 deletions.
11 changes: 8 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ venv/
.idea

# Python packaging
/build/
/dist/
wagtail_headless_preview.egg-info
.Python
build/
dist/
*.egg-info/
*.egg


.tox/
43 changes: 43 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
language: python
cache: pip

# Use container-based infrastructure
dist: xenial
sudo: false

matrix:
include:
- env: TOXENV=py37-django22-wagtail25
python: 3.7
- env: TOXENV=py36-django21-wagtail24
python: 3.6
- env: TOXENV=py36-django21-wagtail23
python: 3.6
- env: TOXENV=py35-django20-wagtail24
python: 3.5
- env: TOXENV=py35-django20-wagtail23
python: 3.5
- env: TOXENV=py35-django20-wagtail22
python: 3.5
- env: TOXENV=py35-django20-wagtail21
python: 3.5
- env: TOXENV=py35-django20-wagtail20
python: 3.5

allow_failures:
- env: TOXENV=py37-djangomaster-wagtail25

install:
- pip install wheel flake8 isort
- pip install -e .[testing]

before_script:
- TESTDIR=$(pwd)

script:
- flake8 wagtail_headless_preview
- isort --check-only --diff --recursive wagtail_headless_preview
- cd wagtail_headless_preview/tests/client
- nohup python3 -m http.server 8020 > /dev/null 2>&1 &
- cd $TESTDIR
- tox
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
include LICENSE *.rst *.txt *.md

recursive-include wagtail_headless_preview/templates *
recursive-include wagtail_headless_preview/templates wagtail_headless_preview/static *

global-exclude __pycache__
global-exclude *.py[co]
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ INSTALLED_APPS = [
]
```

Run migrations:

```sh
$ ./manage.py migrate
```

then configure the preview client URL using the `HEADLESS_PREVIEW_CLIENT_URLS` setting.

For single site, the configuration should look like:
Expand All @@ -44,12 +50,14 @@ HEADLESS_PREVIEW_CLIENT_URLS = {
}
```

Run migrations:
Optionally, you can enable live preview functionality with the `HEADLESS_PREVIEW_LIVE` setting:

```sh
$ ./manage.py migrate
```python
# settings.py
HEADLESS_PREVIEW_LIVE = True
```

Note: Your front-end app must be set up for live preview, a feature that usually requires [Django Channels](https://github.com/django/channels/) or other WebSocket/async libraries.

## Usage

Expand Down Expand Up @@ -97,6 +105,7 @@ from rest_framework.response import Response
# Create the router. "wagtailapi" is the URL namespace
api_router = WagtailAPIRouter('wagtailapi')


class PagePreviewAPIEndpoint(PagesAPIEndpoint):
known_query_parameters = PagesAPIEndpoint.known_query_parameters.union(['content_type', 'token'])

Expand Down
27 changes: 27 additions & 0 deletions runtests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env python

import os
import sys
import warnings

from django.core.management import execute_from_command_line

os.environ['DJANGO_SETTINGS_MODULE'] = 'wagtail_headless_preview.tests.settings'


def runtests():
# Don't ignore DeprecationWarnings
only_wagtail_headless_preview = r'^wagtail_headless_preview(\.|$)'
warnings.filterwarnings('default', category=DeprecationWarning, module=only_wagtail_headless_preview)
warnings.filterwarnings('default', category=PendingDeprecationWarning, module=only_wagtail_headless_preview)

args = sys.argv[1:]
argv = sys.argv[:1] + ['test'] + args
try:
execute_from_command_line(argv)
finally:
pass


if __name__ == '__main__':
runtests()
18 changes: 18 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[bdist_wheel]
universal = 1

[metadata]
description-file = README.md

[flake8]
max-line-length=120
exclude=migrations

[isort]
known_first_party = wagtail_headless_preview
known_django = django
known_wagtail = wagtail
skip = migrations
sections = FUTURE, STDLIB, DJANGO, THIRDPARTY, FIRSTPARTY, LOCALFOLDER
default_section = THIRDPARTY
multi_line_output = 5
12 changes: 11 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,17 @@
author="Matthew Westcott - POC, Karl Hobley",
author_email="matthew.westcott@torchbox.com",
license="BSD",
install_requires=["wagtail>=2.0"],
install_requires=[
"wagtail>=2.0"
],

extras_require={
'testing': [
'tox',
'django-cors-headers'
],
},

classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
Expand Down
35 changes: 35 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[tox]
skipsdist = True

envlist =
py{35,36}-django{20,21}-wagtail{20,21,22,23,24}
py37-django{22,master}-wagtail25

[testenv]
install_command = pip install -e ".[testing]" -U {opts} {packages}
commands =
python runtests.py

basepython =
py35: python3.5
py36: python3.6
py37: python3.7

deps =
django200: django>=2.0,<2.1
django21: Django>=2.1,<2.2
django22: Django>=2.2,<2.3
djangomaster: git+https://github.com/django/django.git@master#egg=Django
wagtail20: wagtail>=2.0,<2.1
wagtail21: wagtail>=2.1,<2.2
wagtail22: wagtail>=2.2,<2.3
wagtail23: wagtail>=2.3,<2.4
wagtail24: wagtail>=2.4,<2.5
wagtail25: wagtail>=2.5,<2.6

[testenv:flake8]
deps=flake8>3.7
commands=flake8 wagtail_headless_preview

[flake8]
ignore = D100,D101,D102,D103,D105,D200,D202,D204,D205,D209,D400,D401,E303,E501,W503,N805,N806
45 changes: 38 additions & 7 deletions wagtail_headless_preview/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from django.contrib.contenttypes.models import ContentType
from django.core.signing import TimestampSigner
from django.db import models
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.shortcuts import render


class PagePreview(models.Model):
Expand Down Expand Up @@ -53,10 +52,19 @@ def create_page_preview(self):
content_json=self.to_json(),
)

def update_page_preview(self, token):
return PagePreview.objects.update_or_create(
token=token,
defaults={
"content_type": self.content_type,
"content_json": self.to_json(),
},
)

def get_client_root_url(self):
try:
return settings.HEADLESS_PREVIEW_CLIENT_URLS[self.get_site().hostname]
except KeyError:
except (AttributeError, KeyError):
return settings.HEADLESS_PREVIEW_CLIENT_URLS["default"]

@classmethod
Expand All @@ -72,17 +80,40 @@ def get_preview_url(self, token):
)
)

def dummy_request(self, original_request=None, **meta):
request = super(HeadlessPreviewMixin, self).dummy_request(
original_request=original_request, **meta
)
request.GET = request.GET.copy()
request.GET["live_preview"] = original_request.GET.get("live_preview")
return request

def serve_preview(self, request, mode_name):
page_preview = self.create_page_preview()
page_preview.save()
PagePreview.garbage_collect()
use_live_preview = request.GET.get("live_preview")
token = request.COOKIES.get("used-token")
if use_live_preview and token:
page_preview, existed = self.update_page_preview(token)
PagePreview.garbage_collect()

from wagtail_headless_preview.signals import preview_update # Imported locally as live preview is optional
preview_update.send(sender=HeadlessPreviewMixin, token=token)
else:
PagePreview.garbage_collect()
page_preview = self.create_page_preview()
page_preview.save()

return render(
response = render(
request,
"wagtail_headless_preview/preview.html",
{"preview_url": self.get_preview_url(page_preview.token)},
)

if use_live_preview:
# Set cookie that auto-expires after 5mins
response.set_cookie(key="used-token", value=page_preview.token, max_age=300)

return response

@classmethod
def get_page_from_preview_token(cls, token):
content_type = ContentType.objects.get_for_model(cls)
Expand Down
3 changes: 3 additions & 0 deletions wagtail_headless_preview/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from django.dispatch import Signal

preview_update = Signal(providing_args=["token"])
43 changes: 43 additions & 0 deletions wagtail_headless_preview/static/js/live-preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
$(document).ready(() => {
let $previewButton = $('.action-preview');
// Make existing Wagtail code send form data to backend on KeyUp
$previewButton.attr('data-auto-update', "true");

// Trigger preview save on key up
let $form = $('#page-edit-form');
let previewUrl = $previewButton.data('action');
let triggerPreviewDataTimeout = -1;
let autoUpdatePreviewDataTimeout = -1;

const triggerPreviewUpdate = () => {
return $.ajax({
url: `${previewUrl}?live_preview=true`,
method: 'GET',
data: new FormData($form[0]),
processData: false,
contentType: false
})
};

const setPreviewData = () => {
return $.ajax({
url: previewUrl,
method: 'POST',
data: new FormData($form[0]),
processData: false,
contentType: false
});
};

$previewButton.one('click', function () {
if ($previewButton.data('auto-update')) {
$form.on('click change keyup DOMSubtreeModified', function () {
clearTimeout(triggerPreviewDataTimeout);
triggerPreviewDataTimeout = setTimeout(triggerPreviewUpdate, 500);

clearTimeout(autoUpdatePreviewDataTimeout);
autoUpdatePreviewDataTimeout = setTimeout(setPreviewData, 300);
}).trigger('change');
}
})
});
Empty file.
23 changes: 23 additions & 0 deletions wagtail_headless_preview/tests/client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Headless preview</title>
<script>
function go() {
var querystring = window.location.search.replace(/^\?/, '');
var params = {};
querystring.replace(/([^=&]+)=([^&]*)/g, function(m, key, value) {
params[decodeURIComponent(key)] = decodeURIComponent(value);
});

var apiUrl = 'http://localhost:8000/api/v2/page_preview/1/?content_type=' + encodeURIComponent(params['content_type']) + '&token=' + encodeURIComponent(params['token']) + '&format=json';
fetch(apiUrl).then(function(response) {
response.text().then(function(text) {
document.body.innerText = text;
});
});
}
</script>
</head>
<body onload="go()"></body>
</html>
Loading

0 comments on commit f9c47cc

Please sign in to comment.