Skip to content

Commit f9c47cc

Browse files
authored
Add live preview capabilities (#6)
- Add live preview capabilities - Better error handling - Garbage collect before creating new preview - Fix live preview logic - Update README
1 parent 544c5f6 commit f9c47cc

24 files changed

+616
-15
lines changed

.gitignore

+8-3
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ venv/
1010
.idea
1111

1212
# Python packaging
13-
/build/
14-
/dist/
15-
wagtail_headless_preview.egg-info
13+
.Python
14+
build/
15+
dist/
16+
*.egg-info/
17+
*.egg
18+
19+
20+
.tox/

.travis.yml

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
language: python
2+
cache: pip
3+
4+
# Use container-based infrastructure
5+
dist: xenial
6+
sudo: false
7+
8+
matrix:
9+
include:
10+
- env: TOXENV=py37-django22-wagtail25
11+
python: 3.7
12+
- env: TOXENV=py36-django21-wagtail24
13+
python: 3.6
14+
- env: TOXENV=py36-django21-wagtail23
15+
python: 3.6
16+
- env: TOXENV=py35-django20-wagtail24
17+
python: 3.5
18+
- env: TOXENV=py35-django20-wagtail23
19+
python: 3.5
20+
- env: TOXENV=py35-django20-wagtail22
21+
python: 3.5
22+
- env: TOXENV=py35-django20-wagtail21
23+
python: 3.5
24+
- env: TOXENV=py35-django20-wagtail20
25+
python: 3.5
26+
27+
allow_failures:
28+
- env: TOXENV=py37-djangomaster-wagtail25
29+
30+
install:
31+
- pip install wheel flake8 isort
32+
- pip install -e .[testing]
33+
34+
before_script:
35+
- TESTDIR=$(pwd)
36+
37+
script:
38+
- flake8 wagtail_headless_preview
39+
- isort --check-only --diff --recursive wagtail_headless_preview
40+
- cd wagtail_headless_preview/tests/client
41+
- nohup python3 -m http.server 8020 > /dev/null 2>&1 &
42+
- cd $TESTDIR
43+
- tox

MANIFEST.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
include LICENSE *.rst *.txt *.md
22

3-
recursive-include wagtail_headless_preview/templates *
3+
recursive-include wagtail_headless_preview/templates wagtail_headless_preview/static *
44

55
global-exclude __pycache__
66
global-exclude *.py[co]

README.md

+12-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ INSTALLED_APPS = [
2424
]
2525
```
2626

27+
Run migrations:
28+
29+
```sh
30+
$ ./manage.py migrate
31+
```
32+
2733
then configure the preview client URL using the `HEADLESS_PREVIEW_CLIENT_URLS` setting.
2834

2935
For single site, the configuration should look like:
@@ -44,12 +50,14 @@ HEADLESS_PREVIEW_CLIENT_URLS = {
4450
}
4551
```
4652

47-
Run migrations:
53+
Optionally, you can enable live preview functionality with the `HEADLESS_PREVIEW_LIVE` setting:
4854

49-
```sh
50-
$ ./manage.py migrate
55+
```python
56+
# settings.py
57+
HEADLESS_PREVIEW_LIVE = True
5158
```
5259

60+
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.
5361

5462
## Usage
5563

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

108+
100109
class PagePreviewAPIEndpoint(PagesAPIEndpoint):
101110
known_query_parameters = PagesAPIEndpoint.known_query_parameters.union(['content_type', 'token'])
102111

runtests.py

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env python
2+
3+
import os
4+
import sys
5+
import warnings
6+
7+
from django.core.management import execute_from_command_line
8+
9+
os.environ['DJANGO_SETTINGS_MODULE'] = 'wagtail_headless_preview.tests.settings'
10+
11+
12+
def runtests():
13+
# Don't ignore DeprecationWarnings
14+
only_wagtail_headless_preview = r'^wagtail_headless_preview(\.|$)'
15+
warnings.filterwarnings('default', category=DeprecationWarning, module=only_wagtail_headless_preview)
16+
warnings.filterwarnings('default', category=PendingDeprecationWarning, module=only_wagtail_headless_preview)
17+
18+
args = sys.argv[1:]
19+
argv = sys.argv[:1] + ['test'] + args
20+
try:
21+
execute_from_command_line(argv)
22+
finally:
23+
pass
24+
25+
26+
if __name__ == '__main__':
27+
runtests()

setup.cfg

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[bdist_wheel]
2+
universal = 1
3+
4+
[metadata]
5+
description-file = README.md
6+
7+
[flake8]
8+
max-line-length=120
9+
exclude=migrations
10+
11+
[isort]
12+
known_first_party = wagtail_headless_preview
13+
known_django = django
14+
known_wagtail = wagtail
15+
skip = migrations
16+
sections = FUTURE, STDLIB, DJANGO, THIRDPARTY, FIRSTPARTY, LOCALFOLDER
17+
default_section = THIRDPARTY
18+
multi_line_output = 5

setup.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,17 @@
2222
author="Matthew Westcott - POC, Karl Hobley",
2323
author_email="[email protected]",
2424
license="BSD",
25-
install_requires=["wagtail>=2.0"],
25+
install_requires=[
26+
"wagtail>=2.0"
27+
],
28+
29+
extras_require={
30+
'testing': [
31+
'tox',
32+
'django-cors-headers'
33+
],
34+
},
35+
2636
classifiers=[
2737
"Development Status :: 4 - Beta",
2838
"Environment :: Web Environment",

tox.ini

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[tox]
2+
skipsdist = True
3+
4+
envlist =
5+
py{35,36}-django{20,21}-wagtail{20,21,22,23,24}
6+
py37-django{22,master}-wagtail25
7+
8+
[testenv]
9+
install_command = pip install -e ".[testing]" -U {opts} {packages}
10+
commands =
11+
python runtests.py
12+
13+
basepython =
14+
py35: python3.5
15+
py36: python3.6
16+
py37: python3.7
17+
18+
deps =
19+
django200: django>=2.0,<2.1
20+
django21: Django>=2.1,<2.2
21+
django22: Django>=2.2,<2.3
22+
djangomaster: git+https://github.com/django/django.git@master#egg=Django
23+
wagtail20: wagtail>=2.0,<2.1
24+
wagtail21: wagtail>=2.1,<2.2
25+
wagtail22: wagtail>=2.2,<2.3
26+
wagtail23: wagtail>=2.3,<2.4
27+
wagtail24: wagtail>=2.4,<2.5
28+
wagtail25: wagtail>=2.5,<2.6
29+
30+
[testenv:flake8]
31+
deps=flake8>3.7
32+
commands=flake8 wagtail_headless_preview
33+
34+
[flake8]
35+
ignore = D100,D101,D102,D103,D105,D200,D202,D204,D205,D209,D400,D401,E303,E501,W503,N805,N806

wagtail_headless_preview/models.py

+38-7
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
from django.contrib.contenttypes.models import ContentType
77
from django.core.signing import TimestampSigner
88
from django.db import models
9-
from django.http import HttpResponseRedirect
10-
from django.shortcuts import get_object_or_404, render
9+
from django.shortcuts import render
1110

1211

1312
class PagePreview(models.Model):
@@ -53,10 +52,19 @@ def create_page_preview(self):
5352
content_json=self.to_json(),
5453
)
5554

55+
def update_page_preview(self, token):
56+
return PagePreview.objects.update_or_create(
57+
token=token,
58+
defaults={
59+
"content_type": self.content_type,
60+
"content_json": self.to_json(),
61+
},
62+
)
63+
5664
def get_client_root_url(self):
5765
try:
5866
return settings.HEADLESS_PREVIEW_CLIENT_URLS[self.get_site().hostname]
59-
except KeyError:
67+
except (AttributeError, KeyError):
6068
return settings.HEADLESS_PREVIEW_CLIENT_URLS["default"]
6169

6270
@classmethod
@@ -72,17 +80,40 @@ def get_preview_url(self, token):
7280
)
7381
)
7482

83+
def dummy_request(self, original_request=None, **meta):
84+
request = super(HeadlessPreviewMixin, self).dummy_request(
85+
original_request=original_request, **meta
86+
)
87+
request.GET = request.GET.copy()
88+
request.GET["live_preview"] = original_request.GET.get("live_preview")
89+
return request
90+
7591
def serve_preview(self, request, mode_name):
76-
page_preview = self.create_page_preview()
77-
page_preview.save()
78-
PagePreview.garbage_collect()
92+
use_live_preview = request.GET.get("live_preview")
93+
token = request.COOKIES.get("used-token")
94+
if use_live_preview and token:
95+
page_preview, existed = self.update_page_preview(token)
96+
PagePreview.garbage_collect()
97+
98+
from wagtail_headless_preview.signals import preview_update # Imported locally as live preview is optional
99+
preview_update.send(sender=HeadlessPreviewMixin, token=token)
100+
else:
101+
PagePreview.garbage_collect()
102+
page_preview = self.create_page_preview()
103+
page_preview.save()
79104

80-
return render(
105+
response = render(
81106
request,
82107
"wagtail_headless_preview/preview.html",
83108
{"preview_url": self.get_preview_url(page_preview.token)},
84109
)
85110

111+
if use_live_preview:
112+
# Set cookie that auto-expires after 5mins
113+
response.set_cookie(key="used-token", value=page_preview.token, max_age=300)
114+
115+
return response
116+
86117
@classmethod
87118
def get_page_from_preview_token(cls, token):
88119
content_type = ContentType.objects.get_for_model(cls)

wagtail_headless_preview/signals.py

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.dispatch import Signal
2+
3+
preview_update = Signal(providing_args=["token"])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
$(document).ready(() => {
2+
let $previewButton = $('.action-preview');
3+
// Make existing Wagtail code send form data to backend on KeyUp
4+
$previewButton.attr('data-auto-update', "true");
5+
6+
// Trigger preview save on key up
7+
let $form = $('#page-edit-form');
8+
let previewUrl = $previewButton.data('action');
9+
let triggerPreviewDataTimeout = -1;
10+
let autoUpdatePreviewDataTimeout = -1;
11+
12+
const triggerPreviewUpdate = () => {
13+
return $.ajax({
14+
url: `${previewUrl}?live_preview=true`,
15+
method: 'GET',
16+
data: new FormData($form[0]),
17+
processData: false,
18+
contentType: false
19+
})
20+
};
21+
22+
const setPreviewData = () => {
23+
return $.ajax({
24+
url: previewUrl,
25+
method: 'POST',
26+
data: new FormData($form[0]),
27+
processData: false,
28+
contentType: false
29+
});
30+
};
31+
32+
$previewButton.one('click', function () {
33+
if ($previewButton.data('auto-update')) {
34+
$form.on('click change keyup DOMSubtreeModified', function () {
35+
clearTimeout(triggerPreviewDataTimeout);
36+
triggerPreviewDataTimeout = setTimeout(triggerPreviewUpdate, 500);
37+
38+
clearTimeout(autoUpdatePreviewDataTimeout);
39+
autoUpdatePreviewDataTimeout = setTimeout(setPreviewData, 300);
40+
}).trigger('change');
41+
}
42+
})
43+
});

wagtail_headless_preview/tests/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Headless preview</title>
5+
<script>
6+
function go() {
7+
var querystring = window.location.search.replace(/^\?/, '');
8+
var params = {};
9+
querystring.replace(/([^=&]+)=([^&]*)/g, function(m, key, value) {
10+
params[decodeURIComponent(key)] = decodeURIComponent(value);
11+
});
12+
13+
var apiUrl = 'http://localhost:8000/api/v2/page_preview/1/?content_type=' + encodeURIComponent(params['content_type']) + '&token=' + encodeURIComponent(params['token']) + '&format=json';
14+
fetch(apiUrl).then(function(response) {
15+
response.text().then(function(text) {
16+
document.body.innerText = text;
17+
});
18+
});
19+
}
20+
</script>
21+
</head>
22+
<body onload="go()"></body>
23+
</html>

0 commit comments

Comments
 (0)