Compare commits
258 Commits
feature/de
...
0.9.0
Author | SHA1 | Date | |
---|---|---|---|
48935218db | |||
4c315f067a | |||
6e56d8cf4d | |||
281086a159 | |||
2e74741033 | |||
537dfb12a6 | |||
1e885adf83 | |||
12853c61e1 | |||
d4ee67b778 | |||
c6e4d9cca8 | |||
38aff16044 | |||
aafc8c4ed5 | |||
f21c423b1c | |||
7e24485aaf | |||
12ae3fa173 | |||
961a58505a | |||
cb03a36ba2 | |||
9605773a74 | |||
46d86d852d | |||
0776d2300a | |||
38620d916f | |||
3ee0645267 | |||
eda00d624e | |||
0e24ae17ac | |||
39c31dc81a | |||
702fa233a9 | |||
7405c34252 | |||
6f96f2f172 | |||
559d3c5356 | |||
5aa754dd80 | |||
282baa4787 | |||
35c22cb6af | |||
6c5ab9c6ae | |||
d92fe13d37 | |||
dfb364b7fc | |||
15a0276041 | |||
c0c3ce19fe | |||
e0fffb70b7 | |||
7f2882ce0e | |||
a629bfc862 | |||
e3ceecfa7d | |||
0f79cf1d15 | |||
29001fac8e | |||
dda0bc720e | |||
5beef1b27c | |||
8465e6dcbb | |||
cf8101156c | |||
7076973fc8 | |||
c2735807b4 | |||
2651eb0e3c | |||
18838b2e8c | |||
763a67e2d4 | |||
d35a7fee57 | |||
c100dc603c | |||
d4421eebcb | |||
fea3bc8b8e | |||
38a18f80a4 | |||
85613db363 | |||
5fbfb82480 | |||
f88722c827 | |||
46ad32236c | |||
e6fac5f7fb | |||
82f2dd460e | |||
4f2dc3a304 | |||
6587d0fd56 | |||
4e221b6666 | |||
99d2e4a347 | |||
4deaaa985f | |||
63d5de9803 | |||
18eea8a9b1 | |||
a4cf8120b4 | |||
09fbb5d795 | |||
d79765efee | |||
0aa07261ad | |||
361f0b1700 | |||
5eefa21699 | |||
c5579fa8d4 | |||
7bb523d962 | |||
03073eb004 | |||
e107d73716 | |||
cbc2ec7270 | |||
2450bd45ac | |||
1b73119766 | |||
ebef7f8785 | |||
623af1a06a | |||
f693e62bbf | |||
4e61ff0d08 | |||
49062d36b4 | |||
66ed40f8ec | |||
59b6e7f31e | |||
f2aa8879a9 | |||
decfc88efe | |||
fc442171e4 | |||
5076dd60bd | |||
194daba67b | |||
9705947d3f | |||
31f8a329f2 | |||
3d920d8ed8 | |||
73ea5157ff | |||
e24cb9aee3 | |||
fb95439d83 | |||
dcb8867dc7 | |||
de2c7f9988 | |||
d3d4e7ec92 | |||
23537ac29b | |||
a78290281b | |||
20011f079d | |||
9790a44fd1 | |||
7436384471 | |||
7034c09d4a | |||
45f2de62ea | |||
cc38634519 | |||
4158bafe58 | |||
b55bdb60b9 | |||
531ae6df98 | |||
699d24bc44 | |||
2977440ee7 | |||
23a9b1df84 | |||
eb837fa7b2 | |||
b523327c8c | |||
a2957b7e77 | |||
9623e67dd7 | |||
7531eb9451 | |||
4503ad387e | |||
5d1abee76c | |||
e2d3b9bf9d | |||
dbb0ecde95 | |||
325c2d5801 | |||
77005097c3 | |||
c1f50a5add | |||
9edb0f736a | |||
39b26be325 | |||
92f898462c | |||
33cf0217da | |||
83a7db5952 | |||
a73e356ffe | |||
3eac2cd4dd | |||
55da67523f | |||
9a7d41284e | |||
ebde527ae9 | |||
5f1c52c93c | |||
974a4d6f46 | |||
97e4116945 | |||
02e18491bb | |||
5156887a9d | |||
90c2289396 | |||
fda9017c38 | |||
2cff8a01fe | |||
7fda6f411a | |||
32748b0d6f | |||
479aec516e | |||
d773d0e8f8 | |||
3391087944 | |||
111e6e1568 | |||
a34386f811 | |||
6c4178cf21 | |||
7e240d50b1 | |||
0a4e8af6ad | |||
9dce9578f0 | |||
33eabf4b77 | |||
4f114689a5 | |||
4afb643d5e | |||
24af956913 | |||
4d4445641e | |||
0ab31bb154 | |||
885b378b63 | |||
74cbec77c9 | |||
c2d0812980 | |||
a7265647ef | |||
9584da5d19 | |||
b7b88b214f | |||
d2bb377110 | |||
6d46c30270 | |||
952b88aba7 | |||
bffd13dd3e | |||
e451f792e3 | |||
47123ce723 | |||
e5fa590f8e | |||
b62fabd47b | |||
9cc44a2931 | |||
c70ecbc408 | |||
ec7b00c318 | |||
dd1dafd450 | |||
ecfae3ec19 | |||
f41fa062be | |||
82d11d57aa | |||
6640bf8d74 | |||
da56a521a9 | |||
3a00f9c2d7 | |||
b761412aa8 | |||
e21102dab0 | |||
51084d3d72 | |||
f14b941756 | |||
fe3fddab51 | |||
d6b4f45998 | |||
5a978ed73a | |||
bcd4ebd31f | |||
7f9f11b86e | |||
7b01913d32 | |||
d468c68970 | |||
6e566344df | |||
1ef4999b70 | |||
d1528f1ed4 | |||
b5d0a657ed | |||
f780e325c4 | |||
762843ce49 | |||
7ff7e51fdd | |||
a178c88d63 | |||
7a6f7d10e6 | |||
6671c9db45 | |||
d69bec8f22 | |||
546fa9d513 | |||
4d27306a05 | |||
88fda12af1 | |||
fe393fccb8 | |||
64ec6218fc | |||
824db4dc7c | |||
154442a303 | |||
cdb0093d09 | |||
e07fdde739 | |||
472635e63e | |||
8e754fef07 | |||
08f88181f4 | |||
456a84d120 | |||
040e181b7b | |||
b49d64f82f | |||
a57e177a94 | |||
991ae15fce | |||
e4f16302f4 | |||
cde821a30a | |||
ed3a9449fd | |||
1c4062eb7e | |||
597c0a50f0 | |||
a7b477d71f | |||
9b90551e3b | |||
b7ca6541f4 | |||
cf946f2bee | |||
3e6302ca03 | |||
00b337da1e | |||
9a0d73fc12 | |||
f01a4b439c | |||
f0d260af7f | |||
bcc56877dc | |||
a2e39154f1 | |||
5f8c768894 | |||
b86259a0dc | |||
6b779f29b0 | |||
8d257867b8 | |||
c058ab18d7 | |||
94b54bfcf7 | |||
6ecc15c1dd | |||
24847c1828 | |||
6e32a2e6a3 | |||
4c0f0760b2 | |||
a287f4ff04 | |||
4484931d4b | |||
3fccb0a872 | |||
1c9e993c21 |
@ -11,6 +11,8 @@ line_length = 79
|
|||||||
multi_line_output = 4
|
multi_line_output = 4
|
||||||
balanced_wrapping = true
|
balanced_wrapping = true
|
||||||
use_parentheses = true
|
use_parentheses = true
|
||||||
|
default_section = THIRDPARTY
|
||||||
|
known_first_party = wagtail_personalisation,tests
|
||||||
|
|
||||||
[*.json, *.yml, *rc]
|
[*.json, *.yml, *rc]
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
|
10
.gitignore
vendored
@ -3,19 +3,23 @@
|
|||||||
*.swo
|
*.swo
|
||||||
*.python-version
|
*.python-version
|
||||||
*.coverage
|
*.coverage
|
||||||
|
.coverage.*
|
||||||
|
|
||||||
*.egg-info/
|
*.egg-info/
|
||||||
|
|
||||||
.cache/
|
.cache/
|
||||||
.idea/
|
.idea/
|
||||||
.tox/
|
.tox/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
build/
|
build/
|
||||||
dist/
|
dist/
|
||||||
tests/sandbox/assets
|
|
||||||
htmlcov/
|
htmlcov/
|
||||||
docs/_build
|
docs/_build
|
||||||
|
|
||||||
coverage.xml
|
coverage.xml
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
.vscode/settings.json
|
|
||||||
|
tests/sandbox/assets
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
26
.travis.yml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
sudo: false
|
||||||
|
language: python
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- python: 2.7
|
||||||
|
env: TOXENV=py27-django111-wagtail110
|
||||||
|
- python: 3.5
|
||||||
|
env: TOXENV=py35-django111-wagtail110
|
||||||
|
- python: 3.6
|
||||||
|
env: TOXENV=py36-django111-wagtail110
|
||||||
|
|
||||||
|
allow_failures:
|
||||||
|
- python: 3.5
|
||||||
|
env: TOXENV=lint
|
||||||
|
|
||||||
|
install:
|
||||||
|
- pip install tox codecov
|
||||||
|
|
||||||
|
script:
|
||||||
|
- tox
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- tox -e coverage-report
|
||||||
|
- codecov
|
11
CHANGES
@ -1,3 +1,8 @@
|
|||||||
0.1 (TBD)
|
0.9.0 (2017-06-02)
|
||||||
====================
|
==================
|
||||||
- Initial release
|
|
||||||
|
Initial release of wagtail-personalisation. This Wagtail module provides basic
|
||||||
|
personalisation based on pre-defined rules in the backend.
|
||||||
|
|
||||||
|
This module was developed by Boris Besemer (@blurrah) and Jasper Berghoef
|
||||||
|
(@jberghoef) for Lab Digital (http://labdigital.nl)
|
||||||
|
10
CONTRIBUTORS.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Authors
|
||||||
|
=======
|
||||||
|
* Jasper Berghoef
|
||||||
|
* Boris Besemer
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
============
|
||||||
|
* Michael van Tellingen
|
||||||
|
* Pim Vernooij
|
||||||
|
* Tomasz Knapik
|
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016-2017 Lab Digital
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
28
Makefile
@ -1,4 +1,4 @@
|
|||||||
.PHONY: all clean requirements develop test lint flake8 isort dist
|
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
|
||||||
|
|
||||||
all: clean requirements dist
|
all: clean requirements dist
|
||||||
|
|
||||||
@ -10,7 +10,9 @@ clean:
|
|||||||
find . -name '*.egg-info' -delete
|
find . -name '*.egg-info' -delete
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
pip install --upgrade -e .
|
pip install --upgrade -e .[docs,test]
|
||||||
|
|
||||||
|
install: develop
|
||||||
|
|
||||||
develop: clean requirements
|
develop: clean requirements
|
||||||
|
|
||||||
@ -21,18 +23,32 @@ retest:
|
|||||||
py.test --nomigrations --reuse-db tests/ -vvv
|
py.test --nomigrations --reuse-db tests/ -vvv
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
py.test --nomigrations --reuse-db tests/ --cov=personalisation --cov-report=term-missing --cov-report=html
|
py.test --nomigrations --reuse-db tests/ --cov=wagtail_personalisation --cov-report=term-missing --cov-report=html
|
||||||
|
|
||||||
|
docs:
|
||||||
|
$(MAKE) -C docs html
|
||||||
|
|
||||||
lint: flake8 isort
|
lint: flake8 isort
|
||||||
|
|
||||||
flake8:
|
flake8:
|
||||||
pip install flake8 flake8-debugger flake8-blind-except
|
flake8 src/ tests/
|
||||||
flake8 src/
|
|
||||||
|
|
||||||
isort:
|
isort:
|
||||||
pip install isort
|
pip install isort
|
||||||
isort --recursive src tests
|
isort --recursive src tests
|
||||||
|
|
||||||
|
|
||||||
dist:
|
dist:
|
||||||
./setup.py sdist bdist_wheel
|
./setup.py sdist bdist_wheel
|
||||||
|
|
||||||
|
sandbox:
|
||||||
|
pip install -r sandbox/requirements.txt
|
||||||
|
sandbox/manage.py migrate
|
||||||
|
sandbox/manage.py loaddata sandbox/exampledata/users.json
|
||||||
|
sandbox/manage.py loaddata sandbox/exampledata/personalisation.json
|
||||||
|
sandbox/manage.py runserver
|
||||||
|
|
||||||
|
release:
|
||||||
|
pip install twine wheel
|
||||||
|
rm -rf dist/*
|
||||||
|
python setup.py sdist bdist_wheel
|
||||||
|
twine upload -s dist/*
|
||||||
|
62
README.rst
@ -1,25 +1,75 @@
|
|||||||
.. image:: logo.png
|
.. start-no-pypi
|
||||||
|
|
||||||
Wagtail personalisation
|
.. image:: https://readthedocs.org/projects/wagtail-personalisation/badge/?version=latest
|
||||||
|
:target: http://wagtail-personalisation.readthedocs.io/en/latest/?badge=latest
|
||||||
|
|
||||||
|
.. image:: https://travis-ci.org/LabD/wagtail-personalisation.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/LabD/wagtail-personalisation
|
||||||
|
|
||||||
|
.. image:: http://codecov.io/github/LabD/wagtail-personalisation/coverage.svg?branch=master
|
||||||
|
:target: http://codecov.io/github/LabD/wagtail-personalisation?branch=master
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/wagtail-personalisation.svg
|
||||||
|
:target: https://pypi.python.org/pypi/wagtail-personalisation/
|
||||||
|
|
||||||
|
.. end-no-pypi
|
||||||
|
|
||||||
|
Wagtail Personalisation
|
||||||
=======================
|
=======================
|
||||||
Wagtail personalisation enables simple content personalisation through segmenting for Wagtail.
|
|
||||||
|
Wagtail Personalisation is a fully-featured personalisation module for
|
||||||
|
`Wagtail CMS`_. It enables editors to create customised pages
|
||||||
|
- or parts of pages - based on segments whose rules are configured directly
|
||||||
|
in the admin interface.
|
||||||
|
|
||||||
|
.. _Wagtail CMS: http://wagtail.io/
|
||||||
|
|
||||||
|
.. image:: logo.png
|
||||||
|
:scale: 50 %
|
||||||
|
:alt: Wagxperience
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
|
||||||
|
.. image:: screenshot.png
|
||||||
|
|
||||||
|
|
||||||
Instructions
|
Instructions
|
||||||
------------
|
------------
|
||||||
|
Wagtail Personalisation requires Wagtail 1.10 and Django 1.11.
|
||||||
|
|
||||||
To install the package with pip::
|
To install the package with pip::
|
||||||
|
|
||||||
pip install wagtail-personalisation
|
pip install wagtail-personalisation
|
||||||
|
|
||||||
Next, include the ``personalisation`` and ``wagtail.contrib.modeladmin`` app in your project's ``INSTALLED_APPS``:
|
Next, include the ``wagtail_personalisation``, ``wagtail.contrib.modeladmin``
|
||||||
|
and ``wagtailfontawesome`` apps in your project's ``INSTALLED_APPS``:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
# ...
|
# ...
|
||||||
'wagtail.contrib.modeladmin',
|
'wagtail.contrib.modeladmin',
|
||||||
'personalisation',
|
'wagtail_personalisation',
|
||||||
|
'wagtailfontawesome',
|
||||||
# ...
|
# ...
|
||||||
]
|
]
|
||||||
|
|
||||||
Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has been added in first, this is a prerequisite for this project.
|
Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has
|
||||||
|
been added in first, this is a prerequisite for this project.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
|
||||||
|
Sandbox
|
||||||
|
-------
|
||||||
|
|
||||||
|
To experiment with the package you can use the sandbox provided in
|
||||||
|
this repository. To install this you will need to create and activate a
|
||||||
|
virtualenv and then run ``make sandbox``. This will start a fresh Wagtail
|
||||||
|
install, with the personalisation module enabled, on http://localhost:8000
|
||||||
|
and http://localhost:8000/cms/. The superuser credentials are
|
||||||
|
``superuser@example.com`` with the password ``testing``.
|
||||||
|
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
SPHINXPROJ = wagtail-personalisation
|
||||||
|
SOURCEDIR = .
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
163
docs/conf.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# wagtail-personalisation documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Mon Dec 19 15:12:32 2016.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
# import os
|
||||||
|
# import sys
|
||||||
|
# sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = []
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = 'wagtail-personalisation'
|
||||||
|
copyright = '2017, Lab Digital BV'
|
||||||
|
author = 'Lab Digital BV'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
version = '0.9.0'
|
||||||
|
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = '0.9.0'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = None
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
#
|
||||||
|
# html_theme_options = {}
|
||||||
|
html_theme_options = {
|
||||||
|
'github_user': 'LabD',
|
||||||
|
'github_banner': True,
|
||||||
|
'github_repo': 'wagtail-personalisation',
|
||||||
|
'travis_button': True,
|
||||||
|
'codecov_button': True,
|
||||||
|
'analytics_id': 'UA-100203499-2',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
# html_static_path = ['_static']
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTMLHelp output ------------------------------------------
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'wagtail-personalisationdoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'wagtail-personalisation.tex', 'wagtail-personalisation Documentation',
|
||||||
|
'Lab Digital BV', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
|
||||||
|
author, 'wagtail-personalisation', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
123
docs/default_rules.rst
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
Included rules
|
||||||
|
==============
|
||||||
|
|
||||||
|
Time rule
|
||||||
|
---------
|
||||||
|
|
||||||
|
The time rule allows you to segment visitors based on the time of their visit.
|
||||||
|
Define a time frame in which visitors are matched to this segment.
|
||||||
|
|
||||||
|
================== ==========================================================
|
||||||
|
Option Description
|
||||||
|
================== ==========================================================
|
||||||
|
Start time The start time of your time frame.
|
||||||
|
End time The end time of your time frame.
|
||||||
|
================== ==========================================================
|
||||||
|
|
||||||
|
``wagtail_personalisation.rules.TimeRule``
|
||||||
|
|
||||||
|
Day rule
|
||||||
|
--------
|
||||||
|
|
||||||
|
The day rule allows you to segment visitors based on the day of their visit.
|
||||||
|
Select one or multiple days on which you'd like your segment to be applied.
|
||||||
|
|
||||||
|
================== ==========================================================
|
||||||
|
Option Description
|
||||||
|
================== ==========================================================
|
||||||
|
Monday Matches when the visitors visits on a monday.
|
||||||
|
Tuesday Matches when the visitors visits on a tuesday.
|
||||||
|
Wednesday Matches when the visitors visits on a wednesday.
|
||||||
|
Thursday Matches when the visitors visits on a thursday.
|
||||||
|
Friday Matches when the visitors visits on a friday.
|
||||||
|
Saturday Matches when the visitors visits on a saturday.
|
||||||
|
Sunday Matches when the visitors visits on a sunday.
|
||||||
|
================== ==========================================================
|
||||||
|
|
||||||
|
``wagtail_personalisation.rules.DayRule``
|
||||||
|
|
||||||
|
Referral rule
|
||||||
|
-------------
|
||||||
|
|
||||||
|
The referral rule allows you to match visitors based on the website they were
|
||||||
|
referred from. For example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
example\.com|secondexample\.com|.*subdomain\.com
|
||||||
|
|
||||||
|
================== ==========================================================
|
||||||
|
Option Description
|
||||||
|
================== ==========================================================
|
||||||
|
Regex string The regex string to match the referral header to.
|
||||||
|
================== ==========================================================
|
||||||
|
|
||||||
|
``wagtail_personalisation.rules.ReferralRule``
|
||||||
|
|
||||||
|
Visit count rule
|
||||||
|
----------------
|
||||||
|
|
||||||
|
The visit count rule allows you to segment a visitor based on the amount of
|
||||||
|
visits per page. Use the operator to to set a maximum, minimum or equal
|
||||||
|
amount of visits.
|
||||||
|
|
||||||
|
================== ==========================================================
|
||||||
|
Option Description
|
||||||
|
================== ==========================================================
|
||||||
|
Page The page on which visits will be counted.
|
||||||
|
Count The amount of visits to match.
|
||||||
|
Operator Whether to match for more than, less than or equal to the
|
||||||
|
specified visit count.
|
||||||
|
================== ==========================================================
|
||||||
|
|
||||||
|
``wagtail_personalisation.rules.VisitCountRule``
|
||||||
|
|
||||||
|
Query rule
|
||||||
|
----------
|
||||||
|
|
||||||
|
The query rule allows you to match a visitor based on the query included in
|
||||||
|
the url. It let's you define both the parameter and the value. It will look
|
||||||
|
something like this:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
example.com/?campaign=ourbestoffer
|
||||||
|
|
||||||
|
================== ==========================================================
|
||||||
|
Option Description
|
||||||
|
================== ==========================================================
|
||||||
|
Parameter The first part of the query ('campaign').
|
||||||
|
Value The second part of the query ('ourbestoffer').
|
||||||
|
================== ==========================================================
|
||||||
|
|
||||||
|
``wagtail_personalisation.rules.QueryRule``
|
||||||
|
|
||||||
|
Device rule
|
||||||
|
-----------
|
||||||
|
|
||||||
|
The device rule allows you to match visitors by the type of device they are
|
||||||
|
using. You can select any combination you want.
|
||||||
|
|
||||||
|
================== ==========================================================
|
||||||
|
Option Description
|
||||||
|
================== ==========================================================
|
||||||
|
Mobile phone Matches when the visitor uses a mobile phone.
|
||||||
|
Tablet Matches when the visitor uses a tablet.
|
||||||
|
Desktop Matches when the visitor uses a desktop.
|
||||||
|
================== ==========================================================
|
||||||
|
|
||||||
|
``wagtail_personalisation.rules.DeviceRule``
|
||||||
|
|
||||||
|
User is logged in rule
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
The user is logged in rule allows you to match visitors that are authenticated
|
||||||
|
and logged in to your app.
|
||||||
|
|
||||||
|
================== ==========================================================
|
||||||
|
Option Description
|
||||||
|
================== ==========================================================
|
||||||
|
Is logged in Whether the user is logged in or logged out.
|
||||||
|
================== ==========================================================
|
||||||
|
|
||||||
|
``wagtail_personalisation.rules.UserIsLoggedInRule``
|
32
docs/getting_started.rst
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
Getting started
|
||||||
|
===============
|
||||||
|
|
||||||
|
|
||||||
|
Installing Wagxperience
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Installing the module
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The Wagxperience app runs in the Wagtail CMS. You can find out more here_.
|
||||||
|
|
||||||
|
.. _here: http://docs.wagtail.io/en/latest/getting_started/tutorial.html
|
||||||
|
|
||||||
|
1. Install the module::
|
||||||
|
|
||||||
|
pip install wagtail-personalisation
|
||||||
|
|
||||||
|
2. Add the module and ``wagtail.contrib.modeladmin`` to your ``INSTALLED_APPS``::
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# ...
|
||||||
|
'wagtail.contrib.modeladmin',
|
||||||
|
'wagtail_personalisation',
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
|
||||||
|
3. Update your database::
|
||||||
|
|
||||||
|
python manage.py migrate
|
||||||
|
|
||||||
|
Continue reading: :doc:`implementation`
|
87
docs/implementation.rst
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
Implementation
|
||||||
|
===============
|
||||||
|
|
||||||
|
Extending a page to be personalisable
|
||||||
|
-------------------------------------
|
||||||
|
Wagxperience offers a ``PersonalisablePage`` base class to extend from.
|
||||||
|
This is a standard ``Page`` class with personalisation options added.
|
||||||
|
|
||||||
|
Creating a new personalisable page
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
Import and extend the ``personalisation.models.PersonalisablePage`` class to create a personalisable page.
|
||||||
|
|
||||||
|
A very simple example for a personalisable homepage::
|
||||||
|
|
||||||
|
from wagtail_personalisation.models import PersonalisablePage
|
||||||
|
|
||||||
|
class HomePage(PersonalisablePage):
|
||||||
|
subtitle = models.CharField(max_length=255)
|
||||||
|
body = RichTextField(blank=True, default='')
|
||||||
|
|
||||||
|
content_panels = PersonalisablePage.content_panels + [
|
||||||
|
FieldPanel('subtitle'),
|
||||||
|
FieldPanel('body'),
|
||||||
|
]
|
||||||
|
|
||||||
|
It's just as simple as extending a standard ``Page`` class.
|
||||||
|
|
||||||
|
Migrating an existing page to be personalisable
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
||||||
|
Creating custom rules
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Rules consist of two important elements, the model's fields and the ``test_user`` function.
|
||||||
|
|
||||||
|
A very simple example of a rule would look something like this::
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
||||||
|
from personalisation import AbstractBaseRule
|
||||||
|
|
||||||
|
class MyNewRule(AbstractBaseRule):
|
||||||
|
field = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
FieldPanel('field'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(MyNewRule, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def test_user(self, request):
|
||||||
|
return self.field
|
||||||
|
|
||||||
|
As you can see, the only real requirement is the ``test_user`` function that will either return
|
||||||
|
``True`` or ``False`` based on the model's fields and optionally the request object.
|
||||||
|
|
||||||
|
Below is the "Time rule" model included with the module, which offers more complex functionality::
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class TimeRule(AbstractBaseRule):
|
||||||
|
"""Time rule to segment users based on a start and end time"""
|
||||||
|
start_time = models.TimeField(_("Starting time"))
|
||||||
|
end_time = models.TimeField(_("Ending time"))
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
FieldRowPanel([
|
||||||
|
FieldPanel('start_time'),
|
||||||
|
FieldPanel('end_time'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(TimeRule, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def test_user(self, request=None):
|
||||||
|
current_time = datetime.now().time()
|
||||||
|
starting_time = self.start_time
|
||||||
|
ending_time = self.end_time
|
||||||
|
|
||||||
|
return starting_time <= current_time <= ending_time
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return 'Time Rule'
|
||||||
|
|
||||||
|
Continue reading: :doc:`usage_guide`
|
24
docs/index.rst
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
.. wagtail-personalisation documentation master file, created by
|
||||||
|
sphinx-quickstart on Mon Dec 19 15:12:32 2016.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
Welcome to the Wagxperience documentation!
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
|
||||||
|
getting_started
|
||||||
|
implementation
|
||||||
|
usage_guide
|
||||||
|
default_rules
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
36
docs/make.bat
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set SOURCEDIR=.
|
||||||
|
set BUILDDIR=_build
|
||||||
|
set SPHINXPROJ=wagtail-personalisation
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.http://sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
95
docs/usage_guide.rst
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
Usage guide
|
||||||
|
===========
|
||||||
|
|
||||||
|
Creating a segment
|
||||||
|
------------------
|
||||||
|
|
||||||
|
As soon as the installation is completed and configured, the module will be
|
||||||
|
visible in the Wagtail administrative area.
|
||||||
|
|
||||||
|
To create a segment, go to the "Segments" page and click on "Add a new segment".
|
||||||
|
|
||||||
|
On this page you will be presented with a form. Follow these steps to create a
|
||||||
|
new segment:
|
||||||
|
|
||||||
|
1. Enter a name for your segment.
|
||||||
|
|
||||||
|
2. (Optional) Select whether to match any or all defined rules.
|
||||||
|
|
||||||
|
``match any`` will result in a segment that is applied as soon as one of
|
||||||
|
your rules matches the visitor. When ``match all`` is selected, all rules
|
||||||
|
must match before the segment is applied.
|
||||||
|
|
||||||
|
3. (Optional) Set the segment persistence.
|
||||||
|
|
||||||
|
When persistence is enabled, your segment will stick to the visitor once
|
||||||
|
applied, even if the rules no longer match on the next visit.
|
||||||
|
|
||||||
|
4. Define your segment rules.
|
||||||
|
|
||||||
|
Wagxperience comes with a basic set of :doc:`default_rules` that allow
|
||||||
|
you to get started quickly. The rules you define will be evaluated once a
|
||||||
|
visitor makes a request to your application.
|
||||||
|
|
||||||
|
5. Save your segment.
|
||||||
|
|
||||||
|
Click "save" to store your segment. It will be enabled by default,
|
||||||
|
unless otherwise defined.
|
||||||
|
|
||||||
|
|
||||||
|
Creating personalized content
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
Once you've created a segment you can start serving these visitors with
|
||||||
|
personalised content. To do this, you can go one of two directions.
|
||||||
|
|
||||||
|
1. Create a copy of a page for your segment.
|
||||||
|
|
||||||
|
2. Create StreamField blocks only visible to your segment.
|
||||||
|
|
||||||
|
3. Create a template block only visible to your segment.
|
||||||
|
|
||||||
|
|
||||||
|
Method 1: Create a copy
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
To create a copy from a page for a specific Segment (which you can change to
|
||||||
|
your liking after copying it) simply go to the Explorer section and find the
|
||||||
|
page you'd wish to personalize.
|
||||||
|
|
||||||
|
You'll notice a new "Variants" dropdown button has appeared. Click the button
|
||||||
|
and select the segment you'd like to create personalized content for.
|
||||||
|
|
||||||
|
Once you've selected the segment, a copy of the page will be created with a
|
||||||
|
title that includes the segment. Don't worry, your visitors won't be able to
|
||||||
|
see this title.
|
||||||
|
|
||||||
|
You can change everything on this page you'd like. Visitors that are included in
|
||||||
|
your segment, will automatically see the new page you've created for them.
|
||||||
|
|
||||||
|
|
||||||
|
Method 2: Create a StreamField block
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
|
||||||
|
Method 3: Create a template block
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
You can add a template block that only shows its contents to users of a
|
||||||
|
specific segment. This is done using the "segment" block.
|
||||||
|
|
||||||
|
When editing templates make sure to load the ``wagtail_personalisation_tags``
|
||||||
|
tags library in the template::
|
||||||
|
|
||||||
|
{% load wagtail_personalisation_tags %}
|
||||||
|
|
||||||
|
After that you can add a template block with the name of the segment you want
|
||||||
|
the content to show up for::
|
||||||
|
|
||||||
|
{% segment name="My Segment" %}
|
||||||
|
<p>Only users within "My Segment" see this!</p>
|
||||||
|
{% endsegment %}
|
||||||
|
|
||||||
|
The template block currently only supports one segment at a time. If you want
|
||||||
|
to target multiple segments you will have to make multiple blocks with the
|
||||||
|
same content.
|
1
frontend/js/dashboard.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '../scss/dashboard.scss';
|
1
frontend/js/form.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '../scss/form.scss';
|
1
frontend/js/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '../scss/index.scss';
|
@ -25,6 +25,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block_container .block--disabled h2,
|
||||||
|
.block_container .block--disabled .inspect_container {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.block_container .block h2 {
|
.block_container .block h2 {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: auto;
|
width: auto;
|
||||||
@ -83,6 +88,11 @@
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.block_container .block span.icon::before {
|
||||||
|
margin-right: 0.3em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
.block_container .block .inspect_container .inspect li {
|
.block_container .block .inspect_container .inspect li {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
@ -96,35 +106,6 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block_container .block .inspect_container .inspect li span::before {
|
|
||||||
display: inline-block;
|
|
||||||
content: "";
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-right: 5px;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block_container .block .inspect_container .segment_stats .visit_stat span::before {
|
|
||||||
background-image: url("./rocket_icon.png");
|
|
||||||
}
|
|
||||||
.block_container .block .inspect_container .segment_stats .days_stat span::before {
|
|
||||||
background-image: url("./calendar_icon.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.block_container .block .inspect_container .segment_rules .persistent_state span::before {
|
|
||||||
background-image: url("./persistent_icon.png");
|
|
||||||
}
|
|
||||||
.block_container .block .inspect_container .segment_rules .persistent_state.fleeting span::before {
|
|
||||||
transform: rotate(45deg) translateY(-2px);
|
|
||||||
}
|
|
||||||
.block_container .block .inspect_container .segment_rules .time_rule span::before {
|
|
||||||
background-image: url("./time_icon.png");
|
|
||||||
}
|
|
||||||
.block_container .block .inspect_container .segment_rules .visit_count_rule span::before {
|
|
||||||
background-image: url("./visit_count_icon.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.block_container .block .inspect_container .inspect li pre {
|
.block_container .block .inspect_container .inspect li pre {
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -138,25 +119,6 @@
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block_container .block .inspect_container .inspect li pre::before {
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
left: -21px;
|
|
||||||
top: 6px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-right: 5px;
|
|
||||||
background-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block_container .block .inspect_container .segment_rules .referral_rule pre::before {
|
|
||||||
background-image: url("./referral_icon.png");
|
|
||||||
}
|
|
||||||
.block_container .block .inspect_container .segment_rules .query_rule pre::before {
|
|
||||||
background-image: url("./referral_icon.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
.block_container .block.suggestion .suggestive_text {
|
.block_container .block.suggestion .suggestive_text {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
BIN
logo.png
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
22
manage.py
@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.sandbox.settings")
|
|
||||||
try:
|
|
||||||
from django.core.management import execute_from_command_line
|
|
||||||
except ImportError:
|
|
||||||
# The above import may fail for some other reason. Ensure that the
|
|
||||||
# issue is really that Django is missing to avoid masking other
|
|
||||||
# exceptions on Python 2.
|
|
||||||
try:
|
|
||||||
import django
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError(
|
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
|
||||||
"forget to activate a virtual environment?"
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
execute_from_command_line(sys.argv)
|
|
53
package.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"autoprefixer": "^7.1.1",
|
||||||
|
"babel-core": "^6.24.1",
|
||||||
|
"babel-loader": "^7.0.0",
|
||||||
|
"babel-preset-es2015": "^6.24.1",
|
||||||
|
"babel-preset-react": "^6.24.1",
|
||||||
|
"babel-preset-stage-0": "^6.24.1",
|
||||||
|
"copy-webpack-plugin": "^4.0.1",
|
||||||
|
"css-loader": "^0.28.2",
|
||||||
|
"eslint": "^3.19.0",
|
||||||
|
"eslint-config-airbnb": "^15.0.1",
|
||||||
|
"eslint-plugin-import": "^2.2.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^5.0.3",
|
||||||
|
"eslint-plugin-react": "^7.0.1",
|
||||||
|
"extract-text-webpack-plugin": "^2.1.0",
|
||||||
|
"file-loader": "^0.11.1",
|
||||||
|
"imagemin-webpack-plugin": "^1.4.4",
|
||||||
|
"jshint": "^2.9.4",
|
||||||
|
"mocha": "^3.4.1",
|
||||||
|
"node-sass": "^4.5.3",
|
||||||
|
"postcss-loader": "^2.0.5",
|
||||||
|
"sass-loader": "^6.0.5",
|
||||||
|
"style-loader": "^0.18.0",
|
||||||
|
"uglify-js": "^3.0.10",
|
||||||
|
"uglifyjs-webpack-plugin": "^0.4.3",
|
||||||
|
"webpack": "^2.6.0"
|
||||||
|
},
|
||||||
|
"name": "wagtail-personalisation",
|
||||||
|
"description": "Wagxperience personalisation module for Wagtail.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "webpack.config.js",
|
||||||
|
"directories": {
|
||||||
|
"doc": "docs",
|
||||||
|
"test": "tests"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "yarn compile && yarn watch",
|
||||||
|
"compile": "webpack --bail",
|
||||||
|
"watch": "webpack --watch",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/LabD/wagtail-personalisation.git"
|
||||||
|
},
|
||||||
|
"author": "Lab Digital",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/LabD/wagtail-personalisation/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/LabD/wagtail-personalisation#readme"
|
||||||
|
}
|
148
sandbox/exampledata/personalisation.json
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
[{
|
||||||
|
"model": "wagtail_personalisation.timerule",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"segment": 2,
|
||||||
|
"start_time": "06:00:00",
|
||||||
|
"end_time": "11:00:00"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "wagtail_personalisation.visitcountrule",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"segment": 1,
|
||||||
|
"operator": "more_than",
|
||||||
|
"count": 3,
|
||||||
|
"counted_page": 3
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "wagtail_personalisation.segment",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"name": "Returning Rook",
|
||||||
|
"create_date": "2017-06-02T05:38:02.304Z",
|
||||||
|
"edit_date": "2017-06-02T10:58:39.399Z",
|
||||||
|
"enable_date": "2017-06-02T10:58:39.389Z",
|
||||||
|
"disable_date": "2017-06-02T10:34:51.722Z",
|
||||||
|
"visit_count": 0,
|
||||||
|
"status": "enabled",
|
||||||
|
"persistent": false,
|
||||||
|
"match_any": false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "wagtail_personalisation.segment",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Early Birds",
|
||||||
|
"create_date": "2017-06-02T05:38:14.749Z",
|
||||||
|
"edit_date": "2017-06-02T10:57:44.504Z",
|
||||||
|
"enable_date": "2017-06-02T10:57:44.497Z",
|
||||||
|
"disable_date": "2017-06-02T10:57:39.984Z",
|
||||||
|
"visit_count": 1,
|
||||||
|
"status": "enabled",
|
||||||
|
"persistent": false,
|
||||||
|
"match_any": false
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "wagtail_personalisation.personalisablepagemetadata",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"canonical_page": 3,
|
||||||
|
"variant": 3,
|
||||||
|
"segment": null
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "wagtail_personalisation.personalisablepagemetadata",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"canonical_page": 3,
|
||||||
|
"variant": 4,
|
||||||
|
"segment": 1
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "home.homepage",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"intro": "<p>Thank you for trying <a href=\"http://wagxperience.io\">Wagxperience</a>!</p>",
|
||||||
|
"body": "[{\"type\": \"personalisable_paragraph\", \"value\": {\"segment\": \"2\", \"paragraph\": \"<p>You are an early bird!</p>\"}}]"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "home.homepage",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"intro": "<p>Thank you for trying <a href=\"http://wagxperience.io\">Wagxperience</a>!</p><p>You've visited the homepage more than 3 times!</p>",
|
||||||
|
"body": "[]"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"path": "0001",
|
||||||
|
"depth": 1,
|
||||||
|
"numchild": 1,
|
||||||
|
"title": "Root",
|
||||||
|
"slug": "root",
|
||||||
|
"content_type": 1,
|
||||||
|
"live": true,
|
||||||
|
"has_unpublished_changes": false,
|
||||||
|
"url_path": "/",
|
||||||
|
"owner": null,
|
||||||
|
"seo_title": "",
|
||||||
|
"show_in_menus": false,
|
||||||
|
"search_description": "",
|
||||||
|
"go_live_at": null,
|
||||||
|
"expire_at": null,
|
||||||
|
"expired": false,
|
||||||
|
"locked": false,
|
||||||
|
"first_published_at": null,
|
||||||
|
"latest_revision_created_at": null
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"pk": 3,
|
||||||
|
"fields": {
|
||||||
|
"path": "00010001",
|
||||||
|
"depth": 2,
|
||||||
|
"numchild": 0,
|
||||||
|
"title": "Home",
|
||||||
|
"slug": "home",
|
||||||
|
"content_type": 2,
|
||||||
|
"live": true,
|
||||||
|
"has_unpublished_changes": false,
|
||||||
|
"url_path": "/home/",
|
||||||
|
"owner": null,
|
||||||
|
"seo_title": "",
|
||||||
|
"show_in_menus": false,
|
||||||
|
"search_description": "",
|
||||||
|
"go_live_at": null,
|
||||||
|
"expire_at": null,
|
||||||
|
"expired": false,
|
||||||
|
"locked": false,
|
||||||
|
"first_published_at": "2017-06-02T10:35:34.706Z",
|
||||||
|
"latest_revision_created_at": "2017-06-02T10:35:34.565Z"
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
"model": "wagtailcore.page",
|
||||||
|
"pk": 4,
|
||||||
|
"fields": {
|
||||||
|
"path": "00010002",
|
||||||
|
"depth": 2,
|
||||||
|
"numchild": 0,
|
||||||
|
"title": "Home (Returning Rook)",
|
||||||
|
"slug": "home-returning-rook",
|
||||||
|
"content_type": 2,
|
||||||
|
"live": true,
|
||||||
|
"has_unpublished_changes": false,
|
||||||
|
"url_path": "/home-returning-rook/",
|
||||||
|
"owner": null,
|
||||||
|
"seo_title": "",
|
||||||
|
"show_in_menus": false,
|
||||||
|
"search_description": "",
|
||||||
|
"go_live_at": null,
|
||||||
|
"expire_at": null,
|
||||||
|
"expired": false,
|
||||||
|
"locked": false,
|
||||||
|
"first_published_at": "2017-06-02T05:38:53.568Z",
|
||||||
|
"latest_revision_created_at": "2017-06-02T05:38:53.390Z"
|
||||||
|
}
|
||||||
|
}]
|
19
sandbox/exampledata/users.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"model": "user.user",
|
||||||
|
"pk": 1,
|
||||||
|
"fields": {
|
||||||
|
"password": "pbkdf2_sha256$36000$jW4Pr4OWkdVf$1KeQmcYPL1/qZvRX9ECQvoYuXTRbs+tlV480K2AqFUM=",
|
||||||
|
"last_login": "2017-05-08T07:38:49.391Z",
|
||||||
|
"is_superuser": true,
|
||||||
|
"first_name": "S.",
|
||||||
|
"last_name": "Uper",
|
||||||
|
"email": "superuser@example.com",
|
||||||
|
"is_staff": true,
|
||||||
|
"is_active": true,
|
||||||
|
"date_joined": "2015-10-17T11:32:57.969Z",
|
||||||
|
"groups": [],
|
||||||
|
"user_permissions": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
12
sandbox/manage.py
Executable file
@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings")
|
||||||
|
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
|
||||||
|
execute_from_command_line(sys.argv)
|
4
sandbox/requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Django>=1.11,<1.12
|
||||||
|
wagtail>=1.10,<1.11
|
||||||
|
django-debug-toolbar==1.8
|
||||||
|
-e .[docs,test]
|
1
sandbox/sandbox/apps/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
30
sandbox/sandbox/apps/home/migrations/0001_initial.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.1 on 2017-05-31 16:59
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import modelcluster.fields
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||||
|
('wagtail_personalisation', '0011_personalisablepagemetadata'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='HomePage',
|
||||||
|
fields=[
|
||||||
|
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
bases=('wagtailcore.page', models.Model),
|
||||||
|
),
|
||||||
|
]
|
59
sandbox/sandbox/apps/home/migrations/0002_create_homepage.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def create_homepage(apps, schema_editor):
|
||||||
|
# Get models
|
||||||
|
ContentType = apps.get_model('contenttypes.ContentType')
|
||||||
|
Page = apps.get_model('wagtailcore.Page')
|
||||||
|
Site = apps.get_model('wagtailcore.Site')
|
||||||
|
HomePage = apps.get_model('home.HomePage')
|
||||||
|
|
||||||
|
# Delete the default homepage
|
||||||
|
# If migration is run multiple times, it may have already been deleted
|
||||||
|
Page.objects.filter(id=2).delete()
|
||||||
|
|
||||||
|
# Create content type for homepage model
|
||||||
|
homepage_content_type, __ = ContentType.objects.get_or_create(
|
||||||
|
model='homepage', app_label='home')
|
||||||
|
|
||||||
|
# Create a new homepage
|
||||||
|
homepage = HomePage.objects.create(
|
||||||
|
title="Home",
|
||||||
|
slug='home',
|
||||||
|
content_type=homepage_content_type,
|
||||||
|
path='00010001',
|
||||||
|
depth=2,
|
||||||
|
numchild=0,
|
||||||
|
url_path='/home/',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a site with the new homepage set as the root
|
||||||
|
Site.objects.create(
|
||||||
|
hostname='localhost', root_page=homepage, is_default_site=True)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_homepage(apps, schema_editor):
|
||||||
|
# Get models
|
||||||
|
ContentType = apps.get_model('contenttypes.ContentType')
|
||||||
|
HomePage = apps.get_model('home.HomePage')
|
||||||
|
|
||||||
|
# Delete the default homepage
|
||||||
|
# Page and Site objects CASCADE
|
||||||
|
HomePage.objects.filter(slug='home', depth=2).delete()
|
||||||
|
|
||||||
|
# Delete content type for homepage model
|
||||||
|
ContentType.objects.filter(model='homepage', app_label='home').delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('home', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(create_homepage, remove_homepage),
|
||||||
|
]
|
@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.1 on 2017-05-31 19:36
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
import wagtail.wagtailcore.fields
|
||||||
|
import wagtail_personalisation
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('home', '0002_create_homepage'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='homepage',
|
||||||
|
name='intro',
|
||||||
|
field=wagtail.wagtailcore.fields.RichTextField(
|
||||||
|
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='homepage',
|
||||||
|
name='body',
|
||||||
|
field=wagtail.wagtailcore.fields.StreamField((('personalisable_paragraph', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
23
sandbox/sandbox/apps/home/models.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
|
||||||
|
from wagtail.wagtailcore import blocks
|
||||||
|
from wagtail.wagtailcore.fields import RichTextField, StreamField
|
||||||
|
from wagtail.wagtailcore.models import Page
|
||||||
|
|
||||||
|
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||||
|
from wagtail_personalisation.blocks import PersonalisedStructBlock
|
||||||
|
|
||||||
|
|
||||||
|
class HomePage(PersonalisablePageMixin, Page):
|
||||||
|
intro = RichTextField()
|
||||||
|
body = StreamField([
|
||||||
|
('personalisable_paragraph', PersonalisedStructBlock([
|
||||||
|
('paragraph', blocks.RichTextBlock()),
|
||||||
|
], icon='pilcrow'))
|
||||||
|
])
|
||||||
|
|
||||||
|
content_panels = Page.content_panels + [
|
||||||
|
RichTextFieldPanel('intro'),
|
||||||
|
StreamFieldPanel('body'),
|
||||||
|
]
|
18
sandbox/sandbox/apps/home/templates/home/home_page.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load wagtailcore_tags %}
|
||||||
|
|
||||||
|
{% block body_class %}template-homepage{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Wagtail Personalisation</h1>
|
||||||
|
<hr>
|
||||||
|
<h2>{{ self.title }}</h2>
|
||||||
|
|
||||||
|
{{ self.intro|richtext }}
|
||||||
|
|
||||||
|
{% for block in page.body %}
|
||||||
|
<div>{% include_block block %}</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
38
sandbox/sandbox/apps/search/templates/search/search.html
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load static wagtailcore_tags %}
|
||||||
|
|
||||||
|
{% block body_class %}template-searchresults{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}Search{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Search</h1>
|
||||||
|
|
||||||
|
<form action="{% url 'search' %}" method="get">
|
||||||
|
<input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}>
|
||||||
|
<input type="submit" value="Search" class="button">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if search_results %}
|
||||||
|
<ul>
|
||||||
|
{% for result in search_results %}
|
||||||
|
<li>
|
||||||
|
<h4><a href="{% pageurl result %}">{{ result }}</a></h4>
|
||||||
|
{% if result.search_description %}
|
||||||
|
{{ result.search_description }}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% if search_results.has_previous %}
|
||||||
|
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.previous_page_number }}">Previous</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if search_results.has_next %}
|
||||||
|
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.next_page_number }}">Next</a>
|
||||||
|
{% endif %}
|
||||||
|
{% elif search_query %}
|
||||||
|
No results found
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
36
sandbox/sandbox/apps/search/views.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from wagtail.wagtailcore.models import Page
|
||||||
|
from wagtail.wagtailsearch.models import Query
|
||||||
|
|
||||||
|
|
||||||
|
def search(request):
|
||||||
|
search_query = request.GET.get('query', None)
|
||||||
|
page = request.GET.get('page', 1)
|
||||||
|
|
||||||
|
# Search
|
||||||
|
if search_query:
|
||||||
|
search_results = Page.objects.live().search(search_query)
|
||||||
|
query = Query.get(search_query)
|
||||||
|
|
||||||
|
# Record hit
|
||||||
|
query.add_hit()
|
||||||
|
else:
|
||||||
|
search_results = Page.objects.none()
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
paginator = Paginator(search_results, 10)
|
||||||
|
try:
|
||||||
|
search_results = paginator.page(page)
|
||||||
|
except PageNotAnInteger:
|
||||||
|
search_results = paginator.page(1)
|
||||||
|
except EmptyPage:
|
||||||
|
search_results = paginator.page(paginator.num_pages)
|
||||||
|
|
||||||
|
return render(request, 'search/search.html', {
|
||||||
|
'search_query': search_query,
|
||||||
|
'search_results': search_results,
|
||||||
|
})
|
0
sandbox/sandbox/apps/user/__init__.py
Normal file
42
sandbox/sandbox/apps/user/admin.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
|
||||||
|
from sandbox.apps.user import forms, models
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(models.User)
|
||||||
|
class UserAdmin(BaseUserAdmin):
|
||||||
|
form = forms.UserChangeForm
|
||||||
|
add_form = forms.UserCreationForm
|
||||||
|
|
||||||
|
# The fields to be used in displaying the User model.
|
||||||
|
# These override the definitions on the base UserAdmin
|
||||||
|
# that reference specific fields on auth.User.
|
||||||
|
list_display = ['email']
|
||||||
|
list_filter = ['is_superuser']
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'fields': ['email', 'password']
|
||||||
|
}),
|
||||||
|
('Personal info', {
|
||||||
|
'fields': ['first_name', 'last_name']
|
||||||
|
}),
|
||||||
|
('Permissions', {
|
||||||
|
'fields': [
|
||||||
|
'is_active', 'is_staff', 'is_superuser',
|
||||||
|
'groups', 'user_permissions'
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
|
||||||
|
# overrides get_fieldsets to use this attribute when creating a user.
|
||||||
|
add_fieldsets = (
|
||||||
|
(None, {
|
||||||
|
'classes': ('wide',),
|
||||||
|
'fields': ['email', 'password1', 'password2']
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
search_fields = ['first_name', 'last_name', 'email']
|
||||||
|
ordering = ['email']
|
||||||
|
filter_horizontal = []
|
63
sandbox/sandbox/apps/user/forms.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from sandbox.apps.user import models
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreationForm(forms.ModelForm):
|
||||||
|
"""A form for creating new users. Includes all the required
|
||||||
|
fields, plus a repeated password.
|
||||||
|
|
||||||
|
"""
|
||||||
|
password1 = forms.CharField(
|
||||||
|
label='Password', widget=forms.PasswordInput,
|
||||||
|
required=False)
|
||||||
|
password2 = forms.CharField(
|
||||||
|
label='Password confirmation', widget=forms.PasswordInput,
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = ['email']
|
||||||
|
|
||||||
|
def clean_password2(self):
|
||||||
|
# Check that the two password entries match
|
||||||
|
password1 = self.cleaned_data.get('password1')
|
||||||
|
password2 = self.cleaned_data.get('password2')
|
||||||
|
if password1 and password2 and password1 != password2:
|
||||||
|
raise forms.ValidationError("Passwords don't match")
|
||||||
|
return password2
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
# Save the provided password in hashed format
|
||||||
|
user = super(UserCreationForm, self).save(commit=False)
|
||||||
|
user.set_password(self.cleaned_data['password1'])
|
||||||
|
if commit:
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
class UserChangeForm(forms.ModelForm):
|
||||||
|
"""A form for updating users. Includes all the fields on
|
||||||
|
the user, but replaces the password field with admin's
|
||||||
|
password hash display field.
|
||||||
|
|
||||||
|
"""
|
||||||
|
password = ReadOnlyPasswordHashField(
|
||||||
|
label=_("Password"),
|
||||||
|
help_text=_("Raw passwords are not stored, so there is no way to see "
|
||||||
|
"this user's password, but you can change the password "
|
||||||
|
"using <a href=\"password/\">this form</a>."))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.User
|
||||||
|
fields = [
|
||||||
|
'email', 'password', 'is_active', 'is_superuser'
|
||||||
|
]
|
||||||
|
|
||||||
|
def clean_password(self):
|
||||||
|
# Regardless of what the user provides, return the initial value.
|
||||||
|
# This is done here, rather than on the field, because the
|
||||||
|
# field does not have access to the initial value
|
||||||
|
return self.initial['password']
|
43
sandbox/sandbox/apps/user/migrations/0001_initial.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.1 on 2017-05-31 12:22
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0008_alter_user_username_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=100, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=100, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
sandbox/sandbox/apps/user/migrations/__init__.py
Normal file
51
sandbox/sandbox/apps/user/models.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from django.contrib.auth.models import (
|
||||||
|
AbstractBaseUser, PermissionsMixin, UserManager)
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.db import connections, models
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class User(AbstractBaseUser, PermissionsMixin):
|
||||||
|
"""Cusomtized version of the default `AbstractUser` from Django.
|
||||||
|
|
||||||
|
"""
|
||||||
|
first_name = models.CharField(_('first name'), max_length=100, blank=True)
|
||||||
|
last_name = models.CharField(_('last name'), max_length=100, blank=True)
|
||||||
|
email = models.EmailField(_('email address'), blank=True, unique=True)
|
||||||
|
is_staff = models.BooleanField(
|
||||||
|
_('staff status'), default=False,
|
||||||
|
help_text=_('Designates whether the user can log into this admin '
|
||||||
|
'site.'))
|
||||||
|
is_active = models.BooleanField(
|
||||||
|
_('active'), default=True,
|
||||||
|
help_text=_('Designates whether this user should be treated as '
|
||||||
|
'active. Unselect this instead of deleting accounts.'))
|
||||||
|
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
|
||||||
|
|
||||||
|
objects = UserManager()
|
||||||
|
|
||||||
|
USERNAME_FIELD = 'email'
|
||||||
|
REQUIRED_FIELDS = []
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('user')
|
||||||
|
verbose_name_plural = _('users')
|
||||||
|
|
||||||
|
def get_full_name(self):
|
||||||
|
"""
|
||||||
|
Returns the first_name plus the last_name, with a space in between.
|
||||||
|
"""
|
||||||
|
full_name = '%s %s' % (self.first_name, self.last_name)
|
||||||
|
return full_name.strip()
|
||||||
|
|
||||||
|
def get_short_name(self):
|
||||||
|
"Returns the short name for the user."
|
||||||
|
return self.first_name
|
||||||
|
|
||||||
|
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Sends an email to this User.
|
||||||
|
"""
|
||||||
|
send_mail(subject, message, from_email, [self.email], **kwargs)
|
162
sandbox/sandbox/settings.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
Django settings for sandbox project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 1.11.1.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
|
import os
|
||||||
|
|
||||||
|
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||||
|
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = '^anfvx$i7%wts8j=7u1h5ua$w6c76*333(@h)rrjlak1c&x0r+'
|
||||||
|
|
||||||
|
|
||||||
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'wagtail.wagtailforms',
|
||||||
|
'wagtail.wagtailredirects',
|
||||||
|
'wagtail.wagtailembeds',
|
||||||
|
'wagtail.wagtailsites',
|
||||||
|
'wagtail.wagtailusers',
|
||||||
|
'wagtail.wagtailsnippets',
|
||||||
|
'wagtail.wagtaildocs',
|
||||||
|
'wagtail.wagtailimages',
|
||||||
|
'wagtail.wagtailsearch',
|
||||||
|
'wagtail.wagtailadmin',
|
||||||
|
'wagtail.wagtailcore',
|
||||||
|
'wagtail.contrib.modeladmin',
|
||||||
|
|
||||||
|
'wagtailfontawesome',
|
||||||
|
'modelcluster',
|
||||||
|
'taggit',
|
||||||
|
'debug_toolbar',
|
||||||
|
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
|
||||||
|
'wagtail_personalisation',
|
||||||
|
|
||||||
|
'sandbox.apps.home',
|
||||||
|
'sandbox.apps.search',
|
||||||
|
'sandbox.apps.user',
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
|
||||||
|
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||||
|
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'sandbox.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [
|
||||||
|
os.path.join(PROJECT_DIR, 'templates'),
|
||||||
|
],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'sandbox.wsgi.application'
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'user.User'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_L10N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||||
|
|
||||||
|
STATICFILES_FINDERS = [
|
||||||
|
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||||
|
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||||
|
]
|
||||||
|
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
os.path.join(PROJECT_DIR, 'static'),
|
||||||
|
]
|
||||||
|
|
||||||
|
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
|
||||||
|
|
||||||
|
# Wagtail settings
|
||||||
|
|
||||||
|
WAGTAIL_SITE_NAME = "sandbox"
|
||||||
|
|
||||||
|
# Base URL to use when referring to full URLs within the Wagtail admin backend -
|
||||||
|
# e.g. in notification emails. Don't include '/admin' or a trailing slash
|
||||||
|
BASE_URL = 'http://example.com'
|
||||||
|
|
||||||
|
|
||||||
|
INTERNAL_IPS = ['127.0.0.1']
|
0
sandbox/sandbox/static/css/sandbox.css
Normal file
0
sandbox/sandbox/static/js/sandbox.js
Normal file
9
sandbox/sandbox/templates/404.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block body_class %}template-404{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Page not found</h1>
|
||||||
|
|
||||||
|
<h2>Sorry, this page could not be found.</h2>
|
||||||
|
{% endblock %}
|
17
sandbox/sandbox/templates/500.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||||
|
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||||
|
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||||
|
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title>Internal server error</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Internal server error</h1>
|
||||||
|
|
||||||
|
<h2>Sorry, there seems to be an error. Please try again soon.</h2>
|
||||||
|
</body>
|
||||||
|
</html>
|
44
sandbox/sandbox/templates/base.html
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{% load static wagtailuserbar %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
|
||||||
|
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
|
||||||
|
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
|
||||||
|
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
|
<title>
|
||||||
|
{% block title %}
|
||||||
|
{% if self.seo_title %}{{ self.seo_title }}{% else %}{{ self.title }}{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block title_suffix %}
|
||||||
|
{% with self.get_site.site_name as site_name %}
|
||||||
|
{% if site_name %}- {{ site_name }}{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endblock %}
|
||||||
|
</title>
|
||||||
|
<meta name="description" content="" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
|
{# Global stylesheets #}
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/sandbox.css' %}">
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
{# Override this in templates to add extra stylesheets #}
|
||||||
|
{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="{% block body_class %}{% endblock %}">
|
||||||
|
{% wagtailuserbar %}
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
{# Global javascript #}
|
||||||
|
<script type="text/javascript" src="{% static 'js/sandbox.js' %}"></script>
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
{# Override this in templates to add extra javascript #}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
42
sandbox/sandbox/urls.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import debug_toolbar
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls import include, url
|
||||||
|
from django.contrib import admin
|
||||||
|
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
||||||
|
from wagtail.wagtailcore import urls as wagtail_urls
|
||||||
|
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
||||||
|
|
||||||
|
from sandbox.apps.search import views as search_views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
|
|
||||||
|
url(r'^cms/', include(wagtailadmin_urls)),
|
||||||
|
url(r'^documents/', include(wagtaildocs_urls)),
|
||||||
|
|
||||||
|
url(r'^search/$', search_views.search, name='search'),
|
||||||
|
|
||||||
|
# For anything not caught by a more specific rule above, hand over to
|
||||||
|
# Wagtail's page serving mechanism. This should be the last pattern in
|
||||||
|
# the list:
|
||||||
|
url(r'', include(wagtail_urls)),
|
||||||
|
|
||||||
|
# Alternatively, if you want Wagtail pages to be served from a subpath
|
||||||
|
# of your site, rather than the site root:
|
||||||
|
# url(r'^pages/', include(wagtail_urls)),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
|
|
||||||
|
# Serve static and media files from development server
|
||||||
|
urlpatterns += staticfiles_urlpatterns()
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||||
|
] + urlpatterns
|
18
sandbox/sandbox/wsgi.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for sandbox project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings")
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
BIN
screenshot.png
Normal file
After Width: | Height: | Size: 98 KiB |
27
setup.cfg
@ -1,8 +1,31 @@
|
|||||||
[tool:pytest]
|
[tool:pytest]
|
||||||
DJANGO_SETTINGS_MODULE = tests.sandbox.settings
|
DJANGO_SETTINGS_MODULE = tests.settings
|
||||||
norecursedirs = .tox .git
|
minversion = 3.0
|
||||||
|
strict = true
|
||||||
|
django_find_project = false
|
||||||
|
testpaths = tests
|
||||||
|
python_paths = .
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
ignore=E731
|
ignore=E731
|
||||||
|
max-line-length = 120
|
||||||
exclude=
|
exclude=
|
||||||
src/**/migrations/*.py
|
src/**/migrations/*.py
|
||||||
|
|
||||||
|
[wheel]
|
||||||
|
universal = 1
|
||||||
|
|
||||||
|
[coverage:run]
|
||||||
|
omit =
|
||||||
|
src/**/migrations/*.py
|
||||||
|
|
||||||
|
|
||||||
|
[bumpversion]
|
||||||
|
current_version = 0.9.0
|
||||||
|
commit = true
|
||||||
|
tag = true
|
||||||
|
tag_name = {new_version}
|
||||||
|
|
||||||
|
[bumpversion:file:setup.py]
|
||||||
|
|
||||||
|
[bumpversion:file:docs/conf.py]
|
||||||
|
44
setup.py
@ -1,50 +1,70 @@
|
|||||||
|
import re
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'django-polymorphic==1.0.2',
|
'wagtail>=1.10,<1.11',
|
||||||
'wagtail>=1.7',
|
'user-agents>=1.0.1',
|
||||||
|
'wagtailfontawesome>=1.0.6',
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
'pytest==3.0.4',
|
'factory_boy==2.8.1',
|
||||||
'pytest-cov==2.4.0',
|
'flake8',
|
||||||
'pytest-django==3.0.0',
|
'flake8-blind-except',
|
||||||
'pytest-sugar==0.7.1',
|
'flake8-debugger',
|
||||||
|
'flake8-imports',
|
||||||
'freezegun==0.3.8',
|
'freezegun==0.3.8',
|
||||||
'factory_boy==2.7.0',
|
'pytest-cov==2.4.0',
|
||||||
|
'pytest-django==3.1.2',
|
||||||
|
'pytest-sugar==0.7.1',
|
||||||
|
'pytest==3.1.0',
|
||||||
|
'wagtail_factories==0.3.0',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
docs_require = [
|
||||||
|
'sphinx>=1.4.0',
|
||||||
|
]
|
||||||
|
|
||||||
|
with open('README.rst') as fh:
|
||||||
|
long_description = re.sub(
|
||||||
|
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='wagtail-personalisation',
|
name='wagtail-personalisation',
|
||||||
version='0.1.0',
|
version='0.9.0',
|
||||||
description='A Wagtail add-on for showing personalized content',
|
description='A Wagtail add-on for showing personalized content',
|
||||||
author='Lab Digital BV',
|
author='Lab Digital BV',
|
||||||
author_email='b.besemer@labdigital.nl',
|
author_email='opensource@labdigital.nl',
|
||||||
url='http://labdigital.nl',
|
url='http://labdigital.nl',
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
extras_require={
|
extras_require={
|
||||||
|
'docs': docs_require,
|
||||||
'test': tests_require,
|
'test': tests_require,
|
||||||
},
|
},
|
||||||
packages=find_packages('src'),
|
packages=find_packages('src'),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
license='BSD',
|
license='MIT',
|
||||||
long_description=open('README.rst').read(),
|
long_description=long_description,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 2 - Pre-Alpha',
|
'Development Status :: 2 - Pre-Alpha',
|
||||||
'Environment :: Web Environment',
|
'Environment :: Web Environment',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python :: 2',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.4',
|
'Programming Language :: Python :: 3.4',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Framework :: Django :: 1.8',
|
'Framework :: Django :: 1.8',
|
||||||
'Framework :: Django :: 1.9',
|
'Framework :: Django :: 1.9',
|
||||||
'Framework :: Django :: 1.10',
|
'Framework :: Django :: 1.10',
|
||||||
|
'Framework :: Django :: 1.11',
|
||||||
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -1,34 +0,0 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
from personalisation import models
|
|
||||||
|
|
||||||
|
|
||||||
class TimeRuleAdminInline(admin.TabularInline):
|
|
||||||
"""Inline the Time Rule into the administration interface for segments"""
|
|
||||||
model = models.TimeRule
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class ReferralRuleAdminInline(admin.TabularInline):
|
|
||||||
"""Inline the Referral Rule into the
|
|
||||||
administration interface for segments"""
|
|
||||||
model = models.ReferralRule
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class VisitCountRuleAdminInline(admin.TabularInline):
|
|
||||||
"""Inline the Visit Count Rule into the
|
|
||||||
administration interface for segments"""
|
|
||||||
model = models.VisitCountRule
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
|
|
||||||
class SegmentAdmin(admin.ModelAdmin):
|
|
||||||
"""Add the inlines to the Segment admin interface"""
|
|
||||||
inlines = (TimeRuleAdminInline,
|
|
||||||
ReferralRuleAdminInline, VisitCountRuleAdminInline)
|
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(models.Segment, SegmentAdmin)
|
|
@ -1,15 +0,0 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from django.conf.urls import url
|
|
||||||
|
|
||||||
from personalisation import views
|
|
||||||
|
|
||||||
app_name = 'segment'
|
|
||||||
urlpatterns = [
|
|
||||||
url(r'^segment/(?P<segment_id>[0-9]+)/enable/$', views.enable,
|
|
||||||
name='enable'),
|
|
||||||
url(r'^segment/(?P<segment_id>[0-9]+)/disable/$', views.disable,
|
|
||||||
name='disable'),
|
|
||||||
url(r'^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$',
|
|
||||||
views.copy_page_view, name='copy_page')
|
|
||||||
]
|
|
@ -1,333 +0,0 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
import re
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from django.db import models
|
|
||||||
from django.db.models.signals import pre_save
|
|
||||||
from django.template.defaultfilters import slugify
|
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
|
||||||
from django.utils.functional import cached_property
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from modelcluster.fields import ParentalKey
|
|
||||||
from modelcluster.models import ClusterableModel
|
|
||||||
from wagtail.utils.decorators import cached_classmethod
|
|
||||||
from wagtail.wagtailadmin.edit_handlers import (
|
|
||||||
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel, ObjectList,
|
|
||||||
PageChooserPanel, TabbedInterface)
|
|
||||||
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
|
|
||||||
from wagtail.wagtailcore.models import Page
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class AbstractBaseRule(models.Model):
|
|
||||||
"""Base for creating rules to segment users with"""
|
|
||||||
segment = ParentalKey(
|
|
||||||
'personalisation.Segment',
|
|
||||||
related_name="%(app_label)s_%(class)s_related",
|
|
||||||
related_query_name="%(app_label)s_%(class)ss"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_user(self):
|
|
||||||
"""Test if the user matches this rule"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "Abstract segmentation rule"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
abstract = True
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class TimeRule(AbstractBaseRule):
|
|
||||||
"""Time rule to segment users based on a start and end time"""
|
|
||||||
start_time = models.TimeField(_("Starting time"))
|
|
||||||
end_time = models.TimeField(_("Ending time"))
|
|
||||||
|
|
||||||
panels = [
|
|
||||||
FieldRowPanel([
|
|
||||||
FieldPanel('start_time'),
|
|
||||||
FieldPanel('end_time'),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(TimeRule, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def test_user(self, request=None):
|
|
||||||
current_time = datetime.now().time()
|
|
||||||
starting_time = self.start_time
|
|
||||||
ending_time = self.end_time
|
|
||||||
|
|
||||||
return starting_time <= current_time <= ending_time
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'Time Rule'
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class ReferralRule(AbstractBaseRule):
|
|
||||||
"""Referral rule to segment users based on a regex test"""
|
|
||||||
regex_string = models.TextField(
|
|
||||||
_("Regex string to match the referer with"))
|
|
||||||
|
|
||||||
panels = [
|
|
||||||
FieldPanel('regex_string'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(ReferralRule, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def test_user(self, request):
|
|
||||||
pattern = re.compile(self.regex_string)
|
|
||||||
|
|
||||||
if 'HTTP_REFERER' in request.META:
|
|
||||||
referer = request.META['HTTP_REFERER']
|
|
||||||
if pattern.search(referer):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'Referral Rule'
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class VisitCountRule(AbstractBaseRule):
|
|
||||||
"""Visit count rule to segment users based on amount of visits"""
|
|
||||||
OPERATOR_CHOICES = (
|
|
||||||
('more_than', _("More than")),
|
|
||||||
('less_than', _("Less than")),
|
|
||||||
('equal_to', _("Equal to")),
|
|
||||||
)
|
|
||||||
operator = models.CharField(max_length=20,
|
|
||||||
choices=OPERATOR_CHOICES, default="more_than")
|
|
||||||
count = models.PositiveSmallIntegerField(default=0, null=True)
|
|
||||||
counted_page = models.ForeignKey(
|
|
||||||
'wagtailcore.Page',
|
|
||||||
null=False,
|
|
||||||
blank=False,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='+',
|
|
||||||
)
|
|
||||||
|
|
||||||
panels = [
|
|
||||||
PageChooserPanel('counted_page'),
|
|
||||||
FieldRowPanel([
|
|
||||||
FieldPanel('operator'),
|
|
||||||
FieldPanel('count'),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(VisitCountRule, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def test_user(self, request):
|
|
||||||
operator = self.operator
|
|
||||||
segment_count = self.count
|
|
||||||
|
|
||||||
def get_visit_count(request):
|
|
||||||
"""Search through the sessions to get the page visit count
|
|
||||||
corresponding to the request."""
|
|
||||||
for page in request.session['visit_count']:
|
|
||||||
if page['path'] == request.path:
|
|
||||||
return page['count']
|
|
||||||
|
|
||||||
visit_count = get_visit_count(request)
|
|
||||||
|
|
||||||
if visit_count and operator == "more_than":
|
|
||||||
if visit_count > segment_count:
|
|
||||||
return True
|
|
||||||
elif visit_count and operator == "less_than":
|
|
||||||
if visit_count < segment_count:
|
|
||||||
return True
|
|
||||||
elif visit_count and operator == "equal_to":
|
|
||||||
if visit_count == segment_count:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'Visit count Rule'
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class QueryRule(AbstractBaseRule):
|
|
||||||
"""Query rule to segment users based on matching queries"""
|
|
||||||
parameter = models.SlugField(_("The query parameter to search for"),
|
|
||||||
max_length=20)
|
|
||||||
value = models.SlugField(_("The value of the parameter to match"),
|
|
||||||
max_length=20)
|
|
||||||
|
|
||||||
panels = [
|
|
||||||
FieldPanel('parameter'),
|
|
||||||
FieldPanel('value'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(QueryRule, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def test_user(self, request):
|
|
||||||
parameter = self.parameter
|
|
||||||
value = self.value
|
|
||||||
|
|
||||||
req_value = request.GET.get(parameter, '')
|
|
||||||
if req_value == value:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return 'Query Rule'
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
|
||||||
class Segment(ClusterableModel):
|
|
||||||
"""Model for a new segment"""
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
create_date = models.DateTimeField(auto_now_add=True)
|
|
||||||
edit_date = models.DateTimeField(auto_now=True)
|
|
||||||
enable_date = models.DateTimeField(null=True, editable=False)
|
|
||||||
disable_date = models.DateTimeField(null=True, editable=False)
|
|
||||||
visit_count = models.PositiveIntegerField(default=0, editable=False)
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
('enabled', 'Enabled'),
|
|
||||||
('disabled', 'Disabled'),
|
|
||||||
)
|
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES,
|
|
||||||
default="enabled")
|
|
||||||
persistent = models.BooleanField(
|
|
||||||
default=False, help_text=_("Should the segment persist between visits?"))
|
|
||||||
|
|
||||||
panels = [
|
|
||||||
MultiFieldPanel([
|
|
||||||
FieldPanel('name', classname="title"),
|
|
||||||
FieldRowPanel([
|
|
||||||
FieldPanel('status'),
|
|
||||||
FieldPanel('persistent'),
|
|
||||||
]),
|
|
||||||
], heading="Segment"),
|
|
||||||
|
|
||||||
MultiFieldPanel([
|
|
||||||
InlinePanel(
|
|
||||||
"{}_related".format(rule._meta.db_table),
|
|
||||||
label=rule.__str__,
|
|
||||||
min_num=0,
|
|
||||||
max_num=1,
|
|
||||||
) for rule in AbstractBaseRule.__subclasses__()
|
|
||||||
], heading="Rules"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
def encoded_name(self):
|
|
||||||
"""Returns a string with a slug for the segment"""
|
|
||||||
return slugify(self.name.lower())
|
|
||||||
|
|
||||||
|
|
||||||
def check_status_change(sender, instance, *args, **kwargs):
|
|
||||||
"""Check if the status has changed. Alter dates accordingly."""
|
|
||||||
try:
|
|
||||||
original_status = sender.objects.get(pk=instance.id).status
|
|
||||||
except sender.DoesNotExist:
|
|
||||||
original_status = ""
|
|
||||||
|
|
||||||
if original_status != instance.status:
|
|
||||||
if instance.status == "enabled":
|
|
||||||
instance.enable_date = timezone.now()
|
|
||||||
instance.visit_count = 0
|
|
||||||
return instance
|
|
||||||
if instance.status == "disabled":
|
|
||||||
instance.disable_date = timezone.now()
|
|
||||||
|
|
||||||
|
|
||||||
pre_save.connect(check_status_change, sender=Segment)
|
|
||||||
|
|
||||||
|
|
||||||
class AdminPersonalisablePageForm(WagtailAdminPageForm):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(AdminPersonalisablePageForm, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def save(self, commit=True):
|
|
||||||
page = super(AdminPersonalisablePageForm, self).save(commit=False)
|
|
||||||
|
|
||||||
if page.segment:
|
|
||||||
segment = page.segment
|
|
||||||
slug = "{}-{}".format(page.slug, segment.encoded_name())
|
|
||||||
title = "{} ({})".format(page.title, segment.name)
|
|
||||||
update_attrs = {
|
|
||||||
'title': title,
|
|
||||||
'slug': slug,
|
|
||||||
'segment': segment,
|
|
||||||
'live': False,
|
|
||||||
'canonical_page': page,
|
|
||||||
'is_segmented': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
if page.is_segmented:
|
|
||||||
slug = "{}-{}".format(
|
|
||||||
page.canonical_page.slug, segment.encoded_name())
|
|
||||||
title = "{} ({})".format(
|
|
||||||
page.canonical_page.title, segment.name)
|
|
||||||
page.slug = slug
|
|
||||||
page.title = title
|
|
||||||
page.save()
|
|
||||||
return page
|
|
||||||
else:
|
|
||||||
new_page = page.copy(
|
|
||||||
update_attrs=update_attrs, copy_revisions=False)
|
|
||||||
return new_page
|
|
||||||
|
|
||||||
return page
|
|
||||||
|
|
||||||
|
|
||||||
class PersonalisablePage(Page):
|
|
||||||
canonical_page = models.ForeignKey(
|
|
||||||
'self', related_name='variations', on_delete=models.SET_NULL,
|
|
||||||
blank=True, null=True
|
|
||||||
)
|
|
||||||
segment = models.ForeignKey(
|
|
||||||
Segment, related_name='segments', on_delete=models.PROTECT,
|
|
||||||
blank=True, null=True
|
|
||||||
)
|
|
||||||
is_segmented = models.BooleanField(default=False)
|
|
||||||
|
|
||||||
variation_panels = [
|
|
||||||
MultiFieldPanel([
|
|
||||||
FieldPanel('segment'),
|
|
||||||
PageChooserPanel('canonical_page', page_type=None),
|
|
||||||
])
|
|
||||||
]
|
|
||||||
|
|
||||||
base_form_class = AdminPersonalisablePageForm
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{}".format(self.title)
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def has_variations(self):
|
|
||||||
return self.variations.exists()
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def is_canonical(self):
|
|
||||||
return not self.canonical_page and self.has_variations
|
|
||||||
|
|
||||||
|
|
||||||
@cached_classmethod
|
|
||||||
def get_edit_handler(cls):
|
|
||||||
tabs = []
|
|
||||||
if cls.content_panels:
|
|
||||||
tabs.append(ObjectList(cls.content_panels, heading=_("Content")))
|
|
||||||
if cls.variation_panels:
|
|
||||||
tabs.append(ObjectList(cls.variation_panels, heading=_("Variations")))
|
|
||||||
if cls.promote_panels:
|
|
||||||
tabs.append(ObjectList(cls.promote_panels, heading=_("Promote")))
|
|
||||||
if cls.settings_panels:
|
|
||||||
tabs.append(ObjectList(cls.settings_panels, heading=_("Settings"),
|
|
||||||
classname='settings'))
|
|
||||||
|
|
||||||
edit_handler = TabbedInterface(tabs, base_form_class=cls.base_form_class)
|
|
||||||
return edit_handler.bind_to_model(cls)
|
|
||||||
|
|
||||||
|
|
||||||
PersonalisablePage.get_edit_handler = get_edit_handler
|
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,117 +0,0 @@
|
|||||||
{% extends "modeladmin/index.html" %}
|
|
||||||
{% load i18n l10n staticfiles modeladmin_tags personalisation_filters %}
|
|
||||||
|
|
||||||
{% block content_main %}
|
|
||||||
<div>
|
|
||||||
<div class="row">
|
|
||||||
{% block content_cols %}
|
|
||||||
|
|
||||||
{% block filters %}
|
|
||||||
{% if view.has_filters and all_count %}
|
|
||||||
<div class="changelist-filter col3">
|
|
||||||
<h2>{% trans 'Filter' %}</h2>
|
|
||||||
{% for spec in view.filter_specs %}{% admin_list_filter view spec %}{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{% block result_list %}
|
|
||||||
<div class="nice-padding block_container">
|
|
||||||
{% if all_count %}
|
|
||||||
{% for segment in object_list %}
|
|
||||||
<div class="block" onclick="location.href = 'edit/{{ segment.pk }}'">
|
|
||||||
<h2>{{ segment }}</h2>
|
|
||||||
<div class="inspect_container">
|
|
||||||
<ul class="inspect segment_stats">
|
|
||||||
<li class="visit_stat">
|
|
||||||
{% trans "This segmented has been visited" %}
|
|
||||||
<span>{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
|
|
||||||
</li>
|
|
||||||
<li class="days_stat">
|
|
||||||
{% trans "This segment has been active for" %}
|
|
||||||
<span>{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<ul class="inspect segment_rules">
|
|
||||||
{% for rule in segment.personalisation_timerule_related.all %}
|
|
||||||
<li class="time_rule">
|
|
||||||
These users visit between
|
|
||||||
<span>{{ rule.start_time }} and {{ rule.end_time }}</span>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for rule in segment.personalisation_referralrule_related.all %}
|
|
||||||
<li class="referral_rule">
|
|
||||||
These visits originate from
|
|
||||||
<pre>{{ rule.regex_string }}</pre>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for rule in segment.personalisation_visitcountrule_related.all %}
|
|
||||||
<li class="visit_count_rule">
|
|
||||||
These users visited {{ rule.counted_page }}
|
|
||||||
<span>{{ rule.get_operator_display }} {{ rule.count }} times</span>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% for rule in segment.personalisation_queryrule_related.all %}
|
|
||||||
<li class="query_rule">
|
|
||||||
These users used a url with the query
|
|
||||||
<pre>?{{ rule.parameter }}={{ rule.value }}</pre>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<li class="persistent_state {{ segment.persistent|yesno:"persistent,fleeting" }}">
|
|
||||||
{% trans "The persistence of this segment is" %}
|
|
||||||
{% if segment.persistent %}
|
|
||||||
<span title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
|
|
||||||
{% else %}
|
|
||||||
<span title="{% trans "This segment is reevaluated on every visit" %}">{% trans "Fleeting" %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if user_can_create %}
|
|
||||||
<ul class="block_actions">
|
|
||||||
{% if segment.status == "disabled" %}
|
|
||||||
<li><a href="{% url 'segment:enable' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
|
|
||||||
{% elif segment.status == "enabled" %}
|
|
||||||
<li><a href="{% url 'segment:disable' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
|
||||||
{% endif %}
|
|
||||||
<li><a href="edit/{{ segment.pk }}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if user_can_create %}
|
|
||||||
{% blocktrans with url=view.create_url name=view.verbose_name %}
|
|
||||||
<a class="block suggestion" href="{{ url }}">
|
|
||||||
<span class="suggestive_text">Add a new {{name}}</span>
|
|
||||||
</a>
|
|
||||||
{% endblocktrans %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% block pagination %}
|
|
||||||
{% if paginator.num_pages > 1 %}
|
|
||||||
<div class="pagination {% if view.has_filters and all_count %}col9{% else %}col12{% endif %}">
|
|
||||||
<p>{% blocktrans with page_obj.number as current_page and paginator.num_pages as num_pages %}Page {{ current_page }} of {{ num_pages }}.{% endblocktrans %}</p>
|
|
||||||
<ul>
|
|
||||||
{% pagination_link_previous page_obj view %}
|
|
||||||
{% pagination_link_next page_obj view %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,19 +0,0 @@
|
|||||||
from django.template import Library
|
|
||||||
from django.utils import timezone
|
|
||||||
|
|
||||||
register = Library()
|
|
||||||
|
|
||||||
|
|
||||||
@register.filter(name='days_since')
|
|
||||||
def active_days(enable_date, disable_date):
|
|
||||||
"""Returns the number of days the segment has been active"""
|
|
||||||
if enable_date is not None:
|
|
||||||
if disable_date is None or disable_date <= enable_date:
|
|
||||||
# There is no disable date, or it is not relevant.
|
|
||||||
delta = timezone.now() - enable_date
|
|
||||||
return delta.days
|
|
||||||
if disable_date > enable_date:
|
|
||||||
# There is a disable date and it is relevant.
|
|
||||||
delta = disable_date - enable_date
|
|
||||||
return delta.days
|
|
||||||
return 0
|
|
@ -1,5 +0,0 @@
|
|||||||
def impersonate_other_page(page, other_page):
|
|
||||||
page.path = other_page.path
|
|
||||||
page.depth = other_page.depth
|
|
||||||
page.url_path = other_page.url_path
|
|
||||||
page.title = other_page.title
|
|
@ -1,46 +0,0 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from django.http import HttpResponseRedirect
|
|
||||||
from django.shortcuts import get_object_or_404, reverse
|
|
||||||
from personalisation.models import PersonalisablePage, Segment
|
|
||||||
|
|
||||||
|
|
||||||
def enable(request, segment_id):
|
|
||||||
"""Enable the selected segment"""
|
|
||||||
segment = get_object_or_404(Segment, pk=segment_id)
|
|
||||||
segment.status = 'enabled'
|
|
||||||
segment.save()
|
|
||||||
|
|
||||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
def disable(request, segment_id):
|
|
||||||
"""Disable the selected segment"""
|
|
||||||
segment = get_object_or_404(Segment, pk=segment_id)
|
|
||||||
segment.status = 'disabled'
|
|
||||||
segment.save()
|
|
||||||
|
|
||||||
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
|
|
||||||
|
|
||||||
|
|
||||||
def copy_page_view(request, page_id, segment_id):
|
|
||||||
"""Copy page with selected segment"""
|
|
||||||
segment = get_object_or_404(Segment, pk=segment_id)
|
|
||||||
page = get_object_or_404(PersonalisablePage, pk=page_id)
|
|
||||||
|
|
||||||
slug = "{}-{}".format(page.slug, segment.encoded_name())
|
|
||||||
title = "{} ({})".format(page.title, segment.name)
|
|
||||||
update_attrs = {
|
|
||||||
'title': title,
|
|
||||||
'slug': slug,
|
|
||||||
'segment': segment,
|
|
||||||
'live': False,
|
|
||||||
'canonical_page': page,
|
|
||||||
'is_segmented': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
new_page = page.copy(update_attrs=update_attrs, copy_revisions=False)
|
|
||||||
|
|
||||||
edit_url = reverse('wagtailadmin_pages:edit', args=[new_page.id])
|
|
||||||
|
|
||||||
return HttpResponseRedirect(edit_url)
|
|
@ -1,228 +0,0 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
from django.conf.urls import include, url
|
|
||||||
from django.shortcuts import reverse
|
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
|
||||||
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
|
||||||
from wagtail.wagtailadmin.widgets import (
|
|
||||||
Button, ButtonWithDropdownFromHook)
|
|
||||||
from wagtail.wagtailcore import hooks
|
|
||||||
|
|
||||||
from personalisation import admin_urls
|
|
||||||
from personalisation.models import (AbstractBaseRule, PersonalisablePage,
|
|
||||||
Segment)
|
|
||||||
from personalisation.utils import impersonate_other_page
|
|
||||||
|
|
||||||
logger = logging.getLogger()
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('register_admin_urls')
|
|
||||||
def register_admin_urls():
|
|
||||||
"""Adds the administration urls for the personalisation apps."""
|
|
||||||
return [
|
|
||||||
url(r'^personalisation/', include(
|
|
||||||
admin_urls,
|
|
||||||
app_name='personalisation',
|
|
||||||
namespace='personalisation')),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class SegmentModelAdmin(ModelAdmin):
|
|
||||||
"""The base model for the Segments administration interface."""
|
|
||||||
model = Segment
|
|
||||||
menu_icon = 'group'
|
|
||||||
add_to_settings_menu = False
|
|
||||||
list_display = ('status', 'name', 'create_date', 'edit_date')
|
|
||||||
index_view_extra_css = ['personalisation/segment/index.css']
|
|
||||||
form_view_extra_css = ['personalisation/segment/form.css']
|
|
||||||
|
|
||||||
|
|
||||||
modeladmin_register(SegmentModelAdmin)
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('before_serve_page')
|
|
||||||
def set_visit_count(page, request, serve_args, serve_kwargs):
|
|
||||||
if 'visit_count' not in request.session:
|
|
||||||
request.session['visit_count'] = []
|
|
||||||
|
|
||||||
# Update the page visit count
|
|
||||||
def create_new_counter(page, request):
|
|
||||||
"""Create a new counter dict and place it in session storage."""
|
|
||||||
countdict = {
|
|
||||||
"slug": page.slug,
|
|
||||||
"id": page.pk,
|
|
||||||
"path": request.path,
|
|
||||||
"count": 1,
|
|
||||||
}
|
|
||||||
request.session['visit_count'].append(countdict)
|
|
||||||
|
|
||||||
if len(request.session['visit_count']) > 0:
|
|
||||||
for index, counter in enumerate(request.session['visit_count']):
|
|
||||||
if counter['id'] == page.pk:
|
|
||||||
# Counter already exists. Increase the count value by 1.
|
|
||||||
newcount = counter['count'] + 1
|
|
||||||
request.session['visit_count'][index]['count'] = newcount
|
|
||||||
request.session.modified = True
|
|
||||||
else:
|
|
||||||
# Counter doesn't exist.
|
|
||||||
# Create a new counter with count value 1.
|
|
||||||
create_new_counter(page, request)
|
|
||||||
else:
|
|
||||||
# No counters exist. Create a new counter with count value 1.
|
|
||||||
create_new_counter(page, request)
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('before_serve_page')
|
|
||||||
def segment_user(page, request, serve_args, serve_kwargs):
|
|
||||||
if 'segments' not in request.session:
|
|
||||||
request.session['segments'] = []
|
|
||||||
|
|
||||||
current_segments = request.session['segments']
|
|
||||||
persistent_segments = Segment.objects.filter(persistent=True)
|
|
||||||
|
|
||||||
current_segments = [item for item in current_segments if any(seg.pk for seg in persistent_segments) == item['id']]
|
|
||||||
|
|
||||||
request.session['segments'] = current_segments
|
|
||||||
|
|
||||||
segments = Segment.objects.all().filter(status='enabled')
|
|
||||||
|
|
||||||
for segment in segments:
|
|
||||||
rules = AbstractBaseRule.__subclasses__()
|
|
||||||
segment_rules = []
|
|
||||||
for rule in rules:
|
|
||||||
queried_rules = rule.objects.filter(segment=segment)
|
|
||||||
for result in queried_rules:
|
|
||||||
segment_rules.append(result)
|
|
||||||
result = _test_rules(segment_rules, request)
|
|
||||||
|
|
||||||
if result:
|
|
||||||
_add_segment_to_user(segment, request)
|
|
||||||
|
|
||||||
if request.session['segments']:
|
|
||||||
logger.info("User has been added to the following segments: {}"
|
|
||||||
.format(request.session['segments']))
|
|
||||||
|
|
||||||
for seg in request.session['segments']:
|
|
||||||
segment = Segment.objects.get(pk=seg['id'])
|
|
||||||
segment.visit_count = segment.visit_count + 1
|
|
||||||
segment.save()
|
|
||||||
|
|
||||||
|
|
||||||
def _test_rules(rules, request):
|
|
||||||
"""Test whether the user matches a segment's rules'"""
|
|
||||||
if len(rules) > 0:
|
|
||||||
for rule in rules:
|
|
||||||
result = rule.test_user(request)
|
|
||||||
|
|
||||||
if result is False:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _add_segment_to_user(segment, request):
|
|
||||||
"""Save the segment in the user session"""
|
|
||||||
|
|
||||||
def check_if_segmented(segment):
|
|
||||||
"""Check if the user has been segmented"""
|
|
||||||
for seg in request.session['segments']:
|
|
||||||
if seg['encoded_name'] == segment.encoded_name():
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not check_if_segmented(segment):
|
|
||||||
segdict = {
|
|
||||||
"encoded_name": segment.encoded_name(),
|
|
||||||
"id": segment.pk,
|
|
||||||
"timestamp": int(time.time()),
|
|
||||||
"persistent": segment.persistent,
|
|
||||||
}
|
|
||||||
request.session['segments'].append(segdict)
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('before_serve_page')
|
|
||||||
def serve_variation(page, request, serve_args, serve_kwargs):
|
|
||||||
user_segments = []
|
|
||||||
|
|
||||||
for segment in request.session['segments']:
|
|
||||||
try:
|
|
||||||
user_segment = Segment.objects.get(pk=segment['id'],
|
|
||||||
status='enabled')
|
|
||||||
except Segment.DoesNotExist:
|
|
||||||
user_segment = None
|
|
||||||
if user_segment:
|
|
||||||
user_segments.append(user_segment)
|
|
||||||
|
|
||||||
if len(user_segments) > 0:
|
|
||||||
variations = _check_for_variations(user_segments, page)
|
|
||||||
|
|
||||||
if variations:
|
|
||||||
variation = variations[0]
|
|
||||||
|
|
||||||
impersonate_other_page(variation, page)
|
|
||||||
|
|
||||||
return variation.serve(request, *serve_args, **serve_kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _check_for_variations(segments, page):
|
|
||||||
for segment in segments:
|
|
||||||
page_class = page.__class__
|
|
||||||
if not any(item == PersonalisablePage for item in page_class.__bases__):
|
|
||||||
page_class = PersonalisablePage
|
|
||||||
|
|
||||||
variation = page_class.objects.filter(
|
|
||||||
canonical_page=page, segment=segment)
|
|
||||||
|
|
||||||
if variation:
|
|
||||||
return variation
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('register_page_listing_buttons')
|
|
||||||
def page_listing_variant_buttons(page, page_perms, is_parent=False):
|
|
||||||
personalisable_page = PersonalisablePage.objects.filter(pk=page.pk)
|
|
||||||
segments = Segment.objects.all()
|
|
||||||
|
|
||||||
if personalisable_page and len(segments) > 0 and not (any(item.segment for item in personalisable_page)):
|
|
||||||
yield ButtonWithDropdownFromHook(
|
|
||||||
_('Variants'),
|
|
||||||
hook_name='register_page_listing_variant_buttons',
|
|
||||||
page=page,
|
|
||||||
page_perms=page_perms,
|
|
||||||
is_parent=is_parent,
|
|
||||||
attrs={'target': '_blank', 'title': _('Create a new variant')}, priority=100)
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('register_page_listing_variant_buttons')
|
|
||||||
def page_listing_more_buttons(page, page_perms, is_parent=False):
|
|
||||||
segments = Segment.objects.all()
|
|
||||||
available_segments = [item for item in segments if not PersonalisablePage.objects.filter(segment=item, pk=page.pk)]
|
|
||||||
|
|
||||||
for segment in available_segments:
|
|
||||||
yield Button(segment.name,
|
|
||||||
reverse('segment:copy_page', args=[page.id, segment.id]),
|
|
||||||
attrs={"title": _('Create this variant')})
|
|
||||||
|
|
||||||
|
|
||||||
class SegmentSummaryPanel(object):
|
|
||||||
order = 500
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
segment_count = Segment.objects.count()
|
|
||||||
target_url = reverse('personalisation_segment_modeladmin_index')
|
|
||||||
title = _("Segments")
|
|
||||||
return mark_safe("""
|
|
||||||
<li class="icon icon-group">
|
|
||||||
<a href="{}"><span>{}</span>{}</a>
|
|
||||||
</li>""".format(target_url, segment_count, title))
|
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('construct_homepage_summary_items')
|
|
||||||
def add_segment_summary_panel(request, summary_items):
|
|
||||||
return summary_items.append(SegmentSummaryPanel())
|
|
1
src/wagtail_personalisation/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
default_app_config = 'wagtail_personalisation.config.WagtailPersonalisationConfig'
|
201
src/wagtail_personalisation/adapters.py
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import F
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
|
from wagtail_personalisation.models import Segment
|
||||||
|
from wagtail_personalisation.rules import AbstractBaseRule
|
||||||
|
from wagtail_personalisation.utils import create_segment_dictionary
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSegmentsAdapter(object):
|
||||||
|
"""Base segments adapter."""
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
"""Prepare the request session for segment storage.
|
||||||
|
|
||||||
|
:param request: The http request
|
||||||
|
:type request: django.http.HttpRequest
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""Prepare the adapter for segment storage."""
|
||||||
|
|
||||||
|
def get_segments(self):
|
||||||
|
"""Return the segments stored in the adapter storage."""
|
||||||
|
|
||||||
|
def get_segment_by_id(self):
|
||||||
|
"""Return a single segment stored in the adapter storage."""
|
||||||
|
|
||||||
|
def add(self):
|
||||||
|
"""Add a new segment to the adapter storage."""
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""Refresh the segments stored in the adapter storage."""
|
||||||
|
|
||||||
|
def _test_rules(self, rules, request, match_any=False):
|
||||||
|
"""Tests the provided rules to see if the request still belongs
|
||||||
|
to a segment.
|
||||||
|
:param rules: The rules to test for
|
||||||
|
:type rules: list of wagtail_personalisation.rules
|
||||||
|
:param request: The http request
|
||||||
|
:type request: django.http.HttpRequest
|
||||||
|
:param match_any: Whether all rules need to match, or any
|
||||||
|
:type match_any: bool
|
||||||
|
:returns: A boolean indicating the segment matches the request
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
if not rules:
|
||||||
|
return False
|
||||||
|
if match_any:
|
||||||
|
return any(rule.test_user(request) for rule in rules)
|
||||||
|
return all(rule.test_user(request) for rule in rules)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||||
|
"""Segment adapter that uses Django's session backend."""
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
super(SessionSegmentsAdapter, self).__init__(request)
|
||||||
|
self.request.session.setdefault('segments', [])
|
||||||
|
self._segment_cache = None
|
||||||
|
|
||||||
|
def get_segments(self):
|
||||||
|
"""Return the persistent segments stored in the request session.
|
||||||
|
|
||||||
|
:returns: The segments in the request session
|
||||||
|
:rtype: list of wagtail_personalisation.models.Segment or empty list
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self._segment_cache is not None:
|
||||||
|
return self._segment_cache
|
||||||
|
|
||||||
|
raw_segments = self.request.session['segments']
|
||||||
|
segment_ids = [segment['id'] for segment in raw_segments]
|
||||||
|
|
||||||
|
segments = (
|
||||||
|
Segment.objects
|
||||||
|
.enabled()
|
||||||
|
.filter(persistent=True)
|
||||||
|
.in_bulk(segment_ids))
|
||||||
|
|
||||||
|
retval = [segments[pk] for pk in segment_ids if pk in segments]
|
||||||
|
self._segment_cache = retval
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def set_segments(self, segments):
|
||||||
|
"""Set the currently active segments
|
||||||
|
|
||||||
|
:param segments: The segments to set for the current request
|
||||||
|
:type segments: list of wagtail_personalisation.models.Segment
|
||||||
|
|
||||||
|
"""
|
||||||
|
cache_segments = []
|
||||||
|
serialized_segments = []
|
||||||
|
segment_ids = set()
|
||||||
|
for segment in segments:
|
||||||
|
serialized = create_segment_dictionary(segment)
|
||||||
|
if serialized['id'] in segment_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cache_segments.append(segment)
|
||||||
|
serialized_segments.append(serialized)
|
||||||
|
segment_ids.add(segment.pk)
|
||||||
|
|
||||||
|
self.request.session['segments'] = serialized_segments
|
||||||
|
self._segment_cache = cache_segments
|
||||||
|
|
||||||
|
def get_segment_by_id(self, segment_id):
|
||||||
|
"""Find and return a single segment from the request session.
|
||||||
|
|
||||||
|
:param segment_id: The primary key of the segment
|
||||||
|
:type segment_id: int
|
||||||
|
:returns: The matching segment
|
||||||
|
:rtype: wagtail_personalisation.models.Segment or None
|
||||||
|
|
||||||
|
"""
|
||||||
|
for segment in self.get_segments():
|
||||||
|
if segment.pk == segment_id:
|
||||||
|
return segment
|
||||||
|
|
||||||
|
def add_page_visit(self, page):
|
||||||
|
"""Mark the page as visited by the user"""
|
||||||
|
visit_count = self.request.session.setdefault('visit_count', [])
|
||||||
|
page_visits = [visit for visit in visit_count if visit['id'] == page.pk]
|
||||||
|
|
||||||
|
if page_visits:
|
||||||
|
for page_visit in page_visits:
|
||||||
|
page_visit['count'] += 1
|
||||||
|
self.request.session.modified = True
|
||||||
|
else:
|
||||||
|
visit_count.append({
|
||||||
|
'slug': page.slug,
|
||||||
|
'id': page.pk,
|
||||||
|
'path': self.request.path,
|
||||||
|
'count': 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_visit_count(self, page=None):
|
||||||
|
"""Return the number of visits on the current request or given page"""
|
||||||
|
path = page.path if page else self.request.path
|
||||||
|
visit_count = self.request.session.setdefault('visit_count', [])
|
||||||
|
for visit in visit_count:
|
||||||
|
if visit['path'] == path:
|
||||||
|
return visit['count']
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def update_visit_count(self):
|
||||||
|
"""Update the visit count for all segments in the request session."""
|
||||||
|
segments = self.request.session['segments']
|
||||||
|
segment_pks = [s['id'] for s in segments]
|
||||||
|
|
||||||
|
# Update counts
|
||||||
|
(Segment.objects
|
||||||
|
.enabled()
|
||||||
|
.filter(pk__in=segment_pks)
|
||||||
|
.update(visit_count=F('visit_count') + 1))
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
"""Retrieve the request session segments and verify whether or not they
|
||||||
|
still apply to the requesting visitor.
|
||||||
|
|
||||||
|
"""
|
||||||
|
enabled_segments = Segment.objects.enabled()
|
||||||
|
rule_models = AbstractBaseRule.get_descendant_models()
|
||||||
|
|
||||||
|
current_segments = self.get_segments()
|
||||||
|
|
||||||
|
# Run tests on all remaining enabled segments to verify applicability.
|
||||||
|
additional_segments = []
|
||||||
|
for segment in enabled_segments:
|
||||||
|
segment_rules = []
|
||||||
|
for rule_model in rule_models:
|
||||||
|
segment_rules.extend(rule_model.objects.filter(segment=segment))
|
||||||
|
|
||||||
|
result = self._test_rules(segment_rules, self.request,
|
||||||
|
match_any=segment.match_any)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
additional_segments.append(segment)
|
||||||
|
|
||||||
|
self.set_segments(current_segments + additional_segments)
|
||||||
|
self.update_visit_count()
|
||||||
|
|
||||||
|
|
||||||
|
SEGMENT_ADAPTER_CLASS = import_string(getattr(
|
||||||
|
settings,
|
||||||
|
'PERSONALISATION_SEGMENTS_ADAPTER',
|
||||||
|
'wagtail_personalisation.adapters.SessionSegmentsAdapter'))
|
||||||
|
|
||||||
|
|
||||||
|
def get_segment_adapter(request):
|
||||||
|
"""Return the Segment Adapter for the given request"""
|
||||||
|
if not hasattr(request, 'segment_adapter'):
|
||||||
|
request.segment_adapter = SEGMENT_ADAPTER_CLASS(request)
|
||||||
|
return request.segment_adapter
|
46
src/wagtail_personalisation/admin.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from wagtail_personalisation import models, rules
|
||||||
|
|
||||||
|
|
||||||
|
class UserIsLoggedInRuleAdminInline(admin.TabularInline):
|
||||||
|
"""Inline the UserIsLoggedIn Rule into the
|
||||||
|
administration interface for segments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
model = rules.UserIsLoggedInRule
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRuleAdminInline(admin.TabularInline):
|
||||||
|
"""Inline the Time Rule into the
|
||||||
|
administration interface for segments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
model = rules.TimeRule
|
||||||
|
|
||||||
|
|
||||||
|
class ReferralRuleAdminInline(admin.TabularInline):
|
||||||
|
"""Inline the Referral Rule into the
|
||||||
|
administration interface for segments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
model = rules.ReferralRule
|
||||||
|
|
||||||
|
|
||||||
|
class VisitCountRuleAdminInline(admin.TabularInline):
|
||||||
|
"""Inline the Visit Count Rule into the
|
||||||
|
administration interface for segments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
model = rules.VisitCountRule
|
||||||
|
|
||||||
|
|
||||||
|
class SegmentAdmin(admin.ModelAdmin):
|
||||||
|
"""Add the inline models to the Segment admin interface."""
|
||||||
|
inlines = (UserIsLoggedInRuleAdminInline, TimeRuleAdminInline,
|
||||||
|
ReferralRuleAdminInline, VisitCountRuleAdminInline)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(models.Segment, SegmentAdmin)
|
16
src/wagtail_personalisation/admin_urls.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from wagtail_personalisation import views
|
||||||
|
|
||||||
|
app_name = 'segment'
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^segment/(?P<segment_id>[0-9]+)/toggle/$',
|
||||||
|
views.toggle, name='toggle'),
|
||||||
|
url(r'^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$',
|
||||||
|
views.copy_page_view, name='copy_page'),
|
||||||
|
url(r'^segment/toggle_segment_view/$',
|
||||||
|
views.toggle_segment_view, name='toggle_segment_view'),
|
||||||
|
]
|
44
src/wagtail_personalisation/blocks.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from wagtail.wagtailcore import blocks
|
||||||
|
|
||||||
|
from wagtail_personalisation.adapters import get_segment_adapter
|
||||||
|
from wagtail_personalisation.models import Segment
|
||||||
|
|
||||||
|
|
||||||
|
def list_segment_choices():
|
||||||
|
for pk, name in Segment.objects.values_list('pk', 'name'):
|
||||||
|
yield pk, name
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalisedStructBlock(blocks.StructBlock):
|
||||||
|
"""Struct block that allows personalisation per block."""
|
||||||
|
|
||||||
|
segment = blocks.ChoiceBlock(
|
||||||
|
choices=list_segment_choices,
|
||||||
|
required=False, label=_("Personalisation segment"),
|
||||||
|
help_text=_("Only show this content block for users in this segment"))
|
||||||
|
|
||||||
|
def render(self, value, context=None):
|
||||||
|
"""Only render this content block for users in this segment.
|
||||||
|
|
||||||
|
:param value: The value from the block
|
||||||
|
:type value: dict
|
||||||
|
:param context: The context containing the request
|
||||||
|
:type context: dict
|
||||||
|
:returns: The provided block if matched, otherwise an empty string
|
||||||
|
:rtype: blocks.StructBlock or empty str
|
||||||
|
|
||||||
|
"""
|
||||||
|
request = context['request']
|
||||||
|
adapter = get_segment_adapter(request)
|
||||||
|
user_segments = adapter.get_segments()
|
||||||
|
|
||||||
|
if value['segment']:
|
||||||
|
for segment in user_segments:
|
||||||
|
if segment.id == int(value['segment']):
|
||||||
|
return super(PersonalisedStructBlock, self).render(
|
||||||
|
value, context)
|
||||||
|
|
||||||
|
return ""
|
13
src/wagtail_personalisation/config.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class WagtailPersonalisationConfig(AppConfig):
|
||||||
|
label = 'wagtail_personalisation'
|
||||||
|
name = 'wagtail_personalisation'
|
||||||
|
verbose_name = _('Wagtail Personalisation')
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
from wagtail_personalisation import receivers
|
||||||
|
|
||||||
|
receivers.register()
|
352
src/wagtail_personalisation/locale/en/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
# Wagtail Personalisation english translation strings.
|
||||||
|
# Copyright (C) 2017 Lab Digital B.V.
|
||||||
|
# This file is distributed under the same license as the wagtail_personalisation package.
|
||||||
|
# Boris Besemer <b.besemer@labdigital.nl>, 2017.
|
||||||
|
#
|
||||||
|
#, fuzzy
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: wagtail_personalisation 0.1.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2017-05-31 09:30-0500\n"
|
||||||
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
"Language: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
#: blocks.py:20
|
||||||
|
msgid "Personalisation segment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: blocks.py:21
|
||||||
|
msgid "Only show this content block for users in this segment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: config.py:8
|
||||||
|
msgid "Wagtail Personalisation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:26
|
||||||
|
msgid "Enabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:27
|
||||||
|
msgid "Disabled"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:39
|
||||||
|
msgid "Should the segment persist between visits?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:42
|
||||||
|
msgid "Should the segment match all the rules or just one of them?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:60
|
||||||
|
msgid "Rules"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:167
|
||||||
|
msgid "Content"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:169
|
||||||
|
msgid "Variations"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:171
|
||||||
|
msgid "Promote"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: models.py:173
|
||||||
|
msgid "Settings"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:29 rules.py:48
|
||||||
|
msgid "Abstract segmentation rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:63
|
||||||
|
msgid "Starting time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:64
|
||||||
|
msgid "Ending time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:74
|
||||||
|
msgid "Time Rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:85
|
||||||
|
msgid "These users visit between"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:86
|
||||||
|
msgid "{} and {}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:103
|
||||||
|
msgid "Monday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:104
|
||||||
|
msgid "Tuesday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:105
|
||||||
|
msgid "Wednesday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:106
|
||||||
|
msgid "Thursday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:107
|
||||||
|
msgid "Friday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:108
|
||||||
|
msgid "Saturday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:109
|
||||||
|
msgid "Sunday"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:122
|
||||||
|
msgid "Day Rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:146
|
||||||
|
msgid "These users visit on"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:162
|
||||||
|
msgid "Regular expression to match the referrer"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:169
|
||||||
|
msgid "Referral Rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:182
|
||||||
|
msgid "These visits originate from"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:183 rules.py:366
|
||||||
|
msgid "{}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:202
|
||||||
|
msgid "More than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:203
|
||||||
|
msgid "Less than"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:204
|
||||||
|
msgid "Equal to"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:247
|
||||||
|
msgid "Visit count Rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:251
|
||||||
|
msgid "These users visited {}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:254
|
||||||
|
msgid "{} {} times"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:271
|
||||||
|
msgid "The query parameter to search for"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:273
|
||||||
|
msgid "The value of the parameter to match"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:282
|
||||||
|
msgid "Query Rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:293
|
||||||
|
msgid "These users used a URL with the query"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:294
|
||||||
|
msgid "?{}={}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:312
|
||||||
|
msgid "Mobile phone"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:313
|
||||||
|
msgid "Tablet"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:314
|
||||||
|
msgid "Desktop"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:323
|
||||||
|
msgid "Device Rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:354
|
||||||
|
msgid "Logged in Rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:360
|
||||||
|
msgid "Logged in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:362
|
||||||
|
msgid "Not logged in"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: rules.py:365
|
||||||
|
msgid "These visitors are"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/base.html:28
|
||||||
|
msgid "Switch view"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:14
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/index.html:14
|
||||||
|
msgid "Filter"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:30
|
||||||
|
msgid "This segment has been visited"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:31
|
||||||
|
msgid "time"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:34
|
||||||
|
msgid "This segment has been active for"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:35
|
||||||
|
msgid "day"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:43
|
||||||
|
msgid "The visitor must match"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:45
|
||||||
|
msgid "Any rule"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:47
|
||||||
|
msgid "All rules"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:52
|
||||||
|
msgid "The persistence of this segment is"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:54
|
||||||
|
msgid "This segment persists in between visits"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:54
|
||||||
|
msgid "Persistent"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:56
|
||||||
|
msgid "This segment is re-evaluated on every visit"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:56
|
||||||
|
msgid "Fleeting"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:76
|
||||||
|
msgid "Enable this segment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:78
|
||||||
|
msgid "Disable this segment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:80
|
||||||
|
msgid "Configure this segment"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:88
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" <a class=\"block suggestion\" href="
|
||||||
|
"\"%(url)s\">\n"
|
||||||
|
" <span class=\"suggestive_text\">Add "
|
||||||
|
"a new %(name)s</span>\n"
|
||||||
|
" </a>\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:101
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/index.html:45
|
||||||
|
#, python-format
|
||||||
|
msgid "Page %(current_page)s of %(num_pages)s."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/index.html:24
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"No %(name)s have been created yet. One of the following must be created "
|
||||||
|
"before you can add any %(name)s:"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/index.html:29
|
||||||
|
#, python-format
|
||||||
|
msgid "No %(name)s have been created yet."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: templates/modeladmin/wagtail_personalisation/segment/index.html:31
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"\n"
|
||||||
|
" Why not <a href=\"%(url)s\">add "
|
||||||
|
"one</a>?\n"
|
||||||
|
" "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: views.py:60
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{visits} visits"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: views.py:63
|
||||||
|
#, python-brace-format
|
||||||
|
msgid "{days} days"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: wagtail_hooks.py:121
|
||||||
|
msgid "Variants"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: wagtail_hooks.py:126
|
||||||
|
msgid "Create a new variant"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: wagtail_hooks.py:146
|
||||||
|
msgid "Create this variant"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: wagtail_hooks.py:159
|
||||||
|
msgid "Segments"
|
||||||
|
msgstr ""
|
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||||
('is_segmented', models.BooleanField(default=False)),
|
('is_segmented', models.BooleanField(default=False)),
|
||||||
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variations', to='personalisation.PersonalisablePage')),
|
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='wagtail_personalisation.PersonalisablePage')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
@ -60,7 +60,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('start_time', models.TimeField(verbose_name='Starting time')),
|
('start_time', models.TimeField(verbose_name='Starting time')),
|
||||||
('end_time', models.TimeField(verbose_name='Ending time')),
|
('end_time', models.TimeField(verbose_name='Ending time')),
|
||||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_timerule_related', related_query_name='personalisation_timerules', to='personalisation.Segment')),
|
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_timerule_related', related_query_name='wagtail_personalisation_timerules', to='wagtail_personalisation.Segment')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
@ -73,7 +73,7 @@ class Migration(migrations.Migration):
|
|||||||
('operator', models.CharField(choices=[('more_than', 'More than'), ('less_than', 'Less than'), ('equal_to', 'Equal to')], default='ht', max_length=20)),
|
('operator', models.CharField(choices=[('more_than', 'More than'), ('less_than', 'Less than'), ('equal_to', 'Equal to')], default='ht', max_length=20)),
|
||||||
('count', models.PositiveSmallIntegerField(default=0, null=True)),
|
('count', models.PositiveSmallIntegerField(default=0, null=True)),
|
||||||
('counted_page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page')),
|
('counted_page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page')),
|
||||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_visitcountrule_related', related_query_name='personalisation_visitcountrules', to='personalisation.Segment')),
|
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_visitcountrule_related', related_query_name='wagtail_personalisation_visitcountrules', to='wagtail_personalisation.Segment')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
@ -82,11 +82,11 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='referralrule',
|
model_name='referralrule',
|
||||||
name='segment',
|
name='segment',
|
||||||
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_referralrule_related', related_query_name='personalisation_referralrules', to='personalisation.Segment'),
|
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_referralrule_related', related_query_name='wagtail_personalisation_referralrules', to='wagtail_personalisation.Segment'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='personalisablepage',
|
model_name='personalisablepage',
|
||||||
name='segment',
|
name='segment',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='segments', to='personalisation.Segment'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='segments', to='wagtail_personalisation.Segment'),
|
||||||
),
|
),
|
||||||
]
|
]
|
@ -10,7 +10,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('personalisation', '0001_initial'),
|
('wagtail_personalisation', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('query_parameter', models.TextField(verbose_name='The query parameter to search for')),
|
('query_parameter', models.TextField(verbose_name='The query parameter to search for')),
|
||||||
('query_value', models.TextField(verbose_name='The value of the parameter to match')),
|
('query_value', models.TextField(verbose_name='The value of the parameter to match')),
|
||||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_queryrule_related', related_query_name='personalisation_queryrules', to='personalisation.Segment')),
|
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_queryrule_related', related_query_name='wagtail_personalisation_queryrules', to='wagtail_personalisation.Segment')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
@ -8,7 +8,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('personalisation', '0002_auto_20161205_1623'),
|
('wagtail_personalisation', '0002_auto_20161205_1623'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
@ -8,7 +8,7 @@ from django.db import migrations, models
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('personalisation', '0003_auto_20161206_1005'),
|
('wagtail_personalisation', '0003_auto_20161206_1005'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.1 on 2016-12-11 12:15
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import modelcluster.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0004_segment_persistent'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UserIsLoggedInRule',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('is_logged_in', models.BooleanField(default=False)),
|
||||||
|
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_userisloggedinrule_related', related_query_name='wagtail_personalisation_userisloggedinrules', to='wagtail_personalisation.Segment')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.1 on 2016-12-22 13:23
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0005_userisloggedinrule'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='match_any',
|
||||||
|
field=models.BooleanField(default=False, help_text='Should the segment match all the rules or just one of them?'),
|
||||||
|
),
|
||||||
|
]
|
34
src/wagtail_personalisation/migrations/0007_dayrule.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.5 on 2017-01-10 14:35
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import modelcluster.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0006_segment_match_any'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DayRule',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('mon', models.BooleanField(default=False, verbose_name='Monday')),
|
||||||
|
('tue', models.BooleanField(default=False, verbose_name='Tuesday')),
|
||||||
|
('wed', models.BooleanField(default=False, verbose_name='Wednesday')),
|
||||||
|
('thu', models.BooleanField(default=False, verbose_name='Thursday')),
|
||||||
|
('fri', models.BooleanField(default=False, verbose_name='Friday')),
|
||||||
|
('sat', models.BooleanField(default=False, verbose_name='Saturday')),
|
||||||
|
('sun', models.BooleanField(default=False, verbose_name='Sunday')),
|
||||||
|
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_dayrule_related', related_query_name='wagtail_personalisation_dayrules', to='wagtail_personalisation.Segment')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
30
src/wagtail_personalisation/migrations/0008_devicerule.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-04-20 15:47
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import modelcluster.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0007_dayrule'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DeviceRule',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('mobile', models.BooleanField(default=False, verbose_name='Mobile phone')),
|
||||||
|
('tablet', models.BooleanField(default=False, verbose_name='Tablet')),
|
||||||
|
('desktop', models.BooleanField(default=False, verbose_name='Desktop')),
|
||||||
|
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_devicerule_related', related_query_name='wagtail_personalisation_devicerules', to='wagtail_personalisation.Segment')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.1 on 2017-05-31 04:28
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0008_devicerule'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='personalisablepage',
|
||||||
|
name='canonical_page',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='personalisablepage',
|
||||||
|
name='page_ptr',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='personalisablepage',
|
||||||
|
name='segment',
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(
|
||||||
|
name='PersonalisablePage',
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,48 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.1 on 2017-05-31 11:01
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0009_auto_20170531_0428'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='dayrule',
|
||||||
|
options={'verbose_name': 'Day Rule'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='devicerule',
|
||||||
|
options={'verbose_name': 'Device Rule'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='queryrule',
|
||||||
|
options={'verbose_name': 'Query Rule'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='referralrule',
|
||||||
|
options={'verbose_name': 'Referral Rule'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='timerule',
|
||||||
|
options={'verbose_name': 'Time Rule'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='userisloggedinrule',
|
||||||
|
options={'verbose_name': 'Logged in Rule'},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='visitcountrule',
|
||||||
|
options={'verbose_name': 'Visit count Rule'},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='referralrule',
|
||||||
|
name='regex_string',
|
||||||
|
field=models.TextField(verbose_name='Regular expression to match the referrer'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.1 on 2017-05-31 14:28
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||||
|
('wagtail_personalisation', '0010_auto_20170531_1101'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PersonalisablePageMetadata',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('is_segmented', models.BooleanField(default=False)),
|
||||||
|
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='personalisable_canonical_metadata', to='wagtailcore.Page')),
|
||||||
|
('segment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='page_metadata', to='wagtail_personalisation.Segment')),
|
||||||
|
('variant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.1 on 2017-06-01 11:48
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0011_personalisablepagemetadata'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='personalisablepagemetadata',
|
||||||
|
name='is_segmented',
|
||||||
|
),
|
||||||
|
]
|
0
src/wagtail_personalisation/migrations/__init__.py
Normal file
210
src/wagtail_personalisation/models.py
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from django.db import models, transaction
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from modelcluster.models import ClusterableModel
|
||||||
|
from wagtail.wagtailadmin.edit_handlers import (
|
||||||
|
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
|
||||||
|
from wagtail.wagtailcore.models import Page
|
||||||
|
|
||||||
|
from wagtail_personalisation.rules import AbstractBaseRule
|
||||||
|
from wagtail_personalisation.utils import count_active_days
|
||||||
|
|
||||||
|
|
||||||
|
class SegmentQuerySet(models.QuerySet):
|
||||||
|
def enabled(self):
|
||||||
|
return self.filter(status=self.model.STATUS_ENABLED)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Segment(ClusterableModel):
|
||||||
|
"""The segment model."""
|
||||||
|
STATUS_ENABLED = 'enabled'
|
||||||
|
STATUS_DISABLED = 'disabled'
|
||||||
|
|
||||||
|
STATUS_CHOICES = (
|
||||||
|
(STATUS_ENABLED, _('Enabled')),
|
||||||
|
(STATUS_DISABLED, _('Disabled')),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
create_date = models.DateTimeField(auto_now_add=True)
|
||||||
|
edit_date = models.DateTimeField(auto_now=True)
|
||||||
|
enable_date = models.DateTimeField(null=True, editable=False)
|
||||||
|
disable_date = models.DateTimeField(null=True, editable=False)
|
||||||
|
visit_count = models.PositiveIntegerField(default=0, editable=False)
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED)
|
||||||
|
persistent = models.BooleanField(
|
||||||
|
default=False, help_text=_("Should the segment persist between visits?"))
|
||||||
|
match_any = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
help_text=_("Should the segment match all the rules or just one of them?")
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = SegmentQuerySet.as_manager()
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
Segment.panels = [
|
||||||
|
MultiFieldPanel([
|
||||||
|
FieldPanel('name', classname="title"),
|
||||||
|
FieldRowPanel([
|
||||||
|
FieldPanel('status'),
|
||||||
|
FieldPanel('persistent'),
|
||||||
|
]),
|
||||||
|
FieldPanel('match_any'),
|
||||||
|
], heading="Segment"),
|
||||||
|
MultiFieldPanel([
|
||||||
|
InlinePanel(
|
||||||
|
"{}_related".format(rule_model._meta.db_table),
|
||||||
|
label=rule_model._meta.verbose_name,
|
||||||
|
) for rule_model in AbstractBaseRule.__subclasses__()
|
||||||
|
], heading=_("Rules")),
|
||||||
|
]
|
||||||
|
|
||||||
|
super(Segment, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def encoded_name(self):
|
||||||
|
"""Return a string with a slug for the segment."""
|
||||||
|
return slugify(self.name.lower())
|
||||||
|
|
||||||
|
def get_active_days(self):
|
||||||
|
"""Return the amount of days the segment has been active."""
|
||||||
|
return count_active_days(self.enable_date, self.disable_date)
|
||||||
|
|
||||||
|
def get_used_pages(self):
|
||||||
|
"""Return the pages that have variants using this segment."""
|
||||||
|
pages = list(PersonalisablePageMetadata.objects.filter(segment=self))
|
||||||
|
|
||||||
|
return pages
|
||||||
|
|
||||||
|
def get_created_variants(self):
|
||||||
|
"""Return the variants using this segment."""
|
||||||
|
pages = Page.objects.filter(_personalisable_page_metadata__segment=self)
|
||||||
|
|
||||||
|
return pages
|
||||||
|
|
||||||
|
def get_rules(self):
|
||||||
|
"""Retrieve all rules in the segment."""
|
||||||
|
segment_rules = []
|
||||||
|
for rule_model in AbstractBaseRule.get_descendant_models():
|
||||||
|
segment_rules.extend(
|
||||||
|
rule_model._default_manager.filter(segment=self))
|
||||||
|
|
||||||
|
return segment_rules
|
||||||
|
|
||||||
|
def toggle(self, save=True):
|
||||||
|
self.status = (
|
||||||
|
self.STATUS_ENABLED if self.status == self.STATUS_DISABLED
|
||||||
|
else self.STATUS_DISABLED)
|
||||||
|
if save:
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalisablePageMetadata(ClusterableModel):
|
||||||
|
"""The personalisable page model. Allows creation of variants with linked
|
||||||
|
segments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
canonical_page = models.ForeignKey(
|
||||||
|
Page, related_name='personalisable_canonical_metadata',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
blank=True, null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
variant = models.OneToOneField(
|
||||||
|
Page, related_name='_personalisable_page_metadata')
|
||||||
|
|
||||||
|
segment = models.ForeignKey(
|
||||||
|
Segment, related_name='page_metadata', null=True, blank=True)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def has_variants(self):
|
||||||
|
"""Return a boolean indicating whether or not the personalisable page
|
||||||
|
has variants.
|
||||||
|
|
||||||
|
:returns: A boolean indicating whether or not the personalisable page
|
||||||
|
has variants.
|
||||||
|
:rtype: bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.variants_metadata.exists()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def variants_metadata(self):
|
||||||
|
return (
|
||||||
|
PersonalisablePageMetadata.objects
|
||||||
|
.filter(canonical_page_id=self.canonical_page_id)
|
||||||
|
.exclude(variant_id=self.variant_id)
|
||||||
|
.exclude(variant_id=self.canonical_page_id))
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def is_canonical(self):
|
||||||
|
"""Return a boolean indicating whether or not the personalisable page
|
||||||
|
is a canonical page.
|
||||||
|
|
||||||
|
:returns: A boolean indicating whether or not the personalisable
|
||||||
|
page
|
||||||
|
is a canonical page.
|
||||||
|
:rtype: bool
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.canonical_page_id == self.variant_id
|
||||||
|
|
||||||
|
def copy_for_segment(self, segment):
|
||||||
|
page = self.canonical_page
|
||||||
|
|
||||||
|
slug = "{}-{}".format(page.slug, segment.encoded_name())
|
||||||
|
title = "{} ({})".format(page.title, segment.name)
|
||||||
|
update_attrs = {
|
||||||
|
'title': title,
|
||||||
|
'slug': slug,
|
||||||
|
'live': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
new_page = self.canonical_page.copy(
|
||||||
|
update_attrs=update_attrs, copy_revisions=False)
|
||||||
|
|
||||||
|
PersonalisablePageMetadata.objects.create(
|
||||||
|
canonical_page=page,
|
||||||
|
variant=new_page,
|
||||||
|
segment=segment)
|
||||||
|
return new_page
|
||||||
|
|
||||||
|
def metadata_for_segments(self, segments):
|
||||||
|
return (
|
||||||
|
self.__class__.objects
|
||||||
|
.filter(
|
||||||
|
canonical_page_id=self.canonical_page_id,
|
||||||
|
segment__in=segments))
|
||||||
|
|
||||||
|
def get_unused_segments(self):
|
||||||
|
if self.is_canonical:
|
||||||
|
return (
|
||||||
|
Segment.objects
|
||||||
|
.exclude(page_metadata__canonical_page_id=self.canonical_page_id))
|
||||||
|
return Segment.objects.none()
|
||||||
|
|
||||||
|
|
||||||
|
class PersonalisablePageMixin(object):
|
||||||
|
"""The personalisable page model. Allows creation of variants with linked
|
||||||
|
segments.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def personalisation_metadata(self):
|
||||||
|
try:
|
||||||
|
metadata = self._personalisable_page_metadata
|
||||||
|
except AttributeError:
|
||||||
|
metadata = PersonalisablePageMetadata.objects.create(
|
||||||
|
canonical_page=self, variant=self)
|
||||||
|
return metadata
|
24
src/wagtail_personalisation/receivers.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from django.db.models.signals import pre_save
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from wagtail_personalisation.models import Segment
|
||||||
|
|
||||||
|
|
||||||
|
def check_status_change(sender, instance, *args, **kwargs):
|
||||||
|
"""Check if the status has changed. Alter dates accordingly."""
|
||||||
|
try:
|
||||||
|
original_status = sender.objects.get(pk=instance.id).status
|
||||||
|
except sender.DoesNotExist:
|
||||||
|
original_status = ""
|
||||||
|
|
||||||
|
if original_status != instance.status:
|
||||||
|
if instance.status == instance.STATUS_ENABLED:
|
||||||
|
instance.enable_date = timezone.now()
|
||||||
|
instance.visit_count = 0
|
||||||
|
return instance
|
||||||
|
if instance.status == instance.STATUS_DISABLED:
|
||||||
|
instance.disable_date = timezone.now()
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
pre_save.connect(check_status_change, sender=Segment)
|
350
src/wagtail_personalisation/rules.py
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.db import models
|
||||||
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from modelcluster.fields import ParentalKey
|
||||||
|
from user_agents import parse
|
||||||
|
from wagtail.wagtailadmin.edit_handlers import (
|
||||||
|
FieldPanel, FieldRowPanel, PageChooserPanel)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class AbstractBaseRule(models.Model):
|
||||||
|
"""Base for creating rules to segment users with."""
|
||||||
|
icon = 'fa-circle-o'
|
||||||
|
|
||||||
|
segment = ParentalKey(
|
||||||
|
'wagtail_personalisation.Segment',
|
||||||
|
related_name="%(app_label)s_%(class)s_related",
|
||||||
|
related_query_name="%(app_label)s_%(class)ss"
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
verbose_name = 'Abstract segmentation rule'
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self._meta.verbose_name)
|
||||||
|
|
||||||
|
def test_user(self):
|
||||||
|
"""Test if the user matches this rule."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def encoded_name(self):
|
||||||
|
"""Return a string with a slug for the rule."""
|
||||||
|
return slugify(force_text(self).lower())
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
"""Return a description explaining the functionality of the rule.
|
||||||
|
Used in the segmentation dashboard.
|
||||||
|
|
||||||
|
:returns: A dict containing a title and a value
|
||||||
|
:rtype: dict
|
||||||
|
|
||||||
|
"""
|
||||||
|
description = {
|
||||||
|
'title': _('Abstract segmentation rule'),
|
||||||
|
'value': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
return description
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_descendant_models(cls):
|
||||||
|
return [model for model in apps.get_models()
|
||||||
|
if issubclass(model, AbstractBaseRule)]
|
||||||
|
|
||||||
|
|
||||||
|
class TimeRule(AbstractBaseRule):
|
||||||
|
"""Time rule to segment users based on a start and end time.
|
||||||
|
|
||||||
|
Matches when the time a request is made falls between the
|
||||||
|
set start time and end time.
|
||||||
|
|
||||||
|
"""
|
||||||
|
icon = 'fa-clock-o'
|
||||||
|
|
||||||
|
start_time = models.TimeField(_("Starting time"))
|
||||||
|
end_time = models.TimeField(_("Ending time"))
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
FieldRowPanel([
|
||||||
|
FieldPanel('start_time'),
|
||||||
|
FieldPanel('end_time'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Time Rule')
|
||||||
|
|
||||||
|
def test_user(self, request=None):
|
||||||
|
return self.start_time <= datetime.now().time() <= self.end_time
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
return {
|
||||||
|
'title': _('These users visit between'),
|
||||||
|
'value': _('{} and {}').format(
|
||||||
|
self.start_time.strftime("%H:%M"),
|
||||||
|
self.end_time.strftime("%H:%M")
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DayRule(AbstractBaseRule):
|
||||||
|
"""Day rule to segment users based on the day(s) of a visit.
|
||||||
|
|
||||||
|
Matches when the day a request is made matches with the days
|
||||||
|
set in the rule.
|
||||||
|
|
||||||
|
"""
|
||||||
|
icon = 'fa-calendar-check-o'
|
||||||
|
|
||||||
|
mon = models.BooleanField(_("Monday"), default=False)
|
||||||
|
tue = models.BooleanField(_("Tuesday"), default=False)
|
||||||
|
wed = models.BooleanField(_("Wednesday"), default=False)
|
||||||
|
thu = models.BooleanField(_("Thursday"), default=False)
|
||||||
|
fri = models.BooleanField(_("Friday"), default=False)
|
||||||
|
sat = models.BooleanField(_("Saturday"), default=False)
|
||||||
|
sun = models.BooleanField(_("Sunday"), default=False)
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
FieldPanel('mon'),
|
||||||
|
FieldPanel('tue'),
|
||||||
|
FieldPanel('wed'),
|
||||||
|
FieldPanel('thu'),
|
||||||
|
FieldPanel('fri'),
|
||||||
|
FieldPanel('sat'),
|
||||||
|
FieldPanel('sun'),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Day Rule')
|
||||||
|
|
||||||
|
def test_user(self, request=None):
|
||||||
|
return [self.mon, self.tue, self.wed, self.thu,
|
||||||
|
self.fri, self.sat, self.sun][datetime.today().weekday()]
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
days = (
|
||||||
|
('mon', self.mon), ('tue', self.tue), ('wed', self.wed),
|
||||||
|
('thu', self.thu), ('fri', self.fri), ('sat', self.sat),
|
||||||
|
('sun', self.sun),
|
||||||
|
)
|
||||||
|
|
||||||
|
chosen_days = [day_name for day_name, chosen in days if chosen]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'title': _('These users visit on'),
|
||||||
|
'value': ", ".join([day for day in chosen_days]).title(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ReferralRule(AbstractBaseRule):
|
||||||
|
"""Referral rule to segment users based on a regex test.
|
||||||
|
|
||||||
|
Matches when the referral header in a request matches with
|
||||||
|
the set regex test.
|
||||||
|
|
||||||
|
"""
|
||||||
|
icon = 'fa-globe'
|
||||||
|
|
||||||
|
regex_string = models.TextField(
|
||||||
|
_("Regular expression to match the referrer"))
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
FieldPanel('regex_string'),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Referral Rule')
|
||||||
|
|
||||||
|
def test_user(self, request):
|
||||||
|
pattern = re.compile(self.regex_string)
|
||||||
|
|
||||||
|
if 'HTTP_REFERER' in request.META:
|
||||||
|
referer = request.META['HTTP_REFERER']
|
||||||
|
if pattern.search(referer):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
return {
|
||||||
|
'title': _('These visits originate from'),
|
||||||
|
'value': self.regex_string,
|
||||||
|
'code': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class VisitCountRule(AbstractBaseRule):
|
||||||
|
"""Visit count rule to segment users based on amount of visits to a
|
||||||
|
specified page.
|
||||||
|
|
||||||
|
Matches when the operator and count validate True
|
||||||
|
when visiting the set page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
icon = 'fa-calculator'
|
||||||
|
|
||||||
|
OPERATOR_CHOICES = (
|
||||||
|
('more_than', _("More than")),
|
||||||
|
('less_than', _("Less than")),
|
||||||
|
('equal_to', _("Equal to")),
|
||||||
|
)
|
||||||
|
operator = models.CharField(max_length=20,
|
||||||
|
choices=OPERATOR_CHOICES, default="more_than")
|
||||||
|
count = models.PositiveSmallIntegerField(default=0, null=True)
|
||||||
|
counted_page = models.ForeignKey(
|
||||||
|
'wagtailcore.Page',
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='+',
|
||||||
|
)
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
PageChooserPanel('counted_page'),
|
||||||
|
FieldRowPanel([
|
||||||
|
FieldPanel('operator'),
|
||||||
|
FieldPanel('count'),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Visit count Rule')
|
||||||
|
|
||||||
|
def test_user(self, request):
|
||||||
|
operator = self.operator
|
||||||
|
segment_count = self.count
|
||||||
|
|
||||||
|
# Local import for cyclic import
|
||||||
|
from wagtail_personalisation.adapters import get_segment_adapter
|
||||||
|
|
||||||
|
adapter = get_segment_adapter(request)
|
||||||
|
|
||||||
|
visit_count = adapter.get_visit_count()
|
||||||
|
if visit_count and operator == "more_than":
|
||||||
|
if visit_count > segment_count:
|
||||||
|
return True
|
||||||
|
elif visit_count and operator == "less_than":
|
||||||
|
if visit_count < segment_count:
|
||||||
|
return True
|
||||||
|
elif visit_count and operator == "equal_to":
|
||||||
|
if visit_count == segment_count:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
return {
|
||||||
|
'title': _('These users visited {}').format(
|
||||||
|
self.counted_page
|
||||||
|
),
|
||||||
|
'value': _('{} {} times').format(
|
||||||
|
self.get_operator_display(),
|
||||||
|
self.count
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class QueryRule(AbstractBaseRule):
|
||||||
|
"""Query rule to segment users based on matching queries.
|
||||||
|
|
||||||
|
Matches when both the set parameter and value match with one
|
||||||
|
present in the request query.
|
||||||
|
|
||||||
|
"""
|
||||||
|
icon = 'fa-link-o'
|
||||||
|
|
||||||
|
parameter = models.SlugField(_("The query parameter to search for"),
|
||||||
|
max_length=20)
|
||||||
|
value = models.SlugField(_("The value of the parameter to match"),
|
||||||
|
max_length=20)
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
FieldPanel('parameter'),
|
||||||
|
FieldPanel('value'),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Query Rule')
|
||||||
|
|
||||||
|
def test_user(self, request):
|
||||||
|
return request.GET.get(self.parameter, '') == self.value
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
return {
|
||||||
|
'title': _('These users used a URL with the query'),
|
||||||
|
'value': _('?{}={}').format(
|
||||||
|
self.parameter,
|
||||||
|
self.value
|
||||||
|
),
|
||||||
|
'code': True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceRule(AbstractBaseRule):
|
||||||
|
"""Device rule to segment users based on matching devices.
|
||||||
|
|
||||||
|
Matches when the set device type matches with the one present
|
||||||
|
in the request user agent headers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
icon = 'fa-tablet'
|
||||||
|
|
||||||
|
mobile = models.BooleanField(_("Mobile phone"), default=False)
|
||||||
|
tablet = models.BooleanField(_("Tablet"), default=False)
|
||||||
|
desktop = models.BooleanField(_("Desktop"), default=False)
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
FieldPanel('mobile'),
|
||||||
|
FieldPanel('tablet'),
|
||||||
|
FieldPanel('desktop'),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Device Rule')
|
||||||
|
|
||||||
|
def test_user(self, request=None):
|
||||||
|
ua_header = request.META['HTTP_USER_AGENT']
|
||||||
|
user_agent = parse(ua_header)
|
||||||
|
|
||||||
|
if user_agent.is_mobile:
|
||||||
|
return self.mobile
|
||||||
|
if user_agent.is_tablet:
|
||||||
|
return self.tablet
|
||||||
|
if user_agent.is_pc:
|
||||||
|
return self.desktop
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class UserIsLoggedInRule(AbstractBaseRule):
|
||||||
|
"""User is logged in rule to segment users based on their authentication
|
||||||
|
status.
|
||||||
|
|
||||||
|
Matches when the user is authenticated.
|
||||||
|
|
||||||
|
"""
|
||||||
|
icon = 'fa-user'
|
||||||
|
|
||||||
|
is_logged_in = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
panels = [
|
||||||
|
FieldPanel('is_logged_in'),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Logged in Rule')
|
||||||
|
|
||||||
|
def test_user(self, request=None):
|
||||||
|
return request.user.is_authenticated() == self.is_logged_in
|
||||||
|
|
||||||
|
def description(self):
|
||||||
|
return {
|
||||||
|
'title': _('These visitors are'),
|
||||||
|
'value': _('Logged in') if self.is_logged_in else _('Not logged in'),
|
||||||
|
}
|
2
src/wagtail_personalisation/static/css/dashboard.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li{display:inline-block;margin-bottom:5px}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
|
||||||
|
/*# sourceMappingURL=dashboard.css.map*/
|
1
src/wagtail_personalisation/static/css/dashboard.css.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":["webpack:///./scss/dashboard.scss"],"names":[],"mappings":"AAAA,cACI,kBACA,kBAAmB,CAGvB,iBACI,cACA,eAAgB,CAGpB,wBACI,cACA,WACA,8BAAsB,sBACtB,kBACA,uBACA,iBACA,kBACA,mBACA,yBACA,kBACA,sBACA,+DAAkE,uDAClE,2GAA8F,2UAC9F,cAAe,CAGnB,0FAEI,UAAY,CAGZ,2BACI,qBACA,UAAW,CAGnB,uCACI,iBAAkB,CAGlB,uCACI,gBACA,SACA,SAAU,CAGV,0CACI,WACA,iBAAkB,CAGtB,qDACI,cAAe,CAG3B,mCACI,yBAA0B,CAG9B,8BACI,sBACA,uEAAkE,+DAGtE,yBACI,wBACI,WACA,cAAe,CAClB,CAGL,2CACI,oBAAa,iCACb,8BAAmB,uEACnB,qBAAiB,iBACjB,yBAA8B,oDAC9B,0BAAoB,2CACpB,kBAAmB,CAGvB,oDACI,cACA,WACA,uBACA,UACA,SACA,eAAgB,CAGhB,yCACI,kBACA,qBAAsB,CAG1B,uDACI,qBACA,iBAAkB,CAGtB,4DACI,cACA,eACA,gBACA,aACA,wBAAyB,CAG7B,2DACI,kBACA,8BAAsB,sBACtB,WACA,sBACA,sBACA,sBACA,gBACA,qBACA,qBACA,iBAAkB,CAG1B,oDACI,cACA,kBACA,wBACA,kBACA,QACA,mCAA2B,2BAC3B,cACA,eACA,eAAgB","file":"../css/dashboard.css","sourcesContent":[".nice-padding {\n padding-left: 50px;\n padding-right: 50px;\n}\n\n.block_container {\n display: block;\n margin-top: 30px;\n}\n\n.block_container .block {\n display: block;\n float: left;\n box-sizing: border-box;\n position: relative;\n width: calc(50% - 10px);\n min-height: 216px;\n padding: 10px 20px;\n margin-bottom: 20px;\n border: 1px solid #d9d9d9;\n border-radius: 3px;\n background-color: #fff;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);\n cursor: pointer;\n}\n\n.block_container .block--disabled h2,\n.block_container .block--disabled .inspect_container {\n opacity: 0.5;\n}\n\n .block_container .block h2 {\n display: inline-block;\n width: auto;\n }\n\n.block_container .block:nth-child(odd) {\n margin-right: 20px;\n}\n\n .block_container .block .block_actions {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n .block_container .block .block_actions li {\n float: left;\n margin-right: 10px;\n }\n\n .block_container .block .block_actions li:last-child {\n margin-right: 0;\n }\n\n.block_container .block.suggestion {\n border: 1px dashed #d9d9d9;\n}\n\n.block_container .block:hover {\n border: 1px solid #fff;\n box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);\n}\n\n@media (max-width: 699px) {\n .block_container .block {\n width: 100%;\n margin-right: 0;\n }\n}\n\n.block_container .block .inspect_container {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: stretch;\n margin-bottom: 10px;\n}\n\n.block_container .block .inspect_container .inspect {\n display: block;\n float: left;\n width: calc(50% - 10px);\n padding: 0;\n margin: 0;\n list-style: none;\n}\n\n .block_container .block span.icon::before {\n margin-right: 0.3em;\n vertical-align: bottom;\n }\n\n .block_container .block .inspect_container .inspect li {\n display: inline-block;\n margin-bottom: 5px;\n }\n\n .block_container .block .inspect_container .inspect li span {\n display: block;\n font-size: 20px;\n font-weight: bold;\n margin: 5px 0;\n overflow-wrap: break-word;\n }\n\n .block_container .block .inspect_container .inspect li pre {\n position: relative;\n box-sizing: border-box;\n width: auto;\n background-color: #eee;\n border: 1px solid #ccc;\n margin: 5px 0 5px 21px;\n padding: 2px 5px;\n word-wrap: break-word;\n word-break: break-all;\n border-radius: 3px;\n }\n\n.block_container .block.suggestion .suggestive_text {\n display: block;\n position: absolute;\n width: calc(100% - 40px);\n text-align: center;\n top: 50%;\n transform: translateY(-50%);\n color: #d9d9d9;\n font-size: 20px;\n font-weight: 100;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./scss/dashboard.scss"],"sourceRoot":""}
|
2
src/wagtail_personalisation/static/css/form.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.block_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap}.block_container .block{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(100% / 3 - 13.33px);padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;cursor:pointer;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1)}.block_container .block:nth-child(3n+1),.block_container .block:nth-child(3n+2){margin-right:20px}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}.block_container .block.disabled{background-color:#eee;cursor:default}.block_container .block.disabled:hover{border:1px solid #d9d9d9;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent}@media (min-width:800px) and (max-width:999px){.block_container .block{width:calc(100% / 2 - 10px)}.block_container .block:nth-child(3n+1),.block_container .block:nth-child(3n+2){margin-right:0}.block_container .block:nth-child(odd){margin-right:20px}}@media (max-width:599px){.block_container .block{width:calc(100% / 2 - 10px)}.block_container .block:nth-child(3n+1),.block_container .block:nth-child(3n+2){margin-right:0}.block_container .block:nth-child(odd){margin-right:20px}}
|
||||||
|
/*# sourceMappingURL=form.css.map*/
|
1
src/wagtail_personalisation/static/css/form.css.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":["webpack:///./scss/form.scss"],"names":[],"mappings":"AAAA,iBACI,oBAAa,iCACb,8BAAmB,uEACnB,mBAAe,eAGf,wBACI,cACA,8BAAsB,sBACtB,kBACA,+BACA,kBACA,mBACA,yBACA,kBACA,sBACA,eACA,+DAAkE,uDAClE,2GAA8F,2UAGlG,gFAEI,iBAAkB,CAGtB,8BACI,sBACA,uEAAkE,+DAGtE,iCACI,sBACA,cAAe,CAGnB,uCACI,yBACA,+DAAkE,uDAG1E,+CACI,wBACI,2BAA4B,CAGhC,gFAEI,cAAe,CAGnB,uCACI,iBAAkB,CACrB,CAGL,yBACI,wBACI,2BAA4B,CAGhC,gFAEI,cAAe,CAGnB,uCACI,iBAAkB,CACrB","file":"../css/form.css","sourcesContent":[".block_container {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n}\n\n .block_container .block {\n display: block;\n box-sizing: border-box;\n position: relative;\n width: calc(100% / 3 - 13.33px);\n padding: 10px 20px;\n margin-bottom: 20px;\n border: 1px solid #d9d9d9;\n border-radius: 3px;\n background-color: #fff;\n cursor: pointer;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);\n }\n\n .block_container .block:nth-child(3n+1),\n .block_container .block:nth-child(3n+2) {\n margin-right: 20px;\n }\n\n .block_container .block:hover {\n border: 1px solid #fff;\n box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);\n }\n\n .block_container .block.disabled {\n background-color: #eee;\n cursor: default;\n }\n\n .block_container .block.disabled:hover {\n border: 1px solid #d9d9d9;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n }\n\n@media (min-width: 800px) and (max-width: 999px) {\n .block_container .block {\n width: calc(100% / 2 - 10px);\n }\n\n .block_container .block:nth-child(3n+1),\n .block_container .block:nth-child(3n+2) {\n margin-right: 0;\n }\n\n .block_container .block:nth-child(2n+1) {\n margin-right: 20px;\n }\n}\n\n@media (max-width: 599px) {\n .block_container .block {\n width: calc(100% / 2 - 10px);\n }\n\n .block_container .block:nth-child(3n+1),\n .block_container .block:nth-child(3n+2) {\n margin-right: 0;\n }\n\n .block_container .block:nth-child(2n+1) {\n margin-right: 20px;\n }\n}\n\n\n// WEBPACK FOOTER //\n// ./scss/form.scss"],"sourceRoot":""}
|
2
src/wagtail_personalisation/static/css/index.css
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
/*# sourceMappingURL=index.css.map*/
|
1
src/wagtail_personalisation/static/css/index.css.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":[],"names":[],"mappings":"","file":"../css/index.css","sourceRoot":""}
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 794 B |
BIN
src/wagtail_personalisation/static/img/key_icon.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/wagtail_personalisation/static/img/persistent_icon.png
Normal file
After Width: | Height: | Size: 845 B |