Compare commits
212 Commits
0.9.1
...
feature/se
Author | SHA1 | Date | |
---|---|---|---|
acd273c06c | |||
4b3af020fd | |||
05afea8d68 | |||
c31415b484 | |||
4a596d62f2 | |||
3c1c0c3306 | |||
937c06cf32 | |||
d7fac2607b | |||
be672f6fde | |||
a47803eca5 | |||
e42e1a865b | |||
293004fdc6 | |||
6f0425cd5f | |||
0fd6d4d2e5 | |||
e0fbefd53f | |||
c87b2936e9 | |||
7010f5acea | |||
d94890848d | |||
f8d226efaf | |||
037381f79f | |||
0f5501ceef | |||
4e5454b348 | |||
9919d76741 | |||
7e9dd8624b | |||
6514bc1763 | |||
65a46f2bd9 | |||
f1b62a7546 | |||
03e02e8b91 | |||
8d8975ac36 | |||
2324a30afd | |||
0bdb80f25a | |||
1c1a7ce1b8 | |||
2a48eb3498 | |||
4ad097b4fa | |||
939247c147 | |||
12f110d913 | |||
c8fe62d2b1 | |||
83c2a4289e | |||
84ac76f33e | |||
f6598ca1f7 | |||
726c0cd70f | |||
4f3f9a4d40 | |||
3a378830e0 | |||
8a151e3bab | |||
bb34bddaf4 | |||
9710d3b479 | |||
5536adc3ec | |||
5b8d578493 | |||
bdba6b65cf | |||
cbcd80d248 | |||
9b1c5a6ab6 | |||
62d258fd9e | |||
32e73329c3 | |||
fde53ea0ef | |||
22a7367211 | |||
0d89d47735 | |||
92189a3be8 | |||
6c9d8b2730 | |||
e141e5396e | |||
c0e2b969e8 | |||
7b5e3d4c9d | |||
6b7a1ed591 | |||
9b25cd2a94 | |||
3a86c189dc | |||
82c26f9772 | |||
03eb812e45 | |||
e3522d0acb | |||
7f5e958ee3 | |||
241bfb5240 | |||
d5df6e0e58 | |||
865efd0792 | |||
454c936e0f | |||
74d3123084 | |||
9bfd816430 | |||
02e06bd9f3 | |||
c7ad3251cf | |||
cb8b7da496 | |||
0efd3ae937 | |||
d335e4fd7b | |||
db2f82967e | |||
37243365a7 | |||
43a2b590b4 | |||
cc1dd337bb | |||
a677846ff7 | |||
7d7861b862 | |||
8e854d0abe | |||
0051061d96 | |||
f898dfe017 | |||
8ced5bd81c | |||
9a86b0c8cc | |||
9408f90789 | |||
ba6056e3f8 | |||
fdc0a7f2e1 | |||
12b0cd9231 | |||
330557be8d | |||
aa917dee9c | |||
364cb1a7e6 | |||
8f789b3e17 | |||
bedbe06c65 | |||
362f15e5ff | |||
8a0dba2efb | |||
59f4877e04 | |||
2ff29cc375 | |||
8527e6ff23 | |||
d7c07cb238 | |||
6e83366df6 | |||
55364f8906 | |||
4fd0b30c66 | |||
c909852b08 | |||
ea1ecc2a98 | |||
0f0aecf673 | |||
c11960f921 | |||
37d49dcdfb | |||
869237360d | |||
33277a0b20 | |||
2cd643fb2d | |||
0f18024ebc | |||
521222f748 | |||
56a8e106d8 | |||
3162191a16 | |||
8c7e99313b | |||
824e42174f | |||
d114bb2570 | |||
7bba1e57cc | |||
3017f32b6b | |||
6b1a7cf1f2 | |||
1525b7946c | |||
7bf1bc3f19 | |||
4c60bcbe6b | |||
ad4f75d471 | |||
086168954d | |||
881090f2f9 | |||
d073c7d268 | |||
7200b5b4c4 | |||
6f97c76958 | |||
ecb4f928fb | |||
29aa91477e | |||
5c3acc6661 | |||
602919d2d4 | |||
ae97118c3f | |||
51774b939e | |||
908f85e295 | |||
99f9700ed0 | |||
7fa8ee1a46 | |||
5ad70d68f6 | |||
06bfe77901 | |||
d5e89d374b | |||
5b39e82f80 | |||
fbcebb43a4 | |||
ef271587ec | |||
786a8801b1 | |||
caf73aa43c | |||
4021d2c915 | |||
33f96af4a3 | |||
6299feb497 | |||
7ced6db126 | |||
c6ce67c9c9 | |||
3df3fc0b16 | |||
a00929846e | |||
49fba11049 | |||
e3488e87ad | |||
808aa6d202 | |||
efb060cc6e | |||
414afa5269 | |||
b3f0ac2d58 | |||
4f9c18d2cf | |||
a4a283e4f3 | |||
30318549e2 | |||
f19de241b0 | |||
95ecd8d200 | |||
6436b85b1d | |||
06471248d3 | |||
e3df03f559 | |||
0a42ce3eeb | |||
e5068894c3 | |||
fdc2b97194 | |||
a8d3aeab68 | |||
c76d6d1617 | |||
a8c4b66d6e | |||
f3fbee99a2 | |||
4918c99b5f | |||
330c3bd377 | |||
9c9a9d3acd | |||
51e9aa9724 | |||
a5705fd53c | |||
9d1f3074c0 | |||
3bfd5b8e8f | |||
232609fb4e | |||
35fd4836b0 | |||
b786b0a4d2 | |||
23b1456438 | |||
1f4a4536ab | |||
b8bf27fb99 | |||
d07e06b4f0 | |||
71d7faba1f | |||
743d3f668e | |||
bc0b69fde5 | |||
7cf22d05f6 | |||
9e0fc8e6fd | |||
a116b14d57 | |||
44cc95617e | |||
c6ff2801c5 | |||
0d2834a55f | |||
ff236a095d | |||
ef20580334 | |||
cf41be4b76 | |||
f339879907 | |||
aa2a239aec | |||
8c96fffd4e | |||
675d219f1f | |||
9c88ec1582 | |||
785d1486e4 |
2
.gitignore
vendored
@ -23,3 +23,5 @@ tests/sandbox/assets
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
.pytest_cache/
|
||||||
|
27
.travis.yml
@ -4,33 +4,12 @@ language: python
|
|||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Django 1.9, Wagtail 1.9
|
|
||||||
- python: 2.7
|
|
||||||
env: TOXENV=py27-django19-wagtail19
|
|
||||||
- python: 3.5
|
|
||||||
env: TOXENV=py35-django19-wagtail19
|
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: TOXENV=py36-django19-wagtail19
|
env: lint
|
||||||
|
|
||||||
# Django 1.10, Wagtail 1.10
|
|
||||||
- python: 2.7
|
|
||||||
env: TOXENV=py27-django110-wagtail110
|
|
||||||
- python: 3.5
|
|
||||||
env: TOXENV=py35-django110-wagtail110
|
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: TOXENV=py36-django110-wagtail110
|
env: TOXENV=py36-django20-wagtail20
|
||||||
|
|
||||||
# Django 1.11, Wagtail 1.10
|
|
||||||
- python: 2.7
|
|
||||||
env: TOXENV=py27-django111-wagtail110
|
|
||||||
- python: 3.5
|
|
||||||
env: TOXENV=py35-django111-wagtail110
|
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: TOXENV=py36-django111-wagtail110
|
env: TOXENV=py36-django20-wagtail21
|
||||||
|
|
||||||
allow_failures:
|
|
||||||
- python: 3.5
|
|
||||||
env: TOXENV=lint
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install tox codecov
|
- pip install tox codecov
|
||||||
|
47
CHANGES
@ -1,3 +1,50 @@
|
|||||||
|
0.11.3
|
||||||
|
==================
|
||||||
|
- Bugfix: Handle errors when testing an invalid visit count rule
|
||||||
|
|
||||||
|
0.11.2
|
||||||
|
==================
|
||||||
|
- Bugfix: Stop populating static segments when the count is reached
|
||||||
|
|
||||||
|
0.11.1
|
||||||
|
==================
|
||||||
|
- Populate entirely static segments from registered Users not active Sessions
|
||||||
|
|
||||||
|
0.11.0
|
||||||
|
==================
|
||||||
|
- Bug Fix: Query rule should not be static
|
||||||
|
- Enable retrieval of user data for static rules through csv download
|
||||||
|
|
||||||
|
0.10.9
|
||||||
|
==================
|
||||||
|
- Bug Fix: Display the number of users in a static segment on dashboard
|
||||||
|
|
||||||
|
0.10.8
|
||||||
|
==================
|
||||||
|
- Don't add users to exclude list for dynamic segments
|
||||||
|
- Store segments a user is excluded from in the session
|
||||||
|
|
||||||
|
0.10.7
|
||||||
|
==================
|
||||||
|
- Bug Fix: Ensure static segment members are show the survey immediately
|
||||||
|
- Records users excluded by randomisation on the segment
|
||||||
|
- Don't re-check excluded users
|
||||||
|
|
||||||
|
0.10.6
|
||||||
|
==================
|
||||||
|
- Accepts and stores randomisation percentage for segment
|
||||||
|
- Adds users to segment based on random number relative to percentage
|
||||||
|
|
||||||
|
0.10.5
|
||||||
|
==================
|
||||||
|
- Count how many users match a segments rules before saving the segment
|
||||||
|
- Stores count on the segment and displays in the dashboard
|
||||||
|
- Enables testing users against rules if there isn't an active request
|
||||||
|
|
||||||
|
0.10.0
|
||||||
|
==================
|
||||||
|
- Adds static and dynamic segments
|
||||||
|
|
||||||
0.9.1 (tbd)
|
0.9.1 (tbd)
|
||||||
==================
|
==================
|
||||||
|
|
||||||
|
@ -8,3 +8,13 @@ Contributors
|
|||||||
* Michael van Tellingen
|
* Michael van Tellingen
|
||||||
* Pim Vernooij
|
* Pim Vernooij
|
||||||
* Tomasz Knapik
|
* Tomasz Knapik
|
||||||
|
* Kaitlyn Crawford
|
||||||
|
* Todd Dembrey
|
||||||
|
* Nathan Begbie
|
||||||
|
* Rob Moorman
|
||||||
|
* Tom Dyson
|
||||||
|
* Bertrand Bordage
|
||||||
|
* Alex Muller
|
||||||
|
* Saeed Marzban
|
||||||
|
* Milton Madanda
|
||||||
|
* Mike Dingjan
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
include README.rst
|
include README.rst
|
||||||
|
|
||||||
recursive-include src
|
recursive-include src *
|
||||||
|
|
||||||
|
recursive-exclude src __pycache__
|
||||||
|
recursive-exclude src *.py[co]
|
||||||
|
9
Makefile
@ -1,13 +1,13 @@
|
|||||||
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
|
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
|
||||||
|
|
||||||
all: clean requirements dist
|
|
||||||
|
|
||||||
default: develop
|
default: develop
|
||||||
|
|
||||||
|
all: clean requirements dist
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find src -name '*.pyc' -delete
|
find src -name '*.pyc' -delete
|
||||||
find tests -name '*.pyc' -delete
|
find tests -name '*.pyc' -delete
|
||||||
find . -name '*.egg-info' -delete
|
find . -name '*.egg-info' |xargs rm -rf
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
pip install --upgrade -e .[docs,test]
|
pip install --upgrade -e .[docs,test]
|
||||||
@ -38,7 +38,8 @@ isort:
|
|||||||
isort --recursive src tests
|
isort --recursive src tests
|
||||||
|
|
||||||
dist:
|
dist:
|
||||||
./setup.py sdist bdist_wheel
|
pip install wheel
|
||||||
|
python ./setup.py sdist bdist_wheel
|
||||||
|
|
||||||
sandbox:
|
sandbox:
|
||||||
pip install -r sandbox/requirements.txt
|
pip install -r sandbox/requirements.txt
|
||||||
|
36
README.rst
@ -3,17 +3,24 @@
|
|||||||
.. image:: https://readthedocs.org/projects/wagtail-personalisation/badge/?version=latest
|
.. image:: https://readthedocs.org/projects/wagtail-personalisation/badge/?version=latest
|
||||||
:target: http://wagtail-personalisation.readthedocs.io/en/latest/?badge=latest
|
:target: http://wagtail-personalisation.readthedocs.io/en/latest/?badge=latest
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/LabD/wagtail-personalisation.svg?branch=master
|
.. image:: https://travis-ci.org/wagtail/wagtail-personalisation.svg?branch=master
|
||||||
:target: https://travis-ci.org/LabD/wagtail-personalisation
|
:target: https://travis-ci.org/wagtail/wagtail-personalisation
|
||||||
|
|
||||||
.. image:: http://codecov.io/github/LabD/wagtail-personalisation/coverage.svg?branch=master
|
.. image:: http://codecov.io/github/wagtail/wagtail-personalisation/coverage.svg?branch=master
|
||||||
:target: http://codecov.io/github/LabD/wagtail-personalisation?branch=master
|
:target: http://codecov.io/github/wagtail/wagtail-personalisation?branch=master
|
||||||
|
|
||||||
.. image:: https://img.shields.io/pypi/v/wagtail-personalisation.svg
|
.. image:: https://img.shields.io/pypi/v/wagtail-personalisation.svg
|
||||||
:target: https://pypi.python.org/pypi/wagtail-personalisation/
|
:target: https://pypi.python.org/pypi/wagtail-personalisation/
|
||||||
|
|
||||||
.. end-no-pypi
|
.. end-no-pypi
|
||||||
|
|
||||||
|
.. image:: logo.png
|
||||||
|
:height: 261
|
||||||
|
:width: 300
|
||||||
|
:scale: 50
|
||||||
|
:alt: Wagxperience
|
||||||
|
:align: center
|
||||||
|
|
||||||
Wagtail Personalisation
|
Wagtail Personalisation
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
@ -24,20 +31,17 @@ in the admin interface.
|
|||||||
|
|
||||||
.. _Wagtail CMS: http://wagtail.io/
|
.. _Wagtail CMS: http://wagtail.io/
|
||||||
|
|
||||||
.. image:: logo.png
|
|
||||||
:scale: 50 %
|
|
||||||
:alt: Wagxperience
|
|
||||||
:align: center
|
|
||||||
|
|
||||||
|
|
||||||
.. image:: screenshot.png
|
.. image:: screenshot.png
|
||||||
|
|
||||||
|
|
||||||
Instructions
|
Instructions
|
||||||
------------
|
------------
|
||||||
Wagtail Personalisation requires Wagtail 1.9 or 1.10 and Django 1.9, 1.10 or 1.11.
|
Wagtail Personalisation requires Wagtail 2.0 or 2.1 and Django 1.11 or 2.0.
|
||||||
|
|
||||||
To install the package with pip::
|
To install the package with pip:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
pip install wagtail-personalisation
|
pip install wagtail-personalisation
|
||||||
|
|
||||||
@ -64,6 +68,16 @@ been added in first, this is a prerequisite for this project.
|
|||||||
# ...
|
# ...
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
You can find more information about installing, extending and using this module
|
||||||
|
on `Read the Docs`_.
|
||||||
|
|
||||||
|
.. _Read the Docs: http://wagtail-personalisation.readthedocs.io
|
||||||
|
|
||||||
|
|
||||||
Sandbox
|
Sandbox
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
BIN
docs/_static/images/dual_streamfield.png
vendored
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
docs/_static/images/edit_segment_rules.png
vendored
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
docs/_static/images/edit_segment_specifics.png
vendored
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
docs/_static/images/editing_variant.png
vendored
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
docs/_static/images/segment_dashboard_header.png
vendored
Normal file
After Width: | Height: | Size: 9.7 KiB |
BIN
docs/_static/images/segment_dashboard_view.png
vendored
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
docs/_static/images/segment_list_view.png
vendored
Normal file
After Width: | Height: | Size: 63 KiB |
BIN
docs/_static/images/single_streamfield.png
vendored
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
docs/_static/images/variants_button.png
vendored
Normal file
After Width: | Height: | Size: 89 KiB |
30
docs/conf.py
@ -17,10 +17,17 @@
|
|||||||
# add these directories to sys.path here. If the directory is relative to the
|
# 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.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
#
|
#
|
||||||
# import os
|
import os
|
||||||
# import sys
|
import sys
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
|
|
||||||
|
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
|
||||||
|
|
||||||
|
if not on_rtd: # only import and set the theme if we're building docs locally
|
||||||
|
import sphinx_rtd_theme
|
||||||
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
@ -47,7 +54,7 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = 'wagtail-personalisation'
|
project = 'wagtail-personalisation'
|
||||||
copyright = '2017, Lab Digital BV'
|
copyright = '2018, Lab Digital BV'
|
||||||
author = 'Lab Digital BV'
|
author = 'Lab Digital BV'
|
||||||
|
|
||||||
# The version info for the project you're documenting, acts as replacement for
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
@ -55,17 +62,17 @@ author = 'Lab Digital BV'
|
|||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.9.1'
|
version = '0.12.0'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.9.1'
|
release = '0.12.0'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
#
|
#
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
# Usually you set "language" from the command line for these cases.
|
# Usually you set "language" from the command line for these cases.
|
||||||
language = None
|
# language = None
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
@ -84,7 +91,7 @@ todo_include_todos = False
|
|||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
html_theme = 'alabaster'
|
# html_theme = 'alabaster'
|
||||||
|
|
||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# 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
|
# further. For a list of options available for each theme, see the
|
||||||
@ -92,14 +99,11 @@ html_theme = 'alabaster'
|
|||||||
#
|
#
|
||||||
# html_theme_options = {}
|
# html_theme_options = {}
|
||||||
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',
|
'analytics_id': 'UA-100203499-2',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html_logo = 'logo.png'
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# 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,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
Included rules
|
Included rules
|
||||||
==============
|
==============
|
||||||
|
|
||||||
|
Wagxperience comes with a base set of rules that allow you to start segmenting
|
||||||
|
your visitors quickly.
|
||||||
|
|
||||||
|
|
||||||
Time rule
|
Time rule
|
||||||
---------
|
---------
|
||||||
|
|
||||||
@ -16,11 +20,12 @@ End time The end time of your time frame.
|
|||||||
|
|
||||||
``wagtail_personalisation.rules.TimeRule``
|
``wagtail_personalisation.rules.TimeRule``
|
||||||
|
|
||||||
|
|
||||||
Day rule
|
Day rule
|
||||||
--------
|
--------
|
||||||
|
|
||||||
The day rule allows you to segment visitors based on the day of their visit.
|
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.
|
Select one or multiple days on which you would like your segment to be applied.
|
||||||
|
|
||||||
================== ==========================================================
|
================== ==========================================================
|
||||||
Option Description
|
Option Description
|
||||||
@ -36,6 +41,7 @@ Sunday Matches when the visitors visits on a sunday.
|
|||||||
|
|
||||||
``wagtail_personalisation.rules.DayRule``
|
``wagtail_personalisation.rules.DayRule``
|
||||||
|
|
||||||
|
|
||||||
Referral rule
|
Referral rule
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
@ -54,6 +60,7 @@ Regex string The regex string to match the referral header to.
|
|||||||
|
|
||||||
``wagtail_personalisation.rules.ReferralRule``
|
``wagtail_personalisation.rules.ReferralRule``
|
||||||
|
|
||||||
|
|
||||||
Visit count rule
|
Visit count rule
|
||||||
----------------
|
----------------
|
||||||
|
|
||||||
@ -72,6 +79,7 @@ Operator Whether to match for more than, less than or equal to the
|
|||||||
|
|
||||||
``wagtail_personalisation.rules.VisitCountRule``
|
``wagtail_personalisation.rules.VisitCountRule``
|
||||||
|
|
||||||
|
|
||||||
Query rule
|
Query rule
|
||||||
----------
|
----------
|
||||||
|
|
||||||
@ -92,6 +100,7 @@ Value The second part of the query ('ourbestoffer').
|
|||||||
|
|
||||||
``wagtail_personalisation.rules.QueryRule``
|
``wagtail_personalisation.rules.QueryRule``
|
||||||
|
|
||||||
|
|
||||||
Device rule
|
Device rule
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
@ -108,6 +117,7 @@ Desktop Matches when the visitor uses a desktop.
|
|||||||
|
|
||||||
``wagtail_personalisation.rules.DeviceRule``
|
``wagtail_personalisation.rules.DeviceRule``
|
||||||
|
|
||||||
|
|
||||||
User is logged in rule
|
User is logged in rule
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
91
docs/editor_guide/creating_personalised_content.rst
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
Creating personalised content
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Once you've created a segment you can start serving personalised content to your
|
||||||
|
visitors. To do this, you can choose one of three methods.
|
||||||
|
|
||||||
|
1. Create a page variant for a segment.
|
||||||
|
2. Use StreamField blocks visible for a segment only.
|
||||||
|
3. Use a template block visible for a segment only.
|
||||||
|
|
||||||
|
|
||||||
|
Method 1: Create a page variant
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
**Why you would want to use this method**
|
||||||
|
|
||||||
|
* It has absolutely no restrictions, you can change anything you want.
|
||||||
|
* That's pretty much it.
|
||||||
|
|
||||||
|
**Why you would want to use a different method**
|
||||||
|
|
||||||
|
* You are editing a page that changes often. You would probably rather not
|
||||||
|
change the variation(s) every time the original page changes.
|
||||||
|
|
||||||
|
To create a variant of a page for a specific Segment (which you can change to
|
||||||
|
your liking after creating it), simply go to the Explorer section and find the
|
||||||
|
page you would like to personalize.
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/variants_button.png
|
||||||
|
:alt: The variants button that appears on personalisable pages.
|
||||||
|
|
||||||
|
When you hover over a page, you'll notice a "Variants" dropdown button appears.
|
||||||
|
Click the button and select the segment you would like to create personalised
|
||||||
|
content for.
|
||||||
|
|
||||||
|
Once you've selected the segment, a copy of the original page will be created
|
||||||
|
with a title that includes the segment. Don't worry, your visitors won't be able
|
||||||
|
to see this title. It's only there for your reference.
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/editing_variant.png
|
||||||
|
:alt: The newly created page allowing you to change anything you want.
|
||||||
|
|
||||||
|
You can change everything on this page you would like. Visitors that are appointed
|
||||||
|
to your segment will automatically see the new variant you've created for them
|
||||||
|
when attempting to visit the original page.
|
||||||
|
|
||||||
|
|
||||||
|
Method 2: Use a StreamField block
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
Preparing a page and it's StreamField blocks for this method is described in the
|
||||||
|
Usage guide for developers. Please refer to
|
||||||
|
:ref:`implementing_streamfield_blocks` for more information.
|
||||||
|
|
||||||
|
**Why you would want to use this method**
|
||||||
|
|
||||||
|
* Allows you to create personalised content in the original page (without
|
||||||
|
creating a variant).
|
||||||
|
* Create multiple StreamField blocks for different segments inline.
|
||||||
|
|
||||||
|
**Why you would want to use a different method**
|
||||||
|
|
||||||
|
* You need someone tech savvy to change the back-end implementation.
|
||||||
|
|
||||||
|
To create personalised StreamField blocks, first select the page you wan't to
|
||||||
|
create the content for. Note that the personalisable StreamField blocks must be
|
||||||
|
activated on the page by your developer.
|
||||||
|
|
||||||
|
Scroll down to the block containing the StreamField and add a personalisable
|
||||||
|
block. The first input field in the block is a dropdown allowing you to select
|
||||||
|
the segment this StreamField block is ment for.
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/single_streamfield.png
|
||||||
|
:alt: Create a new StreamField block and select the segment.
|
||||||
|
|
||||||
|
If you want, you can even add multiple blocks and change the segment to show
|
||||||
|
different content between segments!
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/dual_streamfield.png
|
||||||
|
:alt: You can even create multiple variations of the same block!
|
||||||
|
|
||||||
|
Once saved, the page will selectively show StreamField blocks based on the
|
||||||
|
visitor's segment.
|
||||||
|
|
||||||
|
|
||||||
|
Method 3: Use a template block
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Setting up content in this manner is described in the Usage guide for
|
||||||
|
developers. Please refer to :ref:`implementing_template_blocks` for more
|
||||||
|
information.
|
84
docs/editor_guide/creating_segments.rst
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
Creating a segment
|
||||||
|
==================
|
||||||
|
|
||||||
|
To create a segment, go to the "Segments dashboard" and click "Add segment".
|
||||||
|
You can find the segments dashboard in the administration menu on the left of
|
||||||
|
the page.
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/segment_dashboard_header.png
|
||||||
|
:alt: The segment dashboard header containing the "Add segment" button.
|
||||||
|
|
||||||
|
On this page you will be presented with two forms. One with specific information
|
||||||
|
about your segment, the other allowing you to choose and configure your
|
||||||
|
rules.
|
||||||
|
|
||||||
|
|
||||||
|
Set segment specific options
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/edit_segment_specifics.png
|
||||||
|
:alt: The form that allows you to configure specific segment options.
|
||||||
|
|
||||||
|
1. Enter a name for your segment
|
||||||
|
|
||||||
|
Choose something meaningful like "Newsletter campaign visitors". This will
|
||||||
|
ensure you'll have a general idea which visitors are in this segment in
|
||||||
|
other parts of the administration interface.
|
||||||
|
|
||||||
|
2. Select the status of the segment *Optional*
|
||||||
|
|
||||||
|
You will generally keep this one **enabled**. If for some reason you want
|
||||||
|
to disable the segment, you can change this to **disabled**.
|
||||||
|
|
||||||
|
3. Set the segment persistence. *Optional*
|
||||||
|
|
||||||
|
When persistence is **enabled**, your segment will stick to the visitor once
|
||||||
|
applied, even if the rules no longer match the next visit.
|
||||||
|
|
||||||
|
4. Select whether to match any or all defined rules. *Optional*
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
5. The segment type *Required*
|
||||||
|
|
||||||
|
**Dynamic**: Users in this segment will change as more or less meet the
|
||||||
|
rules specified in the segment.
|
||||||
|
|
||||||
|
**Static**: If the segment contains only static compatible rules the segment
|
||||||
|
will contain the members that pass those rules when the segment is created.
|
||||||
|
Mixed static segments or those containing entirely non static compatible
|
||||||
|
rules will be populated using the count variable.
|
||||||
|
|
||||||
|
6. The segment count *Optional*
|
||||||
|
|
||||||
|
If this number is set for a static segment users will be added to the set
|
||||||
|
until the number is reached. After this no more users will be added.
|
||||||
|
|
||||||
|
7. Randomisation percentage *Optional*
|
||||||
|
|
||||||
|
If this number is set each user matching the rules will have this percentage
|
||||||
|
chance of being placed in the segment.
|
||||||
|
|
||||||
|
Defining rules
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/edit_segment_rules.png
|
||||||
|
:alt: The form that allows you to set the rules for a segment.
|
||||||
|
|
||||||
|
5. Choose the rules you want to use.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
The rules that come with Wagxperience are as follows:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
../default_rules
|
||||||
|
|
||||||
|
Click "save" to store your segment. It will be enabled by default, unless
|
||||||
|
otherwise defined.
|
13
docs/editor_guide/index.rst
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Editor Guide
|
||||||
|
============
|
||||||
|
|
||||||
|
The editor guide is meant for content editors and marketers using Wagxperience
|
||||||
|
to offer a personalised experience to their visitors.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
introduction
|
||||||
|
segments_dashboard
|
||||||
|
creating_segments
|
||||||
|
creating_personalised_content
|
22
docs/editor_guide/introduction.rst
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
Introduction
|
||||||
|
============
|
||||||
|
|
||||||
|
Wagxperience_ is an open source module developed by `Lab Digital`_ for the
|
||||||
|
Wagtail_ content management system. It allows editors and marketeers to create
|
||||||
|
personalised experiences by harnessing the power of segmentation and rules.
|
||||||
|
|
||||||
|
.. _Wagxperience: http://wagxperience.io
|
||||||
|
.. _Wagtail: https://wagtail.io
|
||||||
|
.. _Lab Digital: http://labdigital.nl
|
||||||
|
|
||||||
|
In this guide, we'll take you step by step through the process of offering your
|
||||||
|
visitors a tailor made online experience. The subjects covered are:
|
||||||
|
|
||||||
|
* Using the segments dashboard
|
||||||
|
* Defining a new segment
|
||||||
|
* Setting up rules used to match visitors to a segment
|
||||||
|
* Personalize a page by creating a variant
|
||||||
|
* Using the StreamField to personalize content blocks
|
||||||
|
* And even more helpful stuff...
|
||||||
|
|
||||||
|
So without further ado, let's get started!
|
84
docs/editor_guide/segments_dashboard.rst
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
The segments dashboard
|
||||||
|
======================
|
||||||
|
|
||||||
|
Wagxperience comes with two different views for it's segment dashboard. A "list
|
||||||
|
view" and a "dashboard view". Where the dashboard view attempts to show all
|
||||||
|
relevant information and statistics in a visually pleasing manner, the list view
|
||||||
|
is more fitted for sites using large amounts of segments, as it may be
|
||||||
|
considered more clear in these cases.
|
||||||
|
|
||||||
|
|
||||||
|
Switching between views
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
By default, Wagxperience's "dashboard view" is active on the segment dashboard.
|
||||||
|
If you would like to switch between the dashboard view and list view, open the
|
||||||
|
segment dashboard and click the "Switch view" button in the green header at the
|
||||||
|
top of the page.
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/segment_dashboard_header.png
|
||||||
|
:alt: The header containing the "Switch view" button.
|
||||||
|
|
||||||
|
|
||||||
|
Using the list view
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Advantages of using the list view:
|
||||||
|
|
||||||
|
* Uses the familiar table view that is used on many other parts of the Wagtail
|
||||||
|
administration interface.
|
||||||
|
* Offers a better overview for large amounts of segments.
|
||||||
|
* Allows for reordering based on fields, such as name or status.
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/segment_list_view.png
|
||||||
|
:alt: The segment list view.
|
||||||
|
|
||||||
|
|
||||||
|
Definitions
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
Name
|
||||||
|
The name of your segment.
|
||||||
|
|
||||||
|
Persistent
|
||||||
|
If this is disabled (default), whenever a visitor requests a page, the rules
|
||||||
|
of this segment are reevaluated. This means that when the rules no longer
|
||||||
|
match, the visitor is no longer a part of this segment. However, if
|
||||||
|
persistence is enabled, this segment will "stick" with the visitor, even when
|
||||||
|
the rules no longer apply.
|
||||||
|
|
||||||
|
Match any
|
||||||
|
If this is disabled (default) all rules of this segment must match a visitor
|
||||||
|
before the visitor is appointed to this segment. If this is enabled, only 1
|
||||||
|
rule has to match before the visitor is appointed.
|
||||||
|
|
||||||
|
Status
|
||||||
|
Indicates whether this segment is active (default) or inactive. If it has
|
||||||
|
been set to 'inactive', visitors will not be appointed to this segment and no
|
||||||
|
personalised content for this segment will be shown to visitors.
|
||||||
|
|
||||||
|
Page count
|
||||||
|
The amount of pages that have variants using this segment.
|
||||||
|
|
||||||
|
Variant count
|
||||||
|
The total amount of variants for this segment. Does not yet apply, as this
|
||||||
|
will always match the amount of pages in the "Page count".
|
||||||
|
|
||||||
|
Statistics
|
||||||
|
Shows the amount of visits of this segment and the days it has been
|
||||||
|
enabled. If the segment is disabled and then re-enabled, these statistics
|
||||||
|
will reset.
|
||||||
|
|
||||||
|
|
||||||
|
Using the dashboard view
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
Advantages of using the dashboard view:
|
||||||
|
|
||||||
|
* Offers a more pleasing visual representation of segments.
|
||||||
|
* Focused on giving insights about your segments at a glance.
|
||||||
|
* Shows the actual rules of a segment.
|
||||||
|
* Gives more wordy explanation about the information shown.
|
||||||
|
|
||||||
|
.. figure:: ../_static/images/segment_dashboard_view.png
|
||||||
|
:alt: The segment dashboard view.
|
@ -1,32 +0,0 @@
|
|||||||
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`
|
|
8
docs/getting_started/index.rst
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Getting Started
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
installation
|
||||||
|
sandbox
|
37
docs/getting_started/installation.rst
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
Installing Wagxperience
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Wagtail Personalisation requires Wagtail_ 2.0 or 2.1 and Django_ 1.11 or 2.0.
|
||||||
|
|
||||||
|
.. _Wagtail: https://github.com/wagtail/wagtail
|
||||||
|
.. _Django: https://github.com/django/django
|
||||||
|
|
||||||
|
|
||||||
|
To install the package with pip:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pip install wagtail-personalisation
|
||||||
|
|
||||||
|
Next, include the ``wagtail_personalisation``, ``wagtail.contrib.modeladmin``
|
||||||
|
and ``wagtailfontawesome`` apps in your project's ``INSTALLED_APPS``:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
# ...
|
||||||
|
'wagtail.contrib.modeladmin',
|
||||||
|
'wagtail_personalisation',
|
||||||
|
'wagtailfontawesome',
|
||||||
|
# ...
|
||||||
|
]
|
||||||
|
|
||||||
|
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',
|
||||||
|
# ...
|
||||||
|
]
|
14
docs/getting_started/sandbox.rst
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
Using the sandbox
|
||||||
|
=================
|
||||||
|
|
||||||
|
To experiment with the package you can use the sandbox provided in
|
||||||
|
the repository_. It includes a couple of segments with rules, a personalisable
|
||||||
|
page with a variant and a personalisable StreamField block.
|
||||||
|
|
||||||
|
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``.
|
||||||
|
|
||||||
|
.. _repository: https://github.com/LabD/wagtail-personalisation
|
@ -1,87 +0,0 @@
|
|||||||
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`
|
|
@ -3,22 +3,49 @@
|
|||||||
You can adapt this file completely to your liking, but it should at least
|
You can adapt this file completely to your liking, but it should at least
|
||||||
contain the root `toctree` directive.
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
Welcome to the Wagxperience documentation!
|
Welcome to the Wagxperience documentation
|
||||||
==========================================
|
=========================================
|
||||||
|
|
||||||
|
.. 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/wagtail/wagtail-personalisation.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/wagtail/wagtail-personalisation
|
||||||
|
|
||||||
|
.. image:: http://codecov.io/github/wagtail/wagtail-personalisation/coverage.svg?branch=master
|
||||||
|
:target: http://codecov.io/github/wagtail/wagtail-personalisation?branch=master
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/pypi/v/wagtail-personalisation.svg
|
||||||
|
:target: https://pypi.python.org/pypi/wagtail-personalisation/
|
||||||
|
|
||||||
|
|
||||||
|
Wagxperience is a fully-featured personalisation module for Wagtail.
|
||||||
|
It enables editors to create customised pages - or parts of pages - based on
|
||||||
|
segments whose rules are configured directly in the admin interface.
|
||||||
|
|
||||||
|
|
||||||
|
* **Get up and running**
|
||||||
|
|
||||||
|
* :doc:`getting_started/index`
|
||||||
|
|
||||||
|
|
||||||
|
* **For developers**
|
||||||
|
|
||||||
|
* :doc:`usage_guide/index`
|
||||||
|
|
||||||
|
|
||||||
|
* **For editors & marketeers**
|
||||||
|
|
||||||
|
* :doc:`editor_guide/index`
|
||||||
|
|
||||||
|
|
||||||
|
Index
|
||||||
|
-----
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
:caption: Contents:
|
|
||||||
|
getting_started/index
|
||||||
getting_started
|
usage_guide/index
|
||||||
implementation
|
editor_guide/index
|
||||||
usage_guide
|
|
||||||
default_rules
|
default_rules
|
||||||
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
||||||
|
BIN
docs/logo.png
Normal file
After Width: | Height: | Size: 110 KiB |
@ -1,95 +0,0 @@
|
|||||||
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.
|
|
17
docs/usage_guide/custom_rules.rst
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
Creating custom rules
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Rules consist of two important elements, the model fields and the
|
||||||
|
``test_user`` function. They should inherit the ``AbstractBaseRule`` class from
|
||||||
|
``wagtail_personalisation.rules``.
|
||||||
|
|
||||||
|
A simple example of a rule could look something like this:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../src/wagtail_personalisation/rules.py
|
||||||
|
:pyobject: UserIsLoggedInRule
|
||||||
|
|
||||||
|
As you can see, the only real requirement is the ``test_user`` function that
|
||||||
|
will either return ``True`` or ``False`` based on the model fields and
|
||||||
|
optionally the request object.
|
||||||
|
|
||||||
|
That's it!
|
71
docs/usage_guide/implementation.rst
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
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:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from wagtail.wagtailcore.models import Page
|
||||||
|
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||||
|
|
||||||
|
class HomePage(PersonalisablePageMixin, Page):
|
||||||
|
pass
|
||||||
|
|
||||||
|
All you need is the ``PersonalisablePageMixin`` mixin and a Wagtail ``Page``
|
||||||
|
class of your liking.
|
||||||
|
|
||||||
|
|
||||||
|
.. _implementing_streamfield_blocks:
|
||||||
|
|
||||||
|
Adding personalisable StreamField blocks
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
Taking things a step further, you can also add personalisable StreamField blocks
|
||||||
|
to your page models. Below is the full Homepage model used in the sandbox.
|
||||||
|
|
||||||
|
.. literalinclude:: ../../sandbox/sandbox/apps/home/models.py
|
||||||
|
|
||||||
|
|
||||||
|
.. _implementing_template_blocks:
|
||||||
|
|
||||||
|
Using template blocks for personalisation
|
||||||
|
-----------------------------------------
|
||||||
|
|
||||||
|
*Please note that using the personalisable template tag is not the recommended
|
||||||
|
method for adding personalisation to your content, as it is largely decoupled
|
||||||
|
from the administration interface. Use responsibly.*
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% 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:
|
||||||
|
|
||||||
|
.. code-block:: jinja
|
||||||
|
|
||||||
|
{% 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.
|
8
docs/usage_guide/index.rst
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Usage Guide
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 3
|
||||||
|
|
||||||
|
implementation
|
||||||
|
custom_rules
|
0
frontend/img/.gitkeep
Normal file
@ -86,6 +86,11 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
.stat_card {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block_container .block span.icon::before {
|
.block_container .block span.icon::before {
|
||||||
@ -93,11 +98,6 @@
|
|||||||
vertical-align: bottom;
|
vertical-align: bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block_container .block .inspect_container .inspect li {
|
|
||||||
display: inline-block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block_container .block .inspect_container .inspect li span {
|
.block_container .block .inspect_container .inspect li span {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
BIN
logo.png
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 103 KiB |
BIN
logo_bw.png
Normal file
After Width: | Height: | Size: 98 KiB |
@ -42,12 +42,12 @@
|
|||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/LabD/wagtail-personalisation.git"
|
"url": "git+https://github.com/wagtail/wagtail-personalisation.git"
|
||||||
},
|
},
|
||||||
"author": "Lab Digital",
|
"author": "Lab Digital",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/LabD/wagtail-personalisation/issues"
|
"url": "https://github.com/wagtail/wagtail-personalisation/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/LabD/wagtail-personalisation#readme"
|
"homepage": "https://github.com/wagtail/wagtail-personalisation#readme"
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
"enable_date": "2017-06-02T10:58:39.389Z",
|
"enable_date": "2017-06-02T10:58:39.389Z",
|
||||||
"disable_date": "2017-06-02T10:34:51.722Z",
|
"disable_date": "2017-06-02T10:34:51.722Z",
|
||||||
"visit_count": 0,
|
"visit_count": 0,
|
||||||
"status": "enabled",
|
"enabled": true,
|
||||||
"persistent": false,
|
"persistent": false,
|
||||||
"match_any": false
|
"match_any": false
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"enable_date": "2017-06-02T10:57:44.497Z",
|
"enable_date": "2017-06-02T10:57:44.497Z",
|
||||||
"disable_date": "2017-06-02T10:57:39.984Z",
|
"disable_date": "2017-06-02T10:57:39.984Z",
|
||||||
"visit_count": 1,
|
"visit_count": 1,
|
||||||
"status": "enabled",
|
"enabled": true,
|
||||||
"persistent": false,
|
"persistent": false,
|
||||||
"match_any": false
|
"match_any": false
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Django>=1.11,<1.12
|
Django>=2.0,<2.1
|
||||||
wagtail>=1.10,<1.11
|
wagtail>=2.1,<2.2
|
||||||
django-debug-toolbar==1.8
|
django-debug-toolbar==1.9.1
|
||||||
-e .[docs,test]
|
-e .[docs,test]
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
import wagtail.wagtailcore.fields
|
import wagtail.core.fields
|
||||||
import wagtail_personalisation
|
import wagtail_personalisation
|
||||||
|
|
||||||
|
|
||||||
@ -17,14 +17,14 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='homepage',
|
model_name='homepage',
|
||||||
name='intro',
|
name='intro',
|
||||||
field=wagtail.wagtailcore.fields.RichTextField(
|
field=wagtail.core.fields.RichTextField(
|
||||||
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
|
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='homepage',
|
model_name='homepage',
|
||||||
name='body',
|
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=''),
|
field=wagtail.core.fields.StreamField((('personalisable_paragraph', wagtail.core.blocks.StructBlock((('segment', wagtail.core.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.core.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
|
from wagtail.admin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
|
||||||
from wagtail.wagtailcore import blocks
|
from wagtail.core import blocks
|
||||||
from wagtail.wagtailcore.fields import RichTextField, StreamField
|
from wagtail.core.fields import RichTextField, StreamField
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||||
from wagtail_personalisation.blocks import PersonalisedStructBlock
|
from wagtail_personalisation.blocks import PersonalisedStructBlock
|
||||||
|
@ -3,8 +3,8 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
from wagtail.wagtailsearch.models import Query
|
from wagtail.search.models import Query
|
||||||
|
|
||||||
|
|
||||||
def search(request):
|
def search(request):
|
||||||
|
@ -29,21 +29,30 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'wagtail.wagtailforms',
|
'django.contrib.admin',
|
||||||
'wagtail.wagtailredirects',
|
'django.contrib.auth',
|
||||||
'wagtail.wagtailembeds',
|
'django.contrib.contenttypes',
|
||||||
'wagtail.wagtailsites',
|
'django.contrib.messages',
|
||||||
'wagtail.wagtailusers',
|
'django.contrib.sessions',
|
||||||
'wagtail.wagtailsnippets',
|
'django.contrib.sites',
|
||||||
'wagtail.wagtaildocs',
|
'django.contrib.staticfiles',
|
||||||
'wagtail.wagtailimages',
|
|
||||||
'wagtail.wagtailsearch',
|
'wagtail.contrib.forms',
|
||||||
'wagtail.wagtailadmin',
|
'wagtail.contrib.redirects',
|
||||||
'wagtail.wagtailcore',
|
'wagtail.embeds',
|
||||||
|
'wagtail.sites',
|
||||||
|
'wagtail.users',
|
||||||
|
'wagtail.snippets',
|
||||||
|
'wagtail.documents',
|
||||||
|
'wagtail.images',
|
||||||
|
'wagtail.search',
|
||||||
|
'wagtail.admin',
|
||||||
|
'wagtail.core',
|
||||||
'wagtail.contrib.modeladmin',
|
'wagtail.contrib.modeladmin',
|
||||||
|
|
||||||
'wagtailfontawesome',
|
'wagtailfontawesome',
|
||||||
@ -51,13 +60,6 @@ INSTALLED_APPS = [
|
|||||||
'taggit',
|
'taggit',
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
|
|
||||||
'wagtail_personalisation',
|
'wagtail_personalisation',
|
||||||
|
|
||||||
'sandbox.apps.home',
|
'sandbox.apps.home',
|
||||||
@ -68,17 +70,17 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.http.ConditionalGetMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
|
|
||||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
'wagtail.core.middleware.SiteMiddleware',
|
||||||
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'sandbox.urls'
|
ROOT_URLCONF = 'sandbox.urls'
|
||||||
|
@ -4,14 +4,14 @@ import debug_toolbar
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
from wagtail.wagtailcore import urls as wagtail_urls
|
from wagtail.core import urls as wagtail_urls
|
||||||
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
from wagtail.documents import urls as wagtaildocs_urls
|
||||||
|
|
||||||
from sandbox.apps.search import views as search_views
|
from sandbox.apps.search import views as search_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', admin.site.urls),
|
||||||
|
|
||||||
url(r'^cms/', include(wagtailadmin_urls)),
|
url(r'^cms/', include(wagtailadmin_urls)),
|
||||||
url(r'^documents/', include(wagtaildocs_urls)),
|
url(r'^documents/', include(wagtaildocs_urls)),
|
||||||
|
10
setup.cfg
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.9.1
|
current_version = 0.12.0
|
||||||
commit = true
|
commit = true
|
||||||
tag = true
|
tag = true
|
||||||
tag_name = {new_version}
|
tag_name = {new_version}
|
||||||
@ -15,15 +15,15 @@ python_paths = .
|
|||||||
[flake8]
|
[flake8]
|
||||||
ignore = E731
|
ignore = E731
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
exclude =
|
exclude =
|
||||||
src/**/migrations/*.py
|
src/**/migrations/*.py
|
||||||
|
|
||||||
[wheel]
|
[wheel]
|
||||||
universal = 1
|
universal = 1
|
||||||
|
|
||||||
[coverage:run]
|
[coverage]
|
||||||
omit =
|
include = src/**/
|
||||||
src/**/migrations/*.py
|
omit = src/**/migrations/*.py
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
[bumpversion:file:setup.py]
|
||||||
|
|
||||||
|
36
setup.py
@ -1,29 +1,31 @@
|
|||||||
import re
|
import re
|
||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'wagtail>=1.9,<1.11',
|
'wagtail>=2.0,<2.2',
|
||||||
'user-agents>=1.0.1',
|
'user-agents>=1.1.0',
|
||||||
'wagtailfontawesome>=1.0.6',
|
'wagtailfontawesome>=1.1.3',
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
'factory_boy==2.8.1',
|
'factory_boy==2.8.1',
|
||||||
'flake8',
|
|
||||||
'flake8-blind-except',
|
'flake8-blind-except',
|
||||||
'flake8-debugger',
|
'flake8-debugger',
|
||||||
'flake8-imports',
|
'flake8-imports',
|
||||||
|
'flake8',
|
||||||
'freezegun==0.3.8',
|
'freezegun==0.3.8',
|
||||||
'pytest-cov==2.4.0',
|
'pytest-cov==2.5.1',
|
||||||
'pytest-django==3.1.2',
|
'pytest-django==3.1.2',
|
||||||
'pytest-sugar==0.7.1',
|
'pytest-pythonpath==0.7.2',
|
||||||
'pytest==3.1.0',
|
'pytest-sugar==0.9.1',
|
||||||
'wagtail_factories==0.3.0',
|
'pytest==3.4.2',
|
||||||
|
'wagtail_factories==1.0.0',
|
||||||
|
'pytest-mock==1.6.3',
|
||||||
]
|
]
|
||||||
|
|
||||||
docs_require = [
|
docs_require = [
|
||||||
'sphinx>=1.4.0',
|
'sphinx>=1.7.6',
|
||||||
|
'sphinx_rtd_theme>=0.4.0',
|
||||||
]
|
]
|
||||||
|
|
||||||
with open('README.rst') as fh:
|
with open('README.rst') as fh:
|
||||||
@ -32,11 +34,11 @@ with open('README.rst') as fh:
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='wagtail-personalisation',
|
name='wagtail-personalisation',
|
||||||
version='0.9.1',
|
version='0.12.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 and others',
|
||||||
author_email='opensource@labdigital.nl',
|
author_email='opensource@labdigital.nl',
|
||||||
url='http://labdigital.nl',
|
url='https://labdigital.nl/',
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
extras_require={
|
extras_require={
|
||||||
@ -54,16 +56,10 @@ setup(
|
|||||||
'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 :: 2',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Framework :: Django :: 1.9',
|
'Framework :: Django :: 2',
|
||||||
'Framework :: Django :: 1.10',
|
|
||||||
'Framework :: Django :: 1.11',
|
|
||||||
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from wagtail_personalisation.rules import AbstractBaseRule
|
|||||||
from wagtail_personalisation.utils import create_segment_dictionary
|
from wagtail_personalisation.utils import create_segment_dictionary
|
||||||
|
|
||||||
|
|
||||||
class BaseSegmentsAdapter(object):
|
class BaseSegmentsAdapter:
|
||||||
"""Base segments adapter."""
|
"""Base segments adapter."""
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
@ -66,34 +66,48 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
self.request.session.setdefault('segments', [])
|
self.request.session.setdefault('segments', [])
|
||||||
self._segment_cache = None
|
self._segment_cache = None
|
||||||
|
|
||||||
def get_segments(self):
|
def _segments(self, ids=None):
|
||||||
"""Return the persistent segments stored in the request session.
|
if not ids:
|
||||||
|
ids = []
|
||||||
: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 = (
|
segments = (
|
||||||
Segment.objects
|
Segment.objects
|
||||||
.enabled()
|
.enabled()
|
||||||
.filter(persistent=True)
|
.filter(persistent=True)
|
||||||
.in_bulk(segment_ids))
|
.filter(pk__in=ids)
|
||||||
|
)
|
||||||
|
return segments
|
||||||
|
|
||||||
retval = [segments[pk] for pk in segment_ids if pk in segments]
|
def get_segments(self, key="segments"):
|
||||||
self._segment_cache = retval
|
"""Return the persistent segments stored in the request session.
|
||||||
return retval
|
|
||||||
|
|
||||||
def set_segments(self, segments):
|
:param key: The key under which the segments are stored
|
||||||
|
:type key: String
|
||||||
|
:returns: The segments in the request session
|
||||||
|
:rtype: list of wagtail_personalisation.models.Segment or empty list
|
||||||
|
|
||||||
|
"""
|
||||||
|
if key == "segments" and self._segment_cache is not None:
|
||||||
|
return self._segment_cache
|
||||||
|
|
||||||
|
if key not in self.request.session:
|
||||||
|
return []
|
||||||
|
raw_segments = self.request.session[key]
|
||||||
|
segment_ids = [segment['id'] for segment in raw_segments]
|
||||||
|
|
||||||
|
segments = self._segments(ids=segment_ids)
|
||||||
|
|
||||||
|
result = list(segments)
|
||||||
|
if key == "segments":
|
||||||
|
self._segment_cache = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_segments(self, segments, key="segments"):
|
||||||
"""Set the currently active segments
|
"""Set the currently active segments
|
||||||
|
|
||||||
:param segments: The segments to set for the current request
|
:param segments: The segments to set for the current request
|
||||||
:type segments: list of wagtail_personalisation.models.Segment
|
:type segments: list of wagtail_personalisation.models.Segment
|
||||||
|
:param key: The key under which to store the segments. Optional
|
||||||
|
:type key: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
cache_segments = []
|
cache_segments = []
|
||||||
@ -108,8 +122,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
serialized_segments.append(serialized)
|
serialized_segments.append(serialized)
|
||||||
segment_ids.add(segment.pk)
|
segment_ids.add(segment.pk)
|
||||||
|
|
||||||
self.request.session['segments'] = serialized_segments
|
self.request.session[key] = serialized_segments
|
||||||
self._segment_cache = cache_segments
|
if key == "segments":
|
||||||
|
self._segment_cache = cache_segments
|
||||||
|
|
||||||
def get_segment_by_id(self, segment_id):
|
def get_segment_by_id(self, segment_id):
|
||||||
"""Find and return a single segment from the request session.
|
"""Find and return a single segment from the request session.
|
||||||
@ -120,9 +135,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
:rtype: wagtail_personalisation.models.Segment or None
|
:rtype: wagtail_personalisation.models.Segment or None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for segment in self.get_segments():
|
segments = self._segments(ids=[segment_id])
|
||||||
if segment.pk == segment_id:
|
if segments.exists():
|
||||||
return segment
|
return segments.get()
|
||||||
|
|
||||||
def add_page_visit(self, page):
|
def add_page_visit(self, page):
|
||||||
"""Mark the page as visited by the user"""
|
"""Mark the page as visited by the user"""
|
||||||
@ -132,18 +147,19 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
if page_visits:
|
if page_visits:
|
||||||
for page_visit in page_visits:
|
for page_visit in page_visits:
|
||||||
page_visit['count'] += 1
|
page_visit['count'] += 1
|
||||||
|
page_visit['path'] = page.url_path if page else self.request.path
|
||||||
self.request.session.modified = True
|
self.request.session.modified = True
|
||||||
else:
|
else:
|
||||||
visit_count.append({
|
visit_count.append({
|
||||||
'slug': page.slug,
|
'slug': page.slug,
|
||||||
'id': page.pk,
|
'id': page.pk,
|
||||||
'path': self.request.path,
|
'path': page.url_path if page else self.request.path,
|
||||||
'count': 1,
|
'count': 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_visit_count(self, page=None):
|
def get_visit_count(self, page=None):
|
||||||
"""Return the number of visits on the current request or given page"""
|
"""Return the number of visits on the current request or given page"""
|
||||||
path = page.path if page else self.request.path
|
path = page.url_path if page else self.request.path
|
||||||
visit_count = self.request.session.setdefault('visit_count', [])
|
visit_count = self.request.session.setdefault('visit_count', [])
|
||||||
for visit in visit_count:
|
for visit in visit_count:
|
||||||
if visit['path'] == path:
|
if visit['path'] == path:
|
||||||
@ -170,21 +186,40 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
rule_models = AbstractBaseRule.get_descendant_models()
|
rule_models = AbstractBaseRule.get_descendant_models()
|
||||||
|
|
||||||
current_segments = self.get_segments()
|
current_segments = self.get_segments()
|
||||||
|
excluded_segments = self.get_segments("excluded_segments")
|
||||||
|
current_segments = list(
|
||||||
|
set(current_segments) - set(excluded_segments)
|
||||||
|
)
|
||||||
|
|
||||||
# Run tests on all remaining enabled segments to verify applicability.
|
# Run tests on all remaining enabled segments to verify applicability.
|
||||||
additional_segments = []
|
additional_segments = []
|
||||||
for segment in enabled_segments:
|
for segment in enabled_segments:
|
||||||
segment_rules = []
|
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
|
||||||
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)
|
additional_segments.append(segment)
|
||||||
|
elif (segment.excluded_users.filter(id=self.request.user.id).exists() or
|
||||||
|
segment in excluded_segments):
|
||||||
|
continue
|
||||||
|
elif not segment.is_static or not segment.is_full:
|
||||||
|
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 and segment.randomise_into_segment():
|
||||||
|
if segment.is_static and not segment.is_full:
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
segment.static_users.add(self.request.user)
|
||||||
|
additional_segments.append(segment)
|
||||||
|
elif result:
|
||||||
|
if segment.is_static and self.request.user.is_authenticated:
|
||||||
|
segment.excluded_users.add(self.request.user)
|
||||||
|
else:
|
||||||
|
excluded_segments += [segment]
|
||||||
|
|
||||||
self.set_segments(current_segments + additional_segments)
|
self.set_segments(current_segments + additional_segments)
|
||||||
|
self.set_segments(excluded_segments, "excluded_segments")
|
||||||
self.update_visit_count()
|
self.update_visit_count()
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,4 +13,6 @@ urlpatterns = [
|
|||||||
views.copy_page_view, name='copy_page'),
|
views.copy_page_view, name='copy_page'),
|
||||||
url(r'^segment/toggle_segment_view/$',
|
url(r'^segment/toggle_segment_view/$',
|
||||||
views.toggle_segment_view, name='toggle_segment_view'),
|
views.toggle_segment_view, name='toggle_segment_view'),
|
||||||
|
url(r'^segment/users/(?P<segment_id>[0-9]+)$',
|
||||||
|
views.segment_user_data, name='segment_user_data'),
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from wagtail.wagtailcore import blocks
|
from wagtail.core import blocks
|
||||||
|
|
||||||
from wagtail_personalisation.adapters import get_segment_adapter
|
from wagtail_personalisation.adapters import get_segment_adapter
|
||||||
from wagtail_personalisation.models import Segment
|
from wagtail_personalisation.models import Segment
|
||||||
|
138
src/wagtail_personalisation/forms.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import AnonymousUser
|
||||||
|
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||||
|
from django.test.client import RequestFactory
|
||||||
|
from django.utils.lru_cache import lru_cache
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from wagtail.admin.forms import WagtailAdminModelForm
|
||||||
|
|
||||||
|
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1000)
|
||||||
|
def user_from_data(user_id):
|
||||||
|
User = get_user_model()
|
||||||
|
try:
|
||||||
|
return User.objects.get(id=user_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
return AnonymousUser()
|
||||||
|
|
||||||
|
|
||||||
|
class SegmentAdminForm(WagtailAdminModelForm):
|
||||||
|
|
||||||
|
def count_matching_users(self, rules, match_any):
|
||||||
|
""" Calculates how many users match the given static rules
|
||||||
|
"""
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
static_rules = [rule for rule in rules if rule.static]
|
||||||
|
|
||||||
|
if not static_rules:
|
||||||
|
return count
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
users = User.objects.filter(is_active=True, is_staff=False)
|
||||||
|
|
||||||
|
for user in users.iterator():
|
||||||
|
if match_any:
|
||||||
|
if any(rule.test_user(None, user) for rule in static_rules):
|
||||||
|
count += 1
|
||||||
|
elif all(rule.test_user(None, user) for rule in static_rules):
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
return count
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super(SegmentAdminForm, self).clean()
|
||||||
|
Segment = self._meta.model
|
||||||
|
|
||||||
|
rules = [
|
||||||
|
form.instance for formset in self.formsets.values()
|
||||||
|
for form in formset
|
||||||
|
if form not in formset.deleted_forms
|
||||||
|
]
|
||||||
|
consistent = rules and Segment.all_static(rules)
|
||||||
|
|
||||||
|
if cleaned_data.get('type') == Segment.TYPE_STATIC and not cleaned_data.get('count') and not consistent:
|
||||||
|
self.add_error('count', _('Static segments with non-static compatible rules must include a count.'))
|
||||||
|
|
||||||
|
if self.instance.id and self.instance.is_static:
|
||||||
|
if self.has_changed():
|
||||||
|
self.add_error_to_fields(self, excluded=['name', 'enabled'])
|
||||||
|
|
||||||
|
for formset in self.formsets.values():
|
||||||
|
if formset.has_changed():
|
||||||
|
for form in formset:
|
||||||
|
if form not in formset.deleted_forms:
|
||||||
|
self.add_error_to_fields(form)
|
||||||
|
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
def add_error_to_fields(self, form, excluded=list()):
|
||||||
|
for field in form.changed_data:
|
||||||
|
if field not in excluded:
|
||||||
|
form.add_error(field, _('Cannot update a static segment'))
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
is_new = not self.instance.id
|
||||||
|
|
||||||
|
if not self.instance.is_static:
|
||||||
|
self.instance.count = 0
|
||||||
|
|
||||||
|
if is_new and self.instance.is_static and not self.instance.all_rules_static:
|
||||||
|
rules = [
|
||||||
|
form.instance for formset in self.formsets.values()
|
||||||
|
for form in formset
|
||||||
|
if form not in formset.deleted_forms
|
||||||
|
]
|
||||||
|
self.instance.matched_users_count = self.count_matching_users(
|
||||||
|
rules, self.instance.match_any)
|
||||||
|
self.instance.matched_count_updated_at = datetime.now()
|
||||||
|
|
||||||
|
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
|
if is_new and instance.is_static and instance.all_rules_static:
|
||||||
|
from .adapters import get_segment_adapter
|
||||||
|
|
||||||
|
request = RequestFactory().get('/')
|
||||||
|
request.session = SessionStore()
|
||||||
|
adapter = get_segment_adapter(request)
|
||||||
|
|
||||||
|
users_to_add = []
|
||||||
|
users_to_exclude = []
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
users = User.objects.filter(is_active=True, is_staff=False)
|
||||||
|
|
||||||
|
matched_count = 0
|
||||||
|
for user in users.iterator():
|
||||||
|
request.user = user
|
||||||
|
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
|
||||||
|
if passes:
|
||||||
|
matched_count += 1
|
||||||
|
if instance.count == 0 or len(users_to_add) < instance.count:
|
||||||
|
if instance.randomise_into_segment():
|
||||||
|
users_to_add.append(user)
|
||||||
|
else:
|
||||||
|
users_to_exclude.append(user)
|
||||||
|
|
||||||
|
instance.matched_users_count = matched_count
|
||||||
|
instance.matched_count_updated_at = datetime.now()
|
||||||
|
instance.static_users.add(*users_to_add)
|
||||||
|
instance.excluded_users.add(*users_to_exclude)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
@property
|
||||||
|
def media(self):
|
||||||
|
media = super(SegmentAdminForm, self).media
|
||||||
|
media.add_js(
|
||||||
|
[static('js/segment_form_control.js')]
|
||||||
|
)
|
||||||
|
return media
|
@ -2,8 +2,8 @@
|
|||||||
# Generated by Django 1.11.1 on 2017-05-31 14:28
|
# Generated by Django 1.11.1 on 2017-05-31 14:28
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.6 on 2017-10-17 11:18
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('sessions', '0001_initial'),
|
||||||
|
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='count',
|
||||||
|
field=models.PositiveSmallIntegerField(default=0, help_text='If this number is set for a static segment users will be added to the set until the number is reached. After this no more users will be added.'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='sessions',
|
||||||
|
field=models.ManyToManyField(to='sessions.Session'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('dynamic', 'Dynamic'), ('static', 'Static')], default='dynamic', help_text='\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ', max_length=20),
|
||||||
|
),
|
||||||
|
]
|
26
src/wagtail_personalisation/migrations/0015_static_users.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.6 on 2017-11-01 15:58
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('wagtail_personalisation', '0013_add_dynamic_static_to_segment'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='segment',
|
||||||
|
name='sessions',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='static_users',
|
||||||
|
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,25 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-01-25 09:18
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0015_static_users'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='matched_count_updated_at',
|
||||||
|
field=models.DateTimeField(editable=False, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='matched_users_count',
|
||||||
|
field=models.PositiveIntegerField(default=0, editable=False),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.8 on 2018-01-31 16:12
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0016_auto_20180125_0918'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='randomisation_percent',
|
||||||
|
field=models.PositiveSmallIntegerField(blank=True, default=None, help_text='If this number is set each user matching the rules will have this percentage chance of being placed in the segment.', null=True, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.9 on 2018-02-09 08:28
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('wagtail_personalisation', '0017_segment_randomisation_percent'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='excluded_users',
|
||||||
|
field=models.ManyToManyField(help_text='Users that matched the rules but were excluded from the segment for some reason e.g. randomisation', related_name='excluded_segments', to=settings.AUTH_USER_MODEL),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.5 on 2018-05-26 14:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0018_segment_excluded_users'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='personalisablepagemetadata',
|
||||||
|
name='segment',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='page_metadata', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,50 @@
|
|||||||
|
# Generated by Django 2.0.5 on 2018-05-30 18:51
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import modelcluster.fields
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0019_auto_20180526_1425'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='dayrule',
|
||||||
|
name='segment',
|
||||||
|
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_dayrules', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicerule',
|
||||||
|
name='segment',
|
||||||
|
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_devicerules', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='queryrule',
|
||||||
|
name='segment',
|
||||||
|
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_queryrules', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='referralrule',
|
||||||
|
name='segment',
|
||||||
|
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_referralrules', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='timerule',
|
||||||
|
name='segment',
|
||||||
|
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_timerules', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='userisloggedinrule',
|
||||||
|
name='segment',
|
||||||
|
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_userisloggedinrules', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='visitcountrule',
|
||||||
|
name='segment',
|
||||||
|
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_visitcountrules', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.7 on 2018-07-04 15:26
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0020_rules_delete_relatedqueryname'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='personalisablepagemetadata',
|
||||||
|
name='segment',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='page_metadata', to='wagtail_personalisation.Segment'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.7 on 2018-07-05 13:25
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0021_personalisablepagemetadata_segment_set_on_delete_protect'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='personalisablepagemetadata',
|
||||||
|
name='canonical_page',
|
||||||
|
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='personalisable_canonical_metadata', to='wagtailcore.Page'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 2.0.5 on 2018-07-19 09:57
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0022_personalisablepagemetadata_canonical_protect'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='personalisablepagemetadata',
|
||||||
|
name='variant',
|
||||||
|
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page'),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.2 on 2017-08-10 13:48
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
Segment = apps.get_model('wagtail_personalisation', 'Segment')
|
||||||
|
|
||||||
|
for segment in Segment.objects.all():
|
||||||
|
segment.enabled = segment.status == 'enabled'
|
||||||
|
segment.save()
|
||||||
|
|
||||||
|
|
||||||
|
def backward(apps, schema_editor):
|
||||||
|
Segment = apps.get_model('wagtail_personalisation', 'Segment')
|
||||||
|
|
||||||
|
for segment in Segment.objects.all():
|
||||||
|
if segment.enabled:
|
||||||
|
segment.status = 'enabled'
|
||||||
|
else:
|
||||||
|
segment.status = 'disabled'
|
||||||
|
|
||||||
|
segment.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0023_personalisablepagemetadata_variant_cascade'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='enabled',
|
||||||
|
field=models.BooleanField(default=True, help_text='Should the segment be active?'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forward, reverse_code=backward),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='segment',
|
||||||
|
name='status',
|
||||||
|
),
|
||||||
|
]
|
@ -1,33 +1,39 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
import random
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from modelcluster.models import ClusterableModel
|
from modelcluster.models import ClusterableModel
|
||||||
from wagtail.wagtailadmin.edit_handlers import (
|
from wagtail.admin.edit_handlers import (
|
||||||
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
|
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.rules import AbstractBaseRule
|
from wagtail_personalisation.rules import AbstractBaseRule
|
||||||
from wagtail_personalisation.utils import count_active_days
|
from wagtail_personalisation.utils import count_active_days
|
||||||
|
|
||||||
|
from .forms import SegmentAdminForm
|
||||||
|
|
||||||
|
|
||||||
class SegmentQuerySet(models.QuerySet):
|
class SegmentQuerySet(models.QuerySet):
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
return self.filter(status=self.model.STATUS_ENABLED)
|
return self.filter(enabled=True)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Segment(ClusterableModel):
|
class Segment(ClusterableModel):
|
||||||
"""The segment model."""
|
"""The segment model."""
|
||||||
STATUS_ENABLED = 'enabled'
|
TYPE_DYNAMIC = 'dynamic'
|
||||||
STATUS_DISABLED = 'disabled'
|
TYPE_STATIC = 'static'
|
||||||
|
|
||||||
STATUS_CHOICES = (
|
TYPE_CHOICES = (
|
||||||
(STATUS_ENABLED, _('Enabled')),
|
(TYPE_DYNAMIC, _('Dynamic')),
|
||||||
(STATUS_DISABLED, _('Disabled')),
|
(TYPE_STATIC, _('Static')),
|
||||||
)
|
)
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
@ -36,31 +42,82 @@ class Segment(ClusterableModel):
|
|||||||
enable_date = models.DateTimeField(null=True, editable=False)
|
enable_date = models.DateTimeField(null=True, editable=False)
|
||||||
disable_date = models.DateTimeField(null=True, editable=False)
|
disable_date = models.DateTimeField(null=True, editable=False)
|
||||||
visit_count = models.PositiveIntegerField(default=0, editable=False)
|
visit_count = models.PositiveIntegerField(default=0, editable=False)
|
||||||
status = models.CharField(
|
enabled = models.BooleanField(
|
||||||
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED)
|
default=True, help_text=_("Should the segment be active?"))
|
||||||
persistent = models.BooleanField(
|
persistent = models.BooleanField(
|
||||||
default=False, help_text=_("Should the segment persist between visits?"))
|
default=False, help_text=_("Should the segment persist between visits?"))
|
||||||
match_any = models.BooleanField(
|
match_any = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text=_("Should the segment match all the rules or just one of them?")
|
help_text=_("Should the segment match all the rules or just one of them?")
|
||||||
)
|
)
|
||||||
|
type = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
choices=TYPE_CHOICES,
|
||||||
|
default=TYPE_DYNAMIC,
|
||||||
|
help_text=mark_safe(_("""
|
||||||
|
</br></br><strong>Dynamic:</strong> Users in this segment will change
|
||||||
|
as more or less meet the rules specified in the segment.
|
||||||
|
</br><strong>Static:</strong> If the segment contains only static
|
||||||
|
compatible rules the segment will contain the members that pass
|
||||||
|
those rules when the segment is created. Mixed static segments or
|
||||||
|
those containing entirely non static compatible rules will be
|
||||||
|
populated using the count variable.
|
||||||
|
"""))
|
||||||
|
)
|
||||||
|
count = models.PositiveSmallIntegerField(
|
||||||
|
default=0,
|
||||||
|
help_text=_(
|
||||||
|
"If this number is set for a static segment users will be added to the "
|
||||||
|
"set until the number is reached. After this no more users will be added."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
static_users = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
)
|
||||||
|
excluded_users = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
help_text=_("Users that matched the rules but were excluded from the "
|
||||||
|
"segment for some reason e.g. randomisation"),
|
||||||
|
related_name="excluded_segments"
|
||||||
|
)
|
||||||
|
|
||||||
|
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
|
||||||
|
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
|
||||||
|
|
||||||
|
randomisation_percent = models.PositiveSmallIntegerField(
|
||||||
|
null=True, blank=True, default=None,
|
||||||
|
help_text=_(
|
||||||
|
"If this number is set each user matching the rules will "
|
||||||
|
"have this percentage chance of being placed in the segment."
|
||||||
|
), validators=[
|
||||||
|
MaxValueValidator(100),
|
||||||
|
MinValueValidator(0)
|
||||||
|
])
|
||||||
|
|
||||||
objects = SegmentQuerySet.as_manager()
|
objects = SegmentQuerySet.as_manager()
|
||||||
|
|
||||||
|
base_form_class = SegmentAdminForm
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
Segment.panels = [
|
Segment.panels = [
|
||||||
MultiFieldPanel([
|
MultiFieldPanel([
|
||||||
FieldPanel('name', classname="title"),
|
FieldPanel('name', classname="title"),
|
||||||
FieldRowPanel([
|
FieldRowPanel([
|
||||||
FieldPanel('status'),
|
FieldPanel('enabled'),
|
||||||
FieldPanel('persistent'),
|
FieldPanel('persistent'),
|
||||||
]),
|
]),
|
||||||
FieldPanel('match_any'),
|
FieldPanel('match_any'),
|
||||||
|
FieldPanel('type', widget=forms.RadioSelect),
|
||||||
|
FieldPanel('count', classname='count_field'),
|
||||||
|
FieldPanel('randomisation_percent', classname='percent_field'),
|
||||||
], heading="Segment"),
|
], heading="Segment"),
|
||||||
MultiFieldPanel([
|
MultiFieldPanel([
|
||||||
InlinePanel(
|
InlinePanel(
|
||||||
"{}_related".format(rule_model._meta.db_table),
|
"{}s".format(rule_model._meta.db_table),
|
||||||
label=rule_model._meta.verbose_name,
|
label='{}{}'.format(
|
||||||
|
rule_model._meta.verbose_name,
|
||||||
|
' ({})'.format(_('Static compatible')) if rule_model.static else ''
|
||||||
|
),
|
||||||
) for rule_model in AbstractBaseRule.__subclasses__()
|
) for rule_model in AbstractBaseRule.__subclasses__()
|
||||||
], heading=_("Rules")),
|
], heading=_("Rules")),
|
||||||
]
|
]
|
||||||
@ -70,6 +127,23 @@ class Segment(ClusterableModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_static(self):
|
||||||
|
return self.type == self.TYPE_STATIC
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all_static(cls, rules):
|
||||||
|
return all(rule.static for rule in rules)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_rules_static(self):
|
||||||
|
rules = self.get_rules()
|
||||||
|
return rules and self.all_static(rules)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_full(self):
|
||||||
|
return self.static_users.count() >= self.count
|
||||||
|
|
||||||
def encoded_name(self):
|
def encoded_name(self):
|
||||||
"""Return a string with a slug for the segment."""
|
"""Return a string with a slug for the segment."""
|
||||||
return slugify(self.name.lower())
|
return slugify(self.name.lower())
|
||||||
@ -80,15 +154,11 @@ class Segment(ClusterableModel):
|
|||||||
|
|
||||||
def get_used_pages(self):
|
def get_used_pages(self):
|
||||||
"""Return the pages that have variants using this segment."""
|
"""Return the pages that have variants using this segment."""
|
||||||
pages = list(PersonalisablePageMetadata.objects.filter(segment=self))
|
return PersonalisablePageMetadata.objects.filter(segment=self)
|
||||||
|
|
||||||
return pages
|
|
||||||
|
|
||||||
def get_created_variants(self):
|
def get_created_variants(self):
|
||||||
"""Return the variants using this segment."""
|
"""Return the variants using this segment."""
|
||||||
pages = Page.objects.filter(_personalisable_page_metadata__segment=self)
|
return Page.objects.filter(_personalisable_page_metadata__segment=self)
|
||||||
|
|
||||||
return pages
|
|
||||||
|
|
||||||
def get_rules(self):
|
def get_rules(self):
|
||||||
"""Retrieve all rules in the segment."""
|
"""Retrieve all rules in the segment."""
|
||||||
@ -100,29 +170,44 @@ class Segment(ClusterableModel):
|
|||||||
return segment_rules
|
return segment_rules
|
||||||
|
|
||||||
def toggle(self, save=True):
|
def toggle(self, save=True):
|
||||||
self.status = (
|
self.enabled = not self.enabled
|
||||||
self.STATUS_ENABLED if self.status == self.STATUS_DISABLED
|
|
||||||
else self.STATUS_DISABLED)
|
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def randomise_into_segment(self):
|
||||||
|
""" Returns True if randomisation_percent is not set or it generates
|
||||||
|
a random number less than the randomisation_percent
|
||||||
|
This is so there is some randomisation in which users are added to the
|
||||||
|
segment
|
||||||
|
"""
|
||||||
|
if self.randomisation_percent is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if random.randint(1, 100) <= self.randomisation_percent:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class PersonalisablePageMetadata(ClusterableModel):
|
class PersonalisablePageMetadata(ClusterableModel):
|
||||||
"""The personalisable page model. Allows creation of variants with linked
|
"""The personalisable page model. Allows creation of variants with linked
|
||||||
segments.
|
segments.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Canonical pages should not ever be deleted if they have variants
|
||||||
|
# because the variants will be orphaned.
|
||||||
canonical_page = models.ForeignKey(
|
canonical_page = models.ForeignKey(
|
||||||
Page, related_name='personalisable_canonical_metadata',
|
Page, models.PROTECT, related_name='personalisable_canonical_metadata',
|
||||||
on_delete=models.SET_NULL,
|
null=True
|
||||||
blank=True, null=True
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Delete metadata of the variant if its page gets deleted.
|
||||||
variant = models.OneToOneField(
|
variant = models.OneToOneField(
|
||||||
Page, related_name='_personalisable_page_metadata')
|
Page, models.CASCADE, related_name='_personalisable_page_metadata',
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
segment = models.ForeignKey(
|
segment = models.ForeignKey(Segment, models.PROTECT, null=True,
|
||||||
Segment, related_name='page_metadata', null=True, blank=True)
|
related_name='page_metadata')
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def has_variants(self):
|
def has_variants(self):
|
||||||
@ -193,7 +278,7 @@ class PersonalisablePageMetadata(ClusterableModel):
|
|||||||
return Segment.objects.none()
|
return Segment.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class PersonalisablePageMixin(object):
|
class PersonalisablePageMixin:
|
||||||
"""The personalisable page model. Allows creation of variants with linked
|
"""The personalisable page model. Allows creation of variants with linked
|
||||||
segments.
|
segments.
|
||||||
|
|
||||||
|
@ -7,16 +7,16 @@ from wagtail_personalisation.models import Segment
|
|||||||
def check_status_change(sender, instance, *args, **kwargs):
|
def check_status_change(sender, instance, *args, **kwargs):
|
||||||
"""Check if the status has changed. Alter dates accordingly."""
|
"""Check if the status has changed. Alter dates accordingly."""
|
||||||
try:
|
try:
|
||||||
original_status = sender.objects.get(pk=instance.id).status
|
original_status = sender.objects.get(pk=instance.id).enabled
|
||||||
except sender.DoesNotExist:
|
except sender.DoesNotExist:
|
||||||
original_status = ""
|
original_status = None
|
||||||
|
|
||||||
if original_status != instance.status:
|
if original_status != instance.enabled:
|
||||||
if instance.status == instance.STATUS_ENABLED:
|
if instance.enabled is True:
|
||||||
instance.enable_date = timezone.now()
|
instance.enable_date = timezone.now()
|
||||||
instance.visit_count = 0
|
instance.visit_count = 0
|
||||||
return instance
|
return instance
|
||||||
if instance.status == instance.STATUS_DISABLED:
|
if instance.enabled is False:
|
||||||
instance.disable_date = timezone.now()
|
instance.disable_date = timezone.now()
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,27 +2,34 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.sessions.models import Session
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.test.client import RequestFactory
|
||||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from modelcluster.fields import ParentalKey
|
from modelcluster.fields import ParentalKey
|
||||||
from user_agents import parse
|
from user_agents import parse
|
||||||
from wagtail.wagtailadmin.edit_handlers import (
|
from wagtail.admin.edit_handlers import (
|
||||||
FieldPanel, FieldRowPanel, PageChooserPanel)
|
FieldPanel, FieldRowPanel, PageChooserPanel)
|
||||||
|
|
||||||
|
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class AbstractBaseRule(models.Model):
|
class AbstractBaseRule(models.Model):
|
||||||
"""Base for creating rules to segment users with."""
|
"""Base for creating rules to segment users with."""
|
||||||
icon = 'fa-circle-o'
|
icon = 'fa-circle-o'
|
||||||
|
static = False
|
||||||
|
|
||||||
segment = ParentalKey(
|
segment = ParentalKey(
|
||||||
'wagtail_personalisation.Segment',
|
'wagtail_personalisation.Segment',
|
||||||
related_name="%(app_label)s_%(class)s_related",
|
related_name="%(app_label)s_%(class)ss",
|
||||||
related_query_name="%(app_label)s_%(class)ss"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -190,6 +197,7 @@ class VisitCountRule(AbstractBaseRule):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
icon = 'fa-calculator'
|
icon = 'fa-calculator'
|
||||||
|
static = True
|
||||||
|
|
||||||
OPERATOR_CHOICES = (
|
OPERATOR_CHOICES = (
|
||||||
('more_than', _("More than")),
|
('more_than', _("More than")),
|
||||||
@ -218,16 +226,46 @@ class VisitCountRule(AbstractBaseRule):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Visit count Rule')
|
verbose_name = _('Visit count Rule')
|
||||||
|
|
||||||
def test_user(self, request):
|
def _get_user_session(self, user):
|
||||||
|
sessions = Session.objects.iterator()
|
||||||
|
for session in sessions:
|
||||||
|
session_data = session.get_decoded()
|
||||||
|
if session_data.get('_auth_user_id') == str(user.id):
|
||||||
|
return SessionStore(session_key=session.session_key)
|
||||||
|
return SessionStore()
|
||||||
|
|
||||||
|
def test_user(self, request, user=None):
|
||||||
|
# Local import for cyclic import
|
||||||
|
from wagtail_personalisation.adapters import (
|
||||||
|
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
|
||||||
|
|
||||||
|
# Django formsets don't honour 'required' fields so check rule is valid
|
||||||
|
try:
|
||||||
|
self.counted_page
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if user:
|
||||||
|
# Create a fake request so we can use the adapter
|
||||||
|
request = RequestFactory().get('/')
|
||||||
|
request.user = user
|
||||||
|
|
||||||
|
# If we're using the session adapter check for an active session
|
||||||
|
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
|
||||||
|
request.session = self._get_user_session(user)
|
||||||
|
else:
|
||||||
|
request.session = SessionStore()
|
||||||
|
|
||||||
|
elif not request:
|
||||||
|
# Return false if we don't have a user or a request
|
||||||
|
return False
|
||||||
|
|
||||||
operator = self.operator
|
operator = self.operator
|
||||||
segment_count = self.count
|
segment_count = self.count
|
||||||
|
|
||||||
# Local import for cyclic import
|
|
||||||
from wagtail_personalisation.adapters import get_segment_adapter
|
|
||||||
|
|
||||||
adapter = get_segment_adapter(request)
|
adapter = get_segment_adapter(request)
|
||||||
|
|
||||||
visit_count = adapter.get_visit_count()
|
visit_count = adapter.get_visit_count(self.counted_page)
|
||||||
if visit_count and operator == "more_than":
|
if visit_count and operator == "more_than":
|
||||||
if visit_count > segment_count:
|
if visit_count > segment_count:
|
||||||
return True
|
return True
|
||||||
@ -250,6 +288,28 @@ class VisitCountRule(AbstractBaseRule):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_column_header(self):
|
||||||
|
return "Visit count - %s" % self.counted_page
|
||||||
|
|
||||||
|
def get_user_info_string(self, user):
|
||||||
|
# Local import for cyclic import
|
||||||
|
from wagtail_personalisation.adapters import (
|
||||||
|
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
|
||||||
|
|
||||||
|
# Create a fake request so we can use the adapter
|
||||||
|
request = RequestFactory().get('/')
|
||||||
|
request.user = user
|
||||||
|
|
||||||
|
# If we're using the session adapter check for an active session
|
||||||
|
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
|
||||||
|
request.session = self._get_user_session(user)
|
||||||
|
else:
|
||||||
|
request.session = SessionStore()
|
||||||
|
|
||||||
|
adapter = get_segment_adapter(request)
|
||||||
|
visit_count = adapter.get_visit_count(self.counted_page)
|
||||||
|
return str(visit_count)
|
||||||
|
|
||||||
|
|
||||||
class QueryRule(AbstractBaseRule):
|
class QueryRule(AbstractBaseRule):
|
||||||
"""Query rule to segment users based on matching queries.
|
"""Query rule to segment users based on matching queries.
|
||||||
|
@ -1,2 +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}
|
.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 .inspect_container .inspect .stat_card{display:inline-block;margin-bottom:5px;margin-right:10px}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.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*/
|
/*# sourceMappingURL=dashboard.css.map*/
|
@ -1 +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":""}
|
{"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,CAMnB,+DAJO,qBACA,kBACA,iBAAkB,CAItB,yCACI,kBACA,qBAAsB,CAG1B,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 .stat_card {\n display: inline-block;\n margin-bottom: 5px;\n margin-right: 10px;\n }\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 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":""}
|
0
src/wagtail_personalisation/static/img/.gitkeep
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
(function($) {
|
||||||
|
$(document).ready( () => {
|
||||||
|
var count = $('.count_field');
|
||||||
|
var typeRadio = $('input:radio[name="type"]');
|
||||||
|
|
||||||
|
var updateCountDispay = function(value) {
|
||||||
|
if (value == 'dynamic') {
|
||||||
|
count.slideUp(250);
|
||||||
|
} else {
|
||||||
|
count.slideDown(250);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCountDispay(typeRadio.filter(':checked').val());
|
||||||
|
|
||||||
|
typeRadio.change( event => {
|
||||||
|
updateCountDispay(event.target.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(jQuery);
|
@ -22,24 +22,38 @@
|
|||||||
<div class="nice-padding block_container">
|
<div class="nice-padding block_container">
|
||||||
{% if all_count %}
|
{% if all_count %}
|
||||||
{% for segment in object_list %}
|
{% for segment in object_list %}
|
||||||
<div class="block block--{{ segment.status }}" onclick="location.href = 'edit/{{ segment.pk }}'">
|
<div class="block block--{{ segment.enabled|yesno:"enabled,disabled" }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
|
||||||
<h2>{{ segment }}</h2>
|
<h2>{{ segment }}</h2>
|
||||||
|
|
||||||
<div class="inspect_container">
|
<div class="inspect_container">
|
||||||
<ul class="inspect segment_stats">
|
<ul class="inspect segment_stats">
|
||||||
<li class="visit_stat">
|
<li class="stat_card">
|
||||||
{% trans "This segment has been visited" %}
|
{% trans "This segment has been visited" %}
|
||||||
<span class="icon icon-fa-rocket">{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
|
<span class="icon icon-fa-rocket">{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="days_stat">
|
<li class="stat_card">
|
||||||
{% trans "This segment has been active for" %}
|
{% trans "This segment has been active for" %}
|
||||||
<span class="icon icon-fa-calendar">{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
|
<span class="icon icon-fa-calendar">{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
{% if segment.is_static %}
|
||||||
|
<li class="stat_card">
|
||||||
|
{% trans "This segment is Static" %}
|
||||||
|
<span class="icon icon-fa-user">
|
||||||
|
{{ segment.static_users.count|localize }}
|
||||||
|
{% if segment.static_users.count < segment.count %}
|
||||||
|
/ {{ segment.count }} {% trans "member" %}{{ segment.count|pluralize }}
|
||||||
|
{% else %}
|
||||||
|
{% trans "member" %}{{ segment.count|pluralize }}
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<ul class="inspect segment_rules">
|
<ul class="inspect segment_rules">
|
||||||
<li class="match_state {{ segment.match_any|yesno:"any,all" }}">
|
<li class="stat_card {{ segment.match_any|yesno:"any,all" }}">
|
||||||
{% trans "The visitor must match" %}
|
{% trans "The visitor must match" %}
|
||||||
{% if segment.match_any %}
|
{% if segment.match_any %}
|
||||||
<span class="icon icon-fa-cube">{% trans "Any rule" %}</span>
|
<span class="icon icon-fa-cube">{% trans "Any rule" %}</span>
|
||||||
@ -48,7 +62,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="persistent_state {{ segment.persistent|yesno:"persistent,fleeting" }}">
|
<li class="stat_card {{ segment.persistent|yesno:"persistent,fleeting" }}">
|
||||||
{% trans "The persistence of this segment is" %}
|
{% trans "The persistence of this segment is" %}
|
||||||
{% if segment.persistent %}
|
{% if segment.persistent %}
|
||||||
<span class="icon icon-fa-bookmark" title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
|
<span class="icon icon-fa-bookmark" title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
|
||||||
@ -57,8 +71,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% if segment.randomisation_percent is not None %}
|
||||||
|
<li class="stat_card">
|
||||||
|
<span>{{ segment.randomisation_percent }} %</span>
|
||||||
|
{% trans "Chance that visitors matching the rules are added to the segment" %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for rule in segment.get_rules %}
|
{% for rule in segment.get_rules %}
|
||||||
<li class="{{ rule.encoded_name }}">
|
<li class="stat_card {{ rule.encoded_name }}">
|
||||||
{{ rule.description.title }}
|
{{ rule.description.title }}
|
||||||
{% if rule.description.code %}
|
{% if rule.description.code %}
|
||||||
<pre>{{ rule.description.value }}</pre>
|
<pre>{{ rule.description.value }}</pre>
|
||||||
@ -67,17 +88,25 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if segment.matched_users_count > 0 %}
|
||||||
|
<li class="stat_card">
|
||||||
|
<span class="icon icon-fa-user"> {{ segment.matched_users_count }} {% trans "user" %}{{ segment.matched_users_count|pluralize }}</span> {% trans "were possible matches for this segment at creation" %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if user_can_create %}
|
{% if user_can_create %}
|
||||||
<ul class="block_actions">
|
<ul class="block_actions">
|
||||||
{% if segment.status == segment.STATUS_DISABLED %}
|
{% if segment.enabled %}
|
||||||
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
|
|
||||||
{% elif segment.status == segment.STATUS_ENABLED %}
|
|
||||||
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><a href="{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
||||||
|
{% if segment.is_static %}
|
||||||
|
<li><a href="{% url 'segment:segment_user_data' segment.pk %}" title="{% trans "Download user info" %}">download users csv</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="edit/{{ segment.pk }}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
{% extends "modeladmin/delete.html" %}
|
||||||
|
|
||||||
|
{% load i18n modeladmin_tags %}
|
||||||
|
|
||||||
|
{% block content_main %}
|
||||||
|
<div class="nice-padding">
|
||||||
|
{% if protected_error %}
|
||||||
|
<h2>{% blocktrans with view.verbose_name|capfirst as model_name %}{{ model_name }} could not be deleted{% endblocktrans %}</h2>
|
||||||
|
<p>{% blocktrans with instance as instance_name %}'{{ instance_name }}' is currently referenced by other objects, and cannot be deleted without jeopardising data integrity. To delete it successfully, first remove references from the following objects, then try again:{% endblocktrans %}</p>
|
||||||
|
<ul>
|
||||||
|
{% for obj in linked_objects %}<li><b>{{ obj|get_content_type_for_obj|title }}:</b> {{ obj }}</li>{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p><a href="{{ view.index_url }}" class="button">{% trans 'Go back to listing' %}</a></p>
|
||||||
|
{% elif cannot_delete_page_variants_error %}
|
||||||
|
<h2>{% blocktrans %}Cannot delete all the page variants{% endblocktrans %}</h2>
|
||||||
|
<p>{% blocktrans %}You need to have permissions to delete the page variants associated with this segment.{% endblocktrans %}
|
||||||
|
{% else %}
|
||||||
|
{% with page_variants=view.get_affected_page_objects %}
|
||||||
|
{% if page_variants %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}Deleting the segment will also mean deleting all the page variants associated with it. Do you want to continue?{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}The page objects that <strong>will be deleted</strong> are:{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{% for variant in page_variants %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'wagtailadmin_explore' variant.pk %}">
|
||||||
|
{{ variant }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% trans 'Yes, delete the segment and associated page variants' as submit_button_value %}
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans %}Do you want to continue deleting this segment?{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
{% trans 'Yes, delete the segment' as submit_button_value %}
|
||||||
|
{% endif %}
|
||||||
|
<form action="{{ view.delete_url }}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="submit" value="{{ submit_button_value }}" class="button serious" />
|
||||||
|
</form>
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,57 @@
|
|||||||
|
{% extends "wagtailadmin/base.html" %}
|
||||||
|
|
||||||
|
{% load i18n wagtailadmin_tags %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% trans "Delete" as del_str %}
|
||||||
|
{% include "wagtailadmin/shared/header.html" with title=del_str subtitle=page.get_admin_display_title icon="doc-empty-inverse" %}
|
||||||
|
|
||||||
|
<div class="nice-padding">
|
||||||
|
<p>
|
||||||
|
{% trans 'Are you sure you want to delete this page?' %}
|
||||||
|
{% if descendant_count %}
|
||||||
|
{% blocktrans count counter=descendant_count %}
|
||||||
|
This will also delete one more subpage.
|
||||||
|
{% plural %}
|
||||||
|
This will also delete {{ descendant_count }} more subpages.
|
||||||
|
{% endblocktrans %}
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
{% if variants %}
|
||||||
|
<p>
|
||||||
|
{% blocktrans count counter=variants|length %}
|
||||||
|
This page is personalisable. Deleting it will delete its variant:
|
||||||
|
{% plural %}
|
||||||
|
This page is personalisable. Deleting it will delete all of its variants:
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{% for variant in variants %}
|
||||||
|
<li>
|
||||||
|
<a href="{% url 'wagtailadmin_explore' variant.pk %}">
|
||||||
|
{{ variant }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form action="{% url 'wagtailadmin_pages:delete' page.id %}" method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
{% if variants %}
|
||||||
|
{% trans 'Yes, delete the page and its variants' as submit_button_value %}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'Yes, delete it' as submit_button_value %}
|
||||||
|
{% endif %}
|
||||||
|
<input type="submit" value="{{ submit_button_value }}" class="button serious">
|
||||||
|
<a href="{% if next %}{{ next }}{% else %}{% url 'wagtailadmin_explore' page.get_parent.id %}{% endif %}" class="button button-secondary">{% trans "No, don't delete it" %}</a>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% page_permissions page as page_perms %}
|
||||||
|
{% if page_perms.can_unpublish %}
|
||||||
|
{% url 'wagtailadmin_pages:unpublish' page.id as unpublish_url %}
|
||||||
|
<p style="margin-top: 1em">{% blocktrans %}Alternatively you can <a href="{{ unpublish_url }}">unpublish the page</a>. This removes the page from public view and you can edit or publish it again later.{% endblocktrans %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -1,5 +1,6 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
|
from django.db.models import F
|
||||||
from django.template.base import FilterExpression, kwarg_re
|
from django.template.base import FilterExpression, kwarg_re
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -98,14 +99,20 @@ def parse_tag(token, parser):
|
|||||||
def exclude_variants(pages):
|
def exclude_variants(pages):
|
||||||
"""Checks if page is not a variant
|
"""Checks if page is not a variant
|
||||||
|
|
||||||
:param pages: List of pages to check
|
:param pages: Set of pages to check
|
||||||
:type pages: list
|
:type pages: QuerySet
|
||||||
:return: List of pages that aren't variants
|
:return: Queryset of pages that aren't variants
|
||||||
:rtype: list
|
:rtype: QuerySet
|
||||||
"""
|
"""
|
||||||
return [page for page in pages
|
from wagtail_personalisation.models import PersonalisablePageMetadata
|
||||||
if (hasattr(page, 'personalisation_metadata') is False)
|
excluded_variant_pages = PersonalisablePageMetadata.objects.exclude(
|
||||||
or (hasattr(page, 'personalisation_metadata')
|
canonical_page_id=F('variant_id')
|
||||||
and page.personalisation_metadata is None)
|
).values_list('variant_id')
|
||||||
or (hasattr(page, 'personalisation_metadata')
|
return pages.exclude(pk__in=excluded_variant_pages)
|
||||||
and page.personalisation_metadata.is_canonical)]
|
|
||||||
|
|
||||||
|
def can_delete_pages(pages, user):
|
||||||
|
for variant in pages:
|
||||||
|
if not variant.permissions_for_user(user).can_delete():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import csv
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
from django.db import transaction
|
||||||
|
from django.http import (
|
||||||
|
HttpResponse, HttpResponseForbidden, HttpResponseRedirect)
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
||||||
from wagtail.contrib.modeladmin.views import IndexView
|
from wagtail.contrib.modeladmin.views import DeleteView, IndexView
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.models import Segment
|
from wagtail_personalisation.models import Segment
|
||||||
|
from wagtail_personalisation.utils import can_delete_pages
|
||||||
|
|
||||||
|
|
||||||
class SegmentModelIndexView(IndexView):
|
class SegmentModelIndexView(IndexView):
|
||||||
@ -32,15 +38,52 @@ class SegmentModelDashboardView(IndexView):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class SegmentModelDeleteView(DeleteView):
|
||||||
|
def get_affected_page_objects(self):
|
||||||
|
return Page.objects.filter(pk__in=(
|
||||||
|
self.instance.get_used_pages().values_list('variant_id', flat=True)
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_template_names(self):
|
||||||
|
return [
|
||||||
|
'modeladmin/wagtail_personalisation/segment/delete.html',
|
||||||
|
'modeladmin/delete.html',
|
||||||
|
]
|
||||||
|
|
||||||
|
def delete_instance(self):
|
||||||
|
page_variants = self.get_affected_page_objects()
|
||||||
|
if not can_delete_pages(page_variants, self.request.user):
|
||||||
|
raise PermissionDenied(
|
||||||
|
'User has no permission to delete variant page objects.'
|
||||||
|
)
|
||||||
|
# Deleting page objects triggers deletion of the personalisation
|
||||||
|
# metadata too because of models.CASCADE.
|
||||||
|
with transaction.atomic():
|
||||||
|
for variant in page_variants.iterator():
|
||||||
|
# Delete each one separately so signals are called.
|
||||||
|
variant.delete()
|
||||||
|
super().delete_instance()
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if not can_delete_pages(self.get_affected_page_objects(),
|
||||||
|
self.request.user):
|
||||||
|
context = self.get_context_data(
|
||||||
|
cannot_delete_page_variants_error=True,
|
||||||
|
)
|
||||||
|
return self.render_to_response(context)
|
||||||
|
return super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@modeladmin_register
|
@modeladmin_register
|
||||||
class SegmentModelAdmin(ModelAdmin):
|
class SegmentModelAdmin(ModelAdmin):
|
||||||
"""The model admin for the Segments administration interface."""
|
"""The model admin for the Segments administration interface."""
|
||||||
model = Segment
|
model = Segment
|
||||||
index_view_class = SegmentModelIndexView
|
index_view_class = SegmentModelIndexView
|
||||||
dashboard_view_class = SegmentModelDashboardView
|
dashboard_view_class = SegmentModelDashboardView
|
||||||
|
delete_view_class = SegmentModelDeleteView
|
||||||
menu_icon = 'fa-snowflake-o'
|
menu_icon = 'fa-snowflake-o'
|
||||||
add_to_settings_menu = False
|
add_to_settings_menu = False
|
||||||
list_display = ('name', 'persistent', 'match_any', 'status',
|
list_display = ('name', 'persistent', 'match_any', 'enabled',
|
||||||
'page_count', 'variant_count', 'statistics')
|
'page_count', 'variant_count', 'statistics')
|
||||||
index_view_extra_js = ['js/commons.js', 'js/index.js']
|
index_view_extra_js = ['js/commons.js', 'js/index.js']
|
||||||
index_view_extra_css = ['css/index.css']
|
index_view_extra_css = ['css/index.css']
|
||||||
@ -139,3 +182,32 @@ def copy_page_view(request, page_id, segment_id):
|
|||||||
return HttpResponseRedirect(edit_url)
|
return HttpResponseRedirect(edit_url)
|
||||||
|
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
|
||||||
|
# CSV download views
|
||||||
|
def segment_user_data(request, segment_id):
|
||||||
|
if request.user.has_perm('wagtailadmin.access_admin'):
|
||||||
|
segment = get_object_or_404(Segment, pk=segment_id)
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||||
|
response['Content-Disposition'] = \
|
||||||
|
'attachment;filename=segment-%s-users.csv' % str(segment_id)
|
||||||
|
|
||||||
|
headers = ['Username']
|
||||||
|
for rule in segment.get_rules():
|
||||||
|
if rule.static:
|
||||||
|
headers.append(rule.get_column_header())
|
||||||
|
|
||||||
|
writer = csv.writer(response)
|
||||||
|
writer.writerow(headers)
|
||||||
|
|
||||||
|
for user in segment.static_users.all():
|
||||||
|
row = [user.username]
|
||||||
|
for rule in segment.get_rules():
|
||||||
|
if rule.static:
|
||||||
|
row.append(rule.get_user_info_string(user))
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
@ -3,14 +3,18 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.core.urlresolvers import reverse
|
from django.db import transaction
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
from django.template.defaultfilters import pluralize
|
from django.template.defaultfilters import pluralize
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from wagtail.wagtailadmin.site_summary import SummaryItem, PagesSummaryItem
|
from wagtail.admin import messages
|
||||||
from wagtail.wagtailadmin.widgets import Button, ButtonWithDropdownFromHook
|
from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem
|
||||||
from wagtail.wagtailcore import hooks
|
from wagtail.admin.views.pages import get_valid_next_url_from_request
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook
|
||||||
|
from wagtail.core import hooks
|
||||||
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation import admin_urls, models, utils
|
from wagtail_personalisation import admin_urls, models, utils
|
||||||
from wagtail_personalisation.adapters import get_segment_adapter
|
from wagtail_personalisation.adapters import get_segment_adapter
|
||||||
@ -23,9 +27,7 @@ def register_admin_urls():
|
|||||||
"""Adds the administration urls for the personalisation apps."""
|
"""Adds the administration urls for the personalisation apps."""
|
||||||
return [
|
return [
|
||||||
url(r'^personalisation/', include(
|
url(r'^personalisation/', include(
|
||||||
admin_urls,
|
admin_urls, namespace='wagtail_personalisation')),
|
||||||
app_name='wagtail_personalisation',
|
|
||||||
namespace='wagtail_personalisation')),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +37,7 @@ def set_visit_count(page, request, serve_args, serve_kwargs):
|
|||||||
to a segment.
|
to a segment.
|
||||||
|
|
||||||
:param page: The page being served
|
:param page: The page being served
|
||||||
:type page: wagtail.wagtailcore.models.Page
|
:type page: wagtail.core.models.Page
|
||||||
:param request: The http request
|
:param request: The http request
|
||||||
:type request: django.http.HttpRequest
|
:type request: django.http.HttpRequest
|
||||||
|
|
||||||
@ -49,7 +51,7 @@ def segment_user(page, request, serve_args, serve_kwargs):
|
|||||||
"""Apply a segment to a visitor before serving the page.
|
"""Apply a segment to a visitor before serving the page.
|
||||||
|
|
||||||
:param page: The page being served
|
:param page: The page being served
|
||||||
:type page: wagtail.wagtailcore.models.Page
|
:type page: wagtail.core.models.Page
|
||||||
:param request: The http request
|
:param request: The http request
|
||||||
:type request: django.http.HttpRequest
|
:type request: django.http.HttpRequest
|
||||||
|
|
||||||
@ -57,18 +59,42 @@ def segment_user(page, request, serve_args, serve_kwargs):
|
|||||||
adapter = get_segment_adapter(request)
|
adapter = get_segment_adapter(request)
|
||||||
adapter.refresh()
|
adapter.refresh()
|
||||||
|
|
||||||
|
forced_segment = request.GET.get('segment', None)
|
||||||
|
if request.user.is_superuser and forced_segment is not None:
|
||||||
|
segment = models.Segment.objects.filter(pk=forced_segment).first()
|
||||||
|
if segment:
|
||||||
|
adapter.set_segments([segment])
|
||||||
|
|
||||||
|
|
||||||
|
class UserbarSegmentedLinkItem:
|
||||||
|
def __init__(self, segment):
|
||||||
|
self.segment = segment
|
||||||
|
|
||||||
|
def render(self, request):
|
||||||
|
return f"""<div class="wagtail-userbar__item">
|
||||||
|
<a href="{request.path}?segment={self.segment.pk}"
|
||||||
|
class="wagtail-action">
|
||||||
|
Show as segment: {self.segment.name}</a></div>"""
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register('construct_wagtail_userbar')
|
||||||
|
def add_segment_link_items(request, items):
|
||||||
|
for item in models.Segment.objects.enabled():
|
||||||
|
items.append(UserbarSegmentedLinkItem(item))
|
||||||
|
return items
|
||||||
|
|
||||||
|
|
||||||
@hooks.register('before_serve_page')
|
@hooks.register('before_serve_page')
|
||||||
def serve_variant(page, request, serve_args, serve_kwargs):
|
def serve_variant(page, request, serve_args, serve_kwargs):
|
||||||
"""Apply a segment to a visitor before serving the page.
|
"""Apply a segment to a visitor before serving the page.
|
||||||
|
|
||||||
:param page: The page being served
|
:param page: The page being served
|
||||||
:type page: wagtail.wagtailcore.models.Page
|
:type page: wagtail.core.models.Page
|
||||||
:param request: The http request
|
:param request: The http request
|
||||||
:type request: django.http.HttpRequest
|
:type request: django.http.HttpRequest
|
||||||
:returns: A variant if one is available for the visitor's segment,
|
:returns: A variant if one is available for the visitor's segment,
|
||||||
otherwise the original page
|
otherwise the original page
|
||||||
:rtype: wagtail.wagtailcore.models.Page
|
:rtype: wagtail.core.models.Page
|
||||||
|
|
||||||
"""
|
"""
|
||||||
user_segments = []
|
user_segments = []
|
||||||
@ -146,13 +172,24 @@ def page_listing_more_buttons(page, page_perms, is_parent=False):
|
|||||||
priority=200)
|
priority=200)
|
||||||
|
|
||||||
|
|
||||||
class CorrectedPagesSummaryPanel(PagesSummaryItem):
|
class CorrectedPagesSummaryItem(PagesSummaryItem):
|
||||||
def get_context(self):
|
def get_context(self):
|
||||||
context = super(CorrectedPagesSummaryPanel, self).get_context()
|
# Perform the same check as Wagtail to get the correct count.
|
||||||
|
# Only correct the count when a root page is available to the user.
|
||||||
|
# The `PagesSummaryItem` will return a page count of 0 otherwise.
|
||||||
|
# https://github.com/wagtail/wagtail/blob/5c9ff23e229acabad406c42c4e13cbaea32e6c15/wagtail/admin/site_summary.py#L38
|
||||||
|
context = super().get_context()
|
||||||
|
root_page = context.get('root_page', None)
|
||||||
|
if root_page:
|
||||||
|
pages = utils.exclude_variants(
|
||||||
|
Page.objects.descendant_of(root_page, inclusive=True))
|
||||||
|
page_count = pages.count()
|
||||||
|
|
||||||
pages = utils.exclude_variants(Page.objects.all().specific())
|
if root_page.is_root():
|
||||||
|
page_count -= 1
|
||||||
|
|
||||||
|
context['total_pages'] = page_count
|
||||||
|
|
||||||
context['total_pages'] = len(pages) - 1
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -161,7 +198,7 @@ def add_corrected_pages_summary_panel(request, items):
|
|||||||
"""Replaces the Pages summary panel to hide variants."""
|
"""Replaces the Pages summary panel to hide variants."""
|
||||||
for index, item in enumerate(items):
|
for index, item in enumerate(items):
|
||||||
if item.__class__ is PagesSummaryItem:
|
if item.__class__ is PagesSummaryItem:
|
||||||
items[index] = CorrectedPagesSummaryPanel(request)
|
items[index] = CorrectedPagesSummaryItem(request)
|
||||||
|
|
||||||
|
|
||||||
class SegmentSummaryPanel(SummaryItem):
|
class SegmentSummaryPanel(SummaryItem):
|
||||||
@ -217,3 +254,54 @@ def add_personalisation_summary_panels(request, items):
|
|||||||
items.append(SegmentSummaryPanel(request))
|
items.append(SegmentSummaryPanel(request))
|
||||||
items.append(PersonalisedPagesSummaryPanel(request))
|
items.append(PersonalisedPagesSummaryPanel(request))
|
||||||
items.append(VariantPagesSummaryPanel(request))
|
items.append(VariantPagesSummaryPanel(request))
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.register('before_delete_page')
|
||||||
|
def delete_related_variants(request, page):
|
||||||
|
if not isinstance(page, models.PersonalisablePageMixin) \
|
||||||
|
or not page.personalisation_metadata.is_canonical:
|
||||||
|
return
|
||||||
|
# Get a list of related personalisation metadata for all the related
|
||||||
|
# variants.
|
||||||
|
variants_metadata = (
|
||||||
|
page.personalisation_metadata.variants_metadata
|
||||||
|
.select_related('variant')
|
||||||
|
)
|
||||||
|
next_url = get_valid_next_url_from_request(request)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
parent_id = page.get_parent().id
|
||||||
|
variants_metadata = variants_metadata.select_related('variant')
|
||||||
|
with transaction.atomic():
|
||||||
|
for metadata in variants_metadata.iterator():
|
||||||
|
# Call delete() on objects to trigger any signals or hooks.
|
||||||
|
metadata.variant.delete()
|
||||||
|
# Delete the page's main variant and the page itself.
|
||||||
|
page.personalisation_metadata.delete()
|
||||||
|
page.delete()
|
||||||
|
msg = _("Page '{0}' and its variants deleted.")
|
||||||
|
messages.success(
|
||||||
|
request,
|
||||||
|
msg.format(page.get_admin_display_title())
|
||||||
|
)
|
||||||
|
|
||||||
|
for fn in hooks.get_hooks('after_delete_page'):
|
||||||
|
result = fn(request, page)
|
||||||
|
if hasattr(result, 'status_code'):
|
||||||
|
return result
|
||||||
|
|
||||||
|
if next_url:
|
||||||
|
return redirect(next_url)
|
||||||
|
return redirect('wagtailadmin_explore', parent_id)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'wagtailadmin/pages/wagtail_personalisation/confirm_delete.html', {
|
||||||
|
'page': page,
|
||||||
|
'descendant_count': page.get_descendant_count(),
|
||||||
|
'next': next_url,
|
||||||
|
'variants': Page.objects.filter(
|
||||||
|
pk__in=variants_metadata.values_list('variant_id')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ -9,7 +9,7 @@ pytest_plugins = [
|
|||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def django_db_setup(django_db_setup, django_db_blocker):
|
def django_db_setup(django_db_setup, django_db_blocker):
|
||||||
from wagtail.wagtailcore.models import Page, Site
|
from wagtail.core.models import Page, Site
|
||||||
|
|
||||||
with django_db_blocker.unblock():
|
with django_db_blocker.unblock():
|
||||||
# Remove some initial data that is brought by the tests.site module
|
# Remove some initial data that is brought by the tests.site module
|
||||||
|
@ -8,6 +8,7 @@ from tests.site.pages import models
|
|||||||
|
|
||||||
|
|
||||||
class ContentPageFactory(PageFactory):
|
class ContentPageFactory(PageFactory):
|
||||||
|
parent = None
|
||||||
title = 'Test page'
|
title = 'Test page'
|
||||||
slug = factory.LazyAttribute(lambda obj: slugify(obj.title))
|
slug = factory.LazyAttribute(lambda obj: slugify(obj.title))
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ from wagtail_personalisation import models
|
|||||||
|
|
||||||
class SegmentFactory(factory.DjangoModelFactory):
|
class SegmentFactory(factory.DjangoModelFactory):
|
||||||
name = 'TestSegment'
|
name = 'TestSegment'
|
||||||
status = models.Segment.STATUS_ENABLED
|
enabled = True
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Segment
|
model = models.Segment
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import factory
|
import factory
|
||||||
from wagtail.wagtailcore.models import Site
|
from wagtail.core.models import Site
|
||||||
|
|
||||||
from tests.factories.page import ContentPageFactory
|
from tests.factories.page import ContentPageFactory
|
||||||
|
|
||||||
|
@ -44,3 +44,8 @@ class RequestFactory(BaseRequestFactory):
|
|||||||
request.session = SessionStore()
|
request.session = SessionStore()
|
||||||
request._messages = FallbackStorage(request)
|
request._messages = FallbackStorage(request)
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user(django_user_model):
|
||||||
|
return django_user_model.objects.create(username='user')
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pkg_resources import parse_version as V
|
|
||||||
|
|
||||||
import django
|
|
||||||
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
@ -56,36 +52,28 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_middleware_settings():
|
MIDDLEWARE = (
|
||||||
return (
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
|
||||||
|
|
||||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
'wagtail.core.middleware.SiteMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Django 1.10 started to use "MIDDLEWARE" instead of "MIDDLEWARE_CLASSES".
|
|
||||||
if V(django.get_version()) < V('1.10'):
|
|
||||||
MIDDLEWARE_CLASSES = get_middleware_settings()
|
|
||||||
else:
|
|
||||||
MIDDLEWARE = get_middleware_settings()
|
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
'wagtail_personalisation',
|
'wagtail_personalisation',
|
||||||
|
|
||||||
'wagtail.contrib.modeladmin',
|
'wagtail.contrib.modeladmin',
|
||||||
'wagtail.wagtailsearch',
|
'wagtail.search',
|
||||||
'wagtail.wagtailsites',
|
'wagtail.sites',
|
||||||
'wagtail.wagtailusers',
|
'wagtail.users',
|
||||||
'wagtail.wagtailimages',
|
'wagtail.images',
|
||||||
'wagtail.wagtaildocs',
|
'wagtail.documents',
|
||||||
'wagtail.wagtailadmin',
|
'wagtail.admin',
|
||||||
'wagtail.wagtailcore',
|
'wagtail.core',
|
||||||
|
|
||||||
'taggit',
|
'taggit',
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import wagtail.wagtailcore.fields
|
import wagtail.core.fields
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
import wagtail_personalisation.models
|
import wagtail_personalisation.models
|
||||||
@ -21,9 +21,9 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ContentPage',
|
name='ContentPage',
|
||||||
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')), # noqa: E501
|
||||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.11.1 on 2017-06-02 04:26
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import wagtail.wagtailcore.fields
|
import wagtail.core.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -18,9 +17,9 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='RegularPage',
|
name='RegularPage',
|
||||||
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')), # noqa: E501
|
||||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
from wagtail.admin.edit_handlers import FieldPanel
|
||||||
from wagtail.wagtailcore.fields import RichTextField
|
from wagtail.core.fields import RichTextField
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||||
|
|
||||||
|
@ -2,12 +2,12 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
from wagtail.wagtailcore import urls as wagtail_urls
|
from wagtail.core import urls as wagtail_urls
|
||||||
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
from wagtail.documents import urls as wagtaildocs_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^django-admin/', include(admin.site.urls)),
|
url(r'^django-admin/', admin.site.urls),
|
||||||
|
|
||||||
url(r'^admin/', include(wagtailadmin_urls)),
|
url(r'^admin/', include(wagtailadmin_urls)),
|
||||||
url(r'^documents/', include(wagtaildocs_urls)),
|
url(r'^documents/', include(wagtaildocs_urls)),
|
||||||
|
@ -20,6 +20,23 @@ def test_get_segments(rf):
|
|||||||
assert segments == [segment_1, segment_2]
|
assert segments == [segment_1, segment_2]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_segments_session(rf):
|
||||||
|
request = rf.get('/')
|
||||||
|
|
||||||
|
adapter = adapters.SessionSegmentsAdapter(request)
|
||||||
|
|
||||||
|
segment_1 = SegmentFactory(name='segment-1', persistent=True)
|
||||||
|
segment_2 = SegmentFactory(name='segment-2', persistent=True)
|
||||||
|
|
||||||
|
adapter.set_segments([segment_1, segment_2])
|
||||||
|
assert len(request.session['segments']) == 2
|
||||||
|
|
||||||
|
adapter._segment_cache = None
|
||||||
|
segments = adapter.get_segments()
|
||||||
|
assert segments == [segment_1, segment_2]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_get_segment_by_id(rf):
|
def test_get_segment_by_id(rf):
|
||||||
request = rf.get('/')
|
request = rf.get('/')
|
||||||
@ -47,7 +64,7 @@ def test_refresh_removes_disabled(rf):
|
|||||||
adapter.set_segments([segment_1, segment_2])
|
adapter.set_segments([segment_1, segment_2])
|
||||||
|
|
||||||
adapter = adapters.SessionSegmentsAdapter(request)
|
adapter = adapters.SessionSegmentsAdapter(request)
|
||||||
segment_1.status = segment_1.STATUS_DISABLED
|
segment_1.enabled = False
|
||||||
segment_1.save()
|
segment_1.save()
|
||||||
adapter.refresh()
|
adapter.refresh()
|
||||||
|
|
||||||
|
@ -4,29 +4,25 @@ import datetime
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.factories.page import ContentPageFactory
|
from tests.factories.rule import QueryRuleFactory, ReferralRuleFactory
|
||||||
from tests.factories.rule import (
|
|
||||||
DayRuleFactory, DeviceRuleFactory, ReferralRuleFactory, TimeRuleFactory)
|
|
||||||
from tests.factories.segment import SegmentFactory
|
from tests.factories.segment import SegmentFactory
|
||||||
from tests.factories.site import SiteFactory
|
|
||||||
from wagtail_personalisation.models import Segment
|
from wagtail_personalisation.models import Segment
|
||||||
from wagtail_personalisation.rules import TimeRule
|
from wagtail_personalisation.rules import TimeRule
|
||||||
|
|
||||||
# Factory tests
|
# Factory tests
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_segment_create():
|
def test_segment_create():
|
||||||
factoried_segment = SegmentFactory()
|
factoried_segment = SegmentFactory()
|
||||||
segment = Segment(name='TestSegment', status='enabled')
|
segment = Segment(name='TestSegment', enabled=True)
|
||||||
TimeRule(
|
TimeRule(
|
||||||
start_time=datetime.time(8, 0, 0),
|
start_time=datetime.time(8, 0, 0),
|
||||||
end_time=datetime.time(23, 0, 0),
|
end_time=datetime.time(23, 0, 0),
|
||||||
segment=segment)
|
segment=segment)
|
||||||
|
|
||||||
assert factoried_segment.name == segment.name
|
assert factoried_segment.name == segment.name
|
||||||
assert factoried_segment.status == segment.status
|
assert factoried_segment.enabled == segment.enabled
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@ -37,3 +33,15 @@ def test_referral_rule_create():
|
|||||||
segment=segment)
|
segment=segment)
|
||||||
|
|
||||||
assert referral_rule.regex_string == 'test.test'
|
assert referral_rule.regex_string == 'test.test'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_query_rule_create():
|
||||||
|
segment = SegmentFactory(name='Query')
|
||||||
|
query_rule = QueryRuleFactory(
|
||||||
|
parameter="query",
|
||||||
|
value="value",
|
||||||
|
segment=segment)
|
||||||
|
|
||||||
|
assert query_rule.parameter == 'query'
|
||||||
|
assert query_rule.value == 'value'
|
||||||
|
@ -3,8 +3,12 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from django.db.models import ProtectedError
|
||||||
|
|
||||||
|
from tests.factories.page import ContentPageFactory
|
||||||
from tests.factories.segment import SegmentFactory
|
from tests.factories.segment import SegmentFactory
|
||||||
|
from tests.site.pages import models
|
||||||
|
from wagtail_personalisation.models import PersonalisablePageMetadata
|
||||||
from wagtail_personalisation.rules import TimeRule
|
from wagtail_personalisation.rules import TimeRule
|
||||||
|
|
||||||
|
|
||||||
@ -25,3 +29,34 @@ def test_metadata_page_has_variants(segmented_page):
|
|||||||
canonical = segmented_page.personalisation_metadata.canonical_page
|
canonical = segmented_page.personalisation_metadata.canonical_page
|
||||||
assert canonical.personalisation_metadata.is_canonical
|
assert canonical.personalisation_metadata.is_canonical
|
||||||
assert canonical.personalisation_metadata.has_variants
|
assert canonical.personalisation_metadata.has_variants
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_content_page_model():
|
||||||
|
page = ContentPageFactory()
|
||||||
|
qs = models.ContentPage.objects.all()
|
||||||
|
assert page in qs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_variant_can_be_deleted_without_error(segmented_page):
|
||||||
|
segmented_page.delete()
|
||||||
|
# Make sure the metadata gets deleted because of models.CASCADE.
|
||||||
|
with pytest.raises(PersonalisablePageMetadata.DoesNotExist):
|
||||||
|
segmented_page._personalisable_page_metadata.refresh_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_canonical_page_deletion_is_protected(segmented_page):
|
||||||
|
# When deleting canonical page without deleting variants, it should return
|
||||||
|
# an error. All variants should be deleted beforehand.
|
||||||
|
with pytest.raises(ProtectedError):
|
||||||
|
segmented_page.personalisation_metadata.canonical_page.delete()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_page_protection_when_deleting_segment(segmented_page):
|
||||||
|
segment = segmented_page.personalisation_metadata.segment
|
||||||
|
assert len(segment.get_used_pages())
|
||||||
|
with pytest.raises(ProtectedError):
|
||||||
|
segment.delete()
|
||||||
|
@ -16,6 +16,8 @@ def test_time_rule_create():
|
|||||||
segment=segment)
|
segment=segment)
|
||||||
|
|
||||||
assert time_rule.start_time == datetime.time(8, 0, 0)
|
assert time_rule.start_time == datetime.time(8, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@freeze_time("10:00:00")
|
@freeze_time("10:00:00")
|
||||||
def test_requesttime_segment(client, site):
|
def test_requesttime_segment(client, site):
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from tests.factories.rule import VisitCountRuleFactory
|
||||||
|
from tests.factories.segment import SegmentFactory
|
||||||
|
from wagtail_personalisation.rules import VisitCountRule
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_visit_count(site, client):
|
def test_visit_count(site, client):
|
||||||
@ -20,3 +24,56 @@ def test_visit_count(site, client):
|
|||||||
visit_count = client.session['visit_count']
|
visit_count = client.session['visit_count']
|
||||||
assert visit_count[0]['count'] == 2
|
assert visit_count[0]['count'] == 2
|
||||||
assert visit_count[1]['count'] == 1
|
assert visit_count[1]['count'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_call_test_user_on_invalid_rule_fails(site, user, mocker):
|
||||||
|
rule = VisitCountRule()
|
||||||
|
assert not (rule.test_user(None, user))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_visit_count_call_test_user_with_user(site, client, user):
|
||||||
|
segment = SegmentFactory(name='VisitCount')
|
||||||
|
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
assert rule.test_user(None, user)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_visit_count_call_test_user_with_user_or_request_fails(site, client, user):
|
||||||
|
segment = SegmentFactory(name='VisitCount')
|
||||||
|
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
assert not rule.test_user(None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_column_header(site):
|
||||||
|
segment = SegmentFactory(name='VisitCount')
|
||||||
|
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||||
|
|
||||||
|
assert rule.get_column_header() == 'Visit count - Test page'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_user_info_string_returns_count(site, client, user):
|
||||||
|
segment = SegmentFactory(name='VisitCount')
|
||||||
|
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
assert rule.get_user_info_string(user) == '2'
|
||||||
|
574
tests/unit/test_static_dynamic_segments.py
Normal file
@ -0,0 +1,574 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.forms.models import model_to_dict
|
||||||
|
|
||||||
|
from tests.factories.segment import SegmentFactory
|
||||||
|
from wagtail_personalisation.forms import SegmentAdminForm
|
||||||
|
from wagtail_personalisation.models import Segment
|
||||||
|
from wagtail_personalisation.rules import TimeRule, VisitCountRule
|
||||||
|
|
||||||
|
|
||||||
|
def form_with_data(segment, *rules):
|
||||||
|
model_fields = ['type', 'enabled', 'count', 'name', 'match_any', 'randomisation_percent']
|
||||||
|
|
||||||
|
class TestSegmentAdminForm(SegmentAdminForm):
|
||||||
|
class Meta:
|
||||||
|
model = Segment
|
||||||
|
fields = model_fields
|
||||||
|
|
||||||
|
data = model_to_dict(segment, model_fields)
|
||||||
|
for formset in TestSegmentAdminForm().formsets.values():
|
||||||
|
rule_data = {}
|
||||||
|
count = 0
|
||||||
|
for rule in rules:
|
||||||
|
if isinstance(rule, formset.model):
|
||||||
|
rule_data = model_to_dict(rule)
|
||||||
|
for key, value in rule_data.items():
|
||||||
|
data['{}-{}-{}'.format(formset.prefix, count, key)] = value
|
||||||
|
count += 1
|
||||||
|
data['{}-INITIAL_FORMS'.format(formset.prefix)] = 0
|
||||||
|
data['{}-TOTAL_FORMS'.format(formset.prefix)] = count
|
||||||
|
return TestSegmentAdminForm(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_added_to_static_segment_at_creation(site, user, mocker):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert user in instance.static_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_user_not_added_to_full_static_segment_at_creation(site, django_user_model, mocker):
|
||||||
|
django_user_model.objects.create(username='first')
|
||||||
|
django_user_model.objects.create(username='second')
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user',
|
||||||
|
side_effect=[True, True])
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert len(instance.static_users.all()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_anonymous_user_not_added_to_static_segment_at_creation(site, client, mocker):
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert not instance.static_users.all()
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_match_any_correct_populates(site, django_user_model, mocker):
|
||||||
|
user = django_user_model.objects.create(username='first')
|
||||||
|
other_user = django_user_model.objects.create(username='second')
|
||||||
|
other_page = site.root_page.get_last_child()
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, match_any=True)
|
||||||
|
rule_1 = VisitCountRule(counted_page=site.root_page)
|
||||||
|
rule_2 = VisitCountRule(counted_page=other_page)
|
||||||
|
form = form_with_data(segment, rule_1, rule_2)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', side_effect=[True, False, True, False])
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert user in instance.static_users.all()
|
||||||
|
assert other_user in instance.static_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, mocker):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||||
|
static_rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
non_static_rule = TimeRule(
|
||||||
|
start_time=datetime.time(0, 0, 0),
|
||||||
|
end_time=datetime.time(23, 59, 59),
|
||||||
|
)
|
||||||
|
form = form_with_data(segment, static_rule, non_static_rule)
|
||||||
|
|
||||||
|
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert not instance.static_users.all()
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_session_not_added_to_static_segment_after_creation(site, client, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert not instance.static_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_session_added_to_static_segment_after_creation(site, client, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert user in instance.static_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_anonymou_user_not_added_to_static_segment_after_creation(site, client):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert not instance.static_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_session_not_added_to_static_segment_after_full(site, client, django_user_model):
|
||||||
|
user = django_user_model.objects.create(username='first')
|
||||||
|
other_user = django_user_model.objects.create(username='second')
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert not instance.static_users.all()
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert instance.static_users.count() == 1
|
||||||
|
|
||||||
|
client.cookies.clear()
|
||||||
|
second_session = client.session
|
||||||
|
client.force_login(other_user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert session.session_key != second_session.session_key
|
||||||
|
assert instance.static_users.count() == 1
|
||||||
|
assert user in instance.static_users.all()
|
||||||
|
assert other_user not in instance.static_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_sessions_not_added_to_static_segment_if_rule_not_static(mocker):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||||
|
rule = TimeRule(
|
||||||
|
start_time=datetime.time(0, 0, 0),
|
||||||
|
end_time=datetime.time(23, 59, 59),
|
||||||
|
segment=segment,
|
||||||
|
)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert not instance.static_users.all()
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_does_not_calculate_the_segment_again(site, client, mocker, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=2)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert user in instance.static_users.all()
|
||||||
|
|
||||||
|
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_non_static_rules_have_a_count():
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||||
|
rule = TimeRule(
|
||||||
|
start_time=datetime.time(0, 0, 0),
|
||||||
|
end_time=datetime.time(23, 59, 59),
|
||||||
|
segment=segment,
|
||||||
|
)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
assert not form.is_valid()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_static_segment_with_static_rules_needs_no_count(site):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
assert form.is_valid()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_dynamic_segment_with_non_static_rules_have_a_count():
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC, count=0)
|
||||||
|
rule = TimeRule(
|
||||||
|
start_time=datetime.time(0, 0, 0),
|
||||||
|
end_time=datetime.time(23, 59, 59),
|
||||||
|
)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
assert form.is_valid(), form.errors
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_randomisation_percentage_added_to_segment_at_creation(site, client, mocker, django_user_model):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
segment.randomisation_percent = 80
|
||||||
|
rule = VisitCountRule()
|
||||||
|
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert instance.randomisation_percent == 80
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_randomisation_percentage_min_zero(site, client, mocker, django_user_model):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
segment.randomisation_percent = -1
|
||||||
|
rule = VisitCountRule()
|
||||||
|
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
assert not form.is_valid()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_randomisation_percentage_max_100(site, client, mocker, django_user_model):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
segment.randomisation_percent = 101
|
||||||
|
rule = VisitCountRule()
|
||||||
|
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
assert not form.is_valid()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_in_static_segment_if_random_is_below_percentage(site, client, mocker, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||||
|
randomisation_percent=40)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
mocker.patch('random.randint', return_value=39)
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert instance.id == client.session['segments'][0]['id']
|
||||||
|
assert user in instance.static_users.all()
|
||||||
|
assert user not in instance.excluded_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_in_static_segment_if_random_is_above_percentage(site, client, mocker, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||||
|
randomisation_percent=40)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
mocker.patch('random.randint', return_value=41)
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert len(client.session['segments']) == 0
|
||||||
|
assert user not in instance.static_users.all()
|
||||||
|
assert user in instance.excluded_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_offered_dynamic_segment_if_random_is_below_percentage(site, client, mocker):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||||
|
randomisation_percent=40)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
mocker.patch('random.randint', return_value=39)
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert len(client.session['excluded_segments']) == 0
|
||||||
|
assert instance.id == client.session['segments'][0]['id']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_offered_dynamic_segment_if_random_is_above_percentage(site, client, mocker):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||||
|
randomisation_percent=40)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
mocker.patch('random.randint', return_value=41)
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert len(client.session['segments']) == 0
|
||||||
|
assert instance.id == client.session['excluded_segments'][0]['id']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_in_segment_if_percentage_is_0(site, client, mocker, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||||
|
randomisation_percent=0)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert len(client.session['segments']) == 0
|
||||||
|
assert user not in instance.static_users.all()
|
||||||
|
assert user in instance.excluded_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_always_in_segment_if_percentage_is_100(site, client, mocker, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||||
|
randomisation_percent=100)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert instance.id == client.session['segments'][0]['id']
|
||||||
|
assert user in instance.static_users.all()
|
||||||
|
assert user not in instance.excluded_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_not_added_to_static_segment_at_creation_if_random_above_percent(site, mocker, user):
|
||||||
|
mocker.patch('random.randint', return_value=41)
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, randomisation_percent=40)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert user not in instance.static_users.all()
|
||||||
|
assert user in instance.excluded_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_added_to_static_segment_at_creation_if_random_below_percent(site, mocker, user):
|
||||||
|
mocker.patch('random.randint', return_value=39)
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, randomisation_percent=40)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert user in instance.static_users.all()
|
||||||
|
assert user not in instance.excluded_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_rules_check_skipped_if_user_in_excluded(site, client, mocker, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||||
|
randomisation_percent=100)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
instance.excluded_users.add(user)
|
||||||
|
instance.save
|
||||||
|
|
||||||
|
mock_test_rule = mocker.patch(
|
||||||
|
'wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
assert len(client.session['segments']) == 0
|
||||||
|
assert user not in instance.static_users.all()
|
||||||
|
assert user in instance.excluded_users.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_rules_check_skipped_if_dynamic_segment_in_excluded(site, client, mocker, user):
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||||
|
randomisation_percent=100)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
instance = form.save()
|
||||||
|
instance.persistent = True
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session['excluded_segments'] = [{'id': instance.pk}]
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
mock_test_rule = mocker.patch(
|
||||||
|
'wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
|
|
||||||
|
client.force_login(user)
|
||||||
|
client.get(site.root_page.url)
|
||||||
|
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
assert len(client.session['segments']) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_matched_user_count_added_to_segment_at_creation(site, mocker, django_user_model):
|
||||||
|
django_user_model.objects.create(username='first')
|
||||||
|
django_user_model.objects.create(username='second')
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
rule = VisitCountRule()
|
||||||
|
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
form.instance.type = Segment.TYPE_STATIC
|
||||||
|
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
|
instance = form.save()
|
||||||
|
|
||||||
|
assert mock_test_user.call_count == 2
|
||||||
|
instance.matched_users_count = 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_count_users_matching_static_rules(site, client, mocker, django_user_model):
|
||||||
|
django_user_model.objects.create(username='first')
|
||||||
|
django_user_model.objects.create(username='second')
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
|
|
||||||
|
assert form.count_matching_users([rule], True) is 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_count_matching_users_excludes_staff(site, client, mocker, django_user_model):
|
||||||
|
django_user_model.objects.create(username='first')
|
||||||
|
django_user_model.objects.create(username='second', is_staff=True)
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
|
|
||||||
|
assert form.count_matching_users([rule], True) is 1
|
||||||
|
assert mock_test_user.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_count_matching_users_excludes_inactive(site, client, mocker, django_user_model):
|
||||||
|
django_user_model.objects.create(username='first')
|
||||||
|
django_user_model.objects.create(username='second', is_active=False)
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
|
|
||||||
|
assert form.count_matching_users([rule], True) is 1
|
||||||
|
assert mock_test_user.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_count_matching_users_only_counts_static_rules(site, client, mocker, django_user_model):
|
||||||
|
django_user_model.objects.create(username='first')
|
||||||
|
django_user_model.objects.create(username='second')
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
rule = TimeRule(
|
||||||
|
start_time=datetime.time(0, 0, 0),
|
||||||
|
end_time=datetime.time(23, 59, 59),
|
||||||
|
segment=segment,
|
||||||
|
)
|
||||||
|
form = form_with_data(segment, rule)
|
||||||
|
mock_test_user = mocker.patch('wagtail_personalisation.rules.TimeRule.test_user')
|
||||||
|
|
||||||
|
assert form.count_matching_users([rule], True) is 0
|
||||||
|
assert mock_test_user.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_count_matching_users_handles_match_any(site, client, mocker, django_user_model):
|
||||||
|
django_user_model.objects.create(username='first')
|
||||||
|
django_user_model.objects.create(username='second')
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
first_rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
other_page = site.root_page.get_last_child()
|
||||||
|
second_rule = VisitCountRule(counted_page=other_page)
|
||||||
|
form = form_with_data(segment, first_rule, second_rule)
|
||||||
|
|
||||||
|
mock_test_user = mocker.patch(
|
||||||
|
'wagtail_personalisation.rules.VisitCountRule.test_user',
|
||||||
|
side_effect=[True, False, True, False])
|
||||||
|
|
||||||
|
assert form.count_matching_users([first_rule, second_rule], True) is 2
|
||||||
|
mock_test_user.call_count == 4
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_count_matching_users_handles_match_all(site, client, mocker, django_user_model):
|
||||||
|
django_user_model.objects.create(username='first')
|
||||||
|
django_user_model.objects.create(username='second')
|
||||||
|
|
||||||
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
|
first_rule = VisitCountRule(counted_page=site.root_page)
|
||||||
|
other_page = site.root_page.get_last_child()
|
||||||
|
second_rule = VisitCountRule(counted_page=other_page)
|
||||||
|
form = form_with_data(segment, first_rule, second_rule)
|
||||||
|
|
||||||
|
mock_test_user = mocker.patch(
|
||||||
|
'wagtail_personalisation.rules.VisitCountRule.test_user',
|
||||||
|
side_effect=[True, True, False, True])
|
||||||
|
|
||||||
|
assert form.count_matching_users([first_rule, second_rule], False) is 1
|
||||||
|
mock_test_user.call_count == 4
|
@ -1,21 +1,38 @@
|
|||||||
from wagtail_personalisation.utils import impersonate_other_page
|
import pytest
|
||||||
|
|
||||||
|
from tests.factories.page import ContentPageFactory
|
||||||
|
from wagtail_personalisation.utils import (
|
||||||
|
can_delete_pages, impersonate_other_page)
|
||||||
|
|
||||||
|
|
||||||
class Page(object):
|
@pytest.fixture
|
||||||
def __init__(self, path, depth, url_path, title):
|
def rootpage():
|
||||||
self.path = path
|
return ContentPageFactory(parent=None, path='/', depth=0, title='root')
|
||||||
self.depth = depth
|
|
||||||
self.url_path = url_path
|
|
||||||
self.title = title
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.__dict__ == other.__dict__
|
|
||||||
|
|
||||||
|
|
||||||
def test_impersonate_other_page():
|
@pytest.fixture
|
||||||
page = Page(path="/", depth=0, url_path="/", title="Hoi")
|
def page(rootpage):
|
||||||
other_page = Page(path="/other", depth=1, url_path="/other", title="Doei")
|
return ContentPageFactory(parent=rootpage, path='/hi', title='Hi')
|
||||||
|
|
||||||
impersonate_other_page(page, other_page)
|
|
||||||
|
|
||||||
assert page == other_page
|
@pytest.fixture
|
||||||
|
def otherpage(rootpage):
|
||||||
|
return ContentPageFactory(parent=rootpage, path='/bye', title='Bye')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_impersonate_other_page(page, otherpage):
|
||||||
|
impersonate_other_page(page, otherpage)
|
||||||
|
assert page.title == otherpage.title == 'Bye'
|
||||||
|
assert page.path == otherpage.path
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_can_delete_pages_with_superuser(rf, user, segmented_page):
|
||||||
|
user.is_superuser = True
|
||||||
|
assert can_delete_pages([segmented_page], user)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_cannot_delete_pages_with_standard_user(user, segmented_page):
|
||||||
|
assert not can_delete_pages([segmented_page], user)
|
||||||
|
110
tests/unit/test_views.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import pytest
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.urls import reverse
|
||||||
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
|
from wagtail_personalisation.models import Segment
|
||||||
|
from wagtail_personalisation.rules import VisitCountRule
|
||||||
|
from wagtail_personalisation.views import (
|
||||||
|
SegmentModelDeleteView, SegmentModelAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_segment_user_data_view_requires_admin_access(site, client, django_user_model):
|
||||||
|
user = django_user_model.objects.create(username='first')
|
||||||
|
|
||||||
|
segment = Segment(type=Segment.TYPE_STATIC, count=1)
|
||||||
|
segment.save()
|
||||||
|
|
||||||
|
client.force_login(user)
|
||||||
|
url = reverse('segment:segment_user_data', args=(segment.id,))
|
||||||
|
response = client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 302
|
||||||
|
assert response.url == '/admin/login/?next=%s' % url
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_segment_user_data_view(site, client, mocker, django_user_model):
|
||||||
|
user1 = django_user_model.objects.create(username='first')
|
||||||
|
user2 = django_user_model.objects.create(username='second')
|
||||||
|
admin_user = django_user_model.objects.create(
|
||||||
|
username='admin', is_superuser=True)
|
||||||
|
|
||||||
|
segment = Segment(type=Segment.TYPE_STATIC, count=1)
|
||||||
|
segment.save()
|
||||||
|
segment.static_users.add(user1)
|
||||||
|
segment.static_users.add(user2)
|
||||||
|
|
||||||
|
rule1 = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||||
|
rule2 = VisitCountRule(counted_page=site.root_page.get_last_child(),
|
||||||
|
segment=segment)
|
||||||
|
rule1.save()
|
||||||
|
rule2.save()
|
||||||
|
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.get_user_info_string',
|
||||||
|
side_effect=[3, 9, 0, 1])
|
||||||
|
|
||||||
|
client.force_login(admin_user)
|
||||||
|
response = client.get(
|
||||||
|
reverse('segment:segment_user_data', args=(segment.id,)))
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data_lines = response.content.decode().split("\n")
|
||||||
|
|
||||||
|
assert data_lines[0] == 'Username,Visit count - Test page,Visit count - Regular page\r'
|
||||||
|
assert data_lines[1] == 'first,3,9\r'
|
||||||
|
assert data_lines[2] == 'second,0,1\r'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_segment_delete_view_delete_instance(rf, segmented_page, user):
|
||||||
|
user.is_superuser = True
|
||||||
|
user.save()
|
||||||
|
segment = segmented_page.personalisation_metadata.segment
|
||||||
|
canonical_page = segmented_page.personalisation_metadata.canonical_page
|
||||||
|
variants_metadata = segment.get_used_pages()
|
||||||
|
page_variants = Page.objects.filter(pk__in=(
|
||||||
|
variants_metadata.values_list('variant_id', flat=True)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Make sure all canonical page, variants and variants metadata exist
|
||||||
|
assert canonical_page
|
||||||
|
assert page_variants
|
||||||
|
assert variants_metadata
|
||||||
|
|
||||||
|
# Delete the segment via the method on the view.
|
||||||
|
request = rf.get('/'.format(segment.pk))
|
||||||
|
request.user = user
|
||||||
|
view = SegmentModelDeleteView(
|
||||||
|
instance_pk=str(segment.pk),
|
||||||
|
model_admin=SegmentModelAdmin()
|
||||||
|
)
|
||||||
|
view.request = request
|
||||||
|
view.delete_instance()
|
||||||
|
|
||||||
|
# Segment has been deleted.
|
||||||
|
with pytest.raises(segment.DoesNotExist):
|
||||||
|
segment.refresh_from_db()
|
||||||
|
|
||||||
|
# Canonical page stayed intact.
|
||||||
|
canonical_page.refresh_from_db()
|
||||||
|
|
||||||
|
# Variant pages and their metadata have been deleted.
|
||||||
|
assert not page_variants.all()
|
||||||
|
assert not variants_metadata.all()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_segment_delete_view_raises_permission_denied(rf, segmented_page, user):
|
||||||
|
segment = segmented_page.personalisation_metadata.segment
|
||||||
|
request = rf.get('/'.format(segment.pk))
|
||||||
|
request.user = user
|
||||||
|
view = SegmentModelDeleteView(
|
||||||
|
instance_pk=str(segment.pk),
|
||||||
|
model_admin=SegmentModelAdmin()
|
||||||
|
)
|
||||||
|
view.request = request
|
||||||
|
message = 'User have no permission to delete variant page objects.'
|
||||||
|
with pytest.raises(PermissionDenied, message=message):
|
||||||
|
view.delete_instance()
|
@ -1,4 +1,5 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from tests.factories.segment import SegmentFactory
|
from tests.factories.segment import SegmentFactory
|
||||||
from wagtail_personalisation import adapters, wagtail_hooks
|
from wagtail_personalisation import adapters, wagtail_hooks
|
||||||
@ -60,3 +61,54 @@ def test_page_listing_more_buttons(site, rf, segmented_page):
|
|||||||
result = wagtail_hooks.page_listing_more_buttons(page, [])
|
result = wagtail_hooks.page_listing_more_buttons(page, [])
|
||||||
items = list(result)
|
items = list(result)
|
||||||
assert len(items) == 3
|
assert len(items) == 3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_custom_delete_page_view_does_not_trigger_for_variants(
|
||||||
|
rf,
|
||||||
|
segmented_page
|
||||||
|
):
|
||||||
|
assert (
|
||||||
|
wagtail_hooks.delete_related_variants(rf.get('/'), segmented_page)
|
||||||
|
) is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_custom_delete_page_view_triggers_for_canonical_pages(
|
||||||
|
rf,
|
||||||
|
segmented_page
|
||||||
|
):
|
||||||
|
assert (
|
||||||
|
wagtail_hooks.delete_related_variants(
|
||||||
|
rf.get('/'),
|
||||||
|
segmented_page.personalisation_metadata.canonical_page
|
||||||
|
)
|
||||||
|
) is not None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_custom_delete_page_view_deletes_variants(rf, segmented_page, user):
|
||||||
|
post_request = rf.post('/')
|
||||||
|
user.is_superuser = True
|
||||||
|
rf.user = user
|
||||||
|
canonical_page = segmented_page.personalisation_metadata.canonical_page
|
||||||
|
canonical_page_variant = canonical_page.personalisation_metadata
|
||||||
|
assert canonical_page_variant
|
||||||
|
|
||||||
|
variants = Page.objects.filter(pk__in=(
|
||||||
|
canonical_page.personalisation_metadata.variants_metadata.values_list('variant_id', flat=True)
|
||||||
|
))
|
||||||
|
variants_metadata = canonical_page.personalisation_metadata.variants_metadata
|
||||||
|
# Make sure there are variants that exist in the database.
|
||||||
|
assert len(variants.all())
|
||||||
|
assert len(variants_metadata.all())
|
||||||
|
wagtail_hooks.delete_related_variants(
|
||||||
|
post_request, segmented_page.personalisation_metadata.canonical_page
|
||||||
|
)
|
||||||
|
with pytest.raises(canonical_page.DoesNotExist):
|
||||||
|
canonical_page.refresh_from_db()
|
||||||
|
with pytest.raises(canonical_page_variant.DoesNotExist):
|
||||||
|
canonical_page_variant.refresh_from_db()
|
||||||
|
# Make sure all the variant pages have been deleted.
|
||||||
|
assert not len(variants.all())
|
||||||
|
assert not len(variants_metadata.all())
|
||||||
|
19
tox.ini
@ -1,28 +1,25 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py{27,35,36}-django{19,110,111}-wagtail{19,110},lint
|
envlist = py{36}-django{20}-wagtail{20,21},lint
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
basepython = python3.6
|
||||||
commands = coverage run --parallel -m pytest {posargs}
|
commands = coverage run --parallel -m pytest {posargs}
|
||||||
extras = test
|
extras = test
|
||||||
deps =
|
deps =
|
||||||
django19: django>=1.9,<1.10
|
django20: django>=2.0,<2.1
|
||||||
django110: django>=1.10<1.11
|
wagtail20: wagtail>=2.0,<2.1
|
||||||
django111: django>=1.11,<1.12
|
wagtail21: wagtail>=2.1,<2.2
|
||||||
wagtail19: wagtail>=1.9,<1.10
|
|
||||||
wagtail110: wagtail>=1.10,<1.11
|
|
||||||
|
|
||||||
[testenv:coverage-report]
|
[testenv:coverage-report]
|
||||||
basepython = python3.5
|
basepython = python3.6
|
||||||
deps = coverage
|
deps = coverage
|
||||||
pip_pre = true
|
pip_pre = true
|
||||||
skip_install = true
|
skip_install = true
|
||||||
commands =
|
commands =
|
||||||
coverage combine
|
coverage report --include="src/**/" --omit="src/**/migrations/*.py"
|
||||||
coverage report
|
|
||||||
|
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
basepython = python3.5
|
basepython = python3.6
|
||||||
deps = flake8
|
deps = flake8
|
||||||
commands =
|
commands =
|
||||||
flake8 src tests setup.py
|
flake8 src tests setup.py
|
||||||
|