Compare commits
180 Commits
Author | SHA1 | Date | |
---|---|---|---|
9b96855380 | |||
81c805e406 | |||
293c82d13e | |||
95ee9f18a3 | |||
b8d7dd53ae | |||
dd4530203f | |||
48955675be | |||
a81c5b3560 | |||
53880228e4 | |||
2bee66d0ae | |||
16e24b6791 | |||
477bfb9665 | |||
6108469047 | |||
686f180081 | |||
9b1dbe35cb | |||
7e0594e341 | |||
0c19456053 | |||
18140f76ab | |||
88b17ceeb8 | |||
570de7d128 | |||
b82d5165c3 | |||
8d802dbbf4 | |||
9274073c68 | |||
1f1264cf95 | |||
3f16ad686e | |||
7101b63122 | |||
ffd839159b | |||
d074ef85b9 | |||
f3e403bec6 | |||
137b5b411c | |||
39f3500813 | |||
6a6c3e8d7b | |||
336ed2317c | |||
06569a3cc1 | |||
da6e5127ed | |||
3d054ec585 | |||
43b5b62e60 | |||
40a9959680 | |||
13e13ccae5 | |||
318b65b3eb | |||
6b9b4e0af2 | |||
69a4514129 | |||
585cb0b16a | |||
4ae8a5e60b | |||
d7ad1be51f | |||
bd5b85cedb | |||
956c1bf4f5 | |||
d775ef57e6 | |||
d34c449638 | |||
23af862798 | |||
88263dea60 | |||
2e1e09f60b | |||
86e669e4f4 | |||
807005461e | |||
7f9e0971f5 | |||
a4a1a2ddca | |||
9cc6e966ba | |||
311abeb6c8 | |||
60675203c6 | |||
ceef806301 | |||
650e061f91 | |||
9235932f00 | |||
1e0efc975a | |||
7517dcd051 | |||
94c9efa315 | |||
f73e59421b | |||
f054b86e07 | |||
cbb56847ae | |||
b135e79c77 | |||
5cd8751450 | |||
c07b280276 | |||
28266c4500 | |||
94a5c6b289 | |||
875d8302de | |||
4c09ad4ca7 | |||
0d260a12a4 | |||
7888f0b615 | |||
02e63ed82c | |||
a411ad1ccc | |||
1a1df18bf3 | |||
56d28faec8 | |||
f95b8dcb93 | |||
d3f4d42d82 | |||
4c08581919 | |||
11886ae135 | |||
83cc7f790e | |||
dcdeb4e9a2 | |||
2e827be41a | |||
0f9bfb0343 | |||
1c74e6cfb9 | |||
9c45ac56db | |||
2f7b92fb2e | |||
1e69d929aa | |||
a178a8b533 | |||
f2e01c803a | |||
eb9d4f3e31 | |||
4ceb59c719 | |||
6fcab3ac11 | |||
1f464adaa7 | |||
d15f6c37d3 | |||
7d679d7111 | |||
b11a6ce4ca | |||
4e9a6e902d | |||
3ce0aef8d5 | |||
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 | |||
8f789b3e17 | |||
9c88ec1582 | |||
785d1486e4 |
44
.github/workflows/python-test.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
name: Python Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
format:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Install dependencies
|
||||
run: pip install tox
|
||||
- name: Validate formatting
|
||||
run: tox -e format
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
max-parallel: 5
|
||||
matrix:
|
||||
python: ["3.7", "3.8", "3.9", "3.10", "3.11"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install tox tox-gh-actions
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
- name: Prepare artifacts
|
||||
run: mkdir -p .coverage-data && mv .coverage.* .coverage-data/
|
||||
- uses: actions/upload-artifact@master
|
||||
with:
|
||||
name: coverage-data
|
||||
path: .coverage-data/
|
3
.gitignore
vendored
@ -13,6 +13,7 @@
|
||||
.vscode/
|
||||
|
||||
build/
|
||||
ve/
|
||||
dist/
|
||||
htmlcov/
|
||||
docs/_build
|
||||
@ -23,3 +24,5 @@ tests/sandbox/assets
|
||||
node_modules
|
||||
|
||||
.DS_Store
|
||||
|
||||
.pytest_cache/
|
||||
|
31
.travis.yml
@ -1,31 +0,0 @@
|
||||
---
|
||||
sudo: false
|
||||
language: python
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
env: lint
|
||||
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django111-wagtail113
|
||||
|
||||
install:
|
||||
- pip install tox codecov
|
||||
|
||||
script:
|
||||
- tox
|
||||
|
||||
after_success:
|
||||
- tox -e coverage-report
|
||||
- codecov
|
||||
|
||||
deploy:
|
||||
provider: pypi
|
||||
distributions: sdist bdist_wheel
|
||||
user: praekelt.org
|
||||
password:
|
||||
secure: IxPnc95OFNQsl7kFfLlLc38KfHh/W79VXnhEkdb2x1GZznOsG167QlhpAnyXIJysCQxgMMwLMuPOOdk1WIxOpGNM1/M80hNzPAfxMYWPuwposDdwoIc8SyREPJt16BXWAY+rAH8SHxld9p1YdRbOEPSSglODe4dCmQWsCbKjV3aKv+gZxBvf6pMxUglp2fBIlCwrE77MyMYh9iW0AGzD6atC2xN9NgAm4f+2WFlKCUL/MztLhNWdvWEiibQav6Tts7p8tWrsBVzywDW2IOy3O0ihPgRdISZ7QrxhiJTjlHYPAy4eRGOnYSQXvp6Dy8ONE5a6Uv5g3Xw2UmQo85sSMUs2VxR0G7d+PQgL0A7ENBQ5i7eSAFHYs8EswiGilW2A7mM4qRXwg9URLelYSdkM+aNXvR+25dCsXakiO4NjCz/BPgNzxPeQLlBdxR5vHugeM/XYuhy6CHlZrR/uueaO0X8RyhJcqeDjBy58IrwYS3Mpj7QCfBpQ9PqsqXEWV9BKwKiBXM2+3hkhawFDWa0GW2PDbORKtSLy/ORfGLx5Y9qxQYKEGvFQA3iqkTjrLj+KeUziKtuvEAcvsdBIJVIxeoHwdl+qqxEm8A7YuRBnWVxWc3jE6wBXboeFP92gVe/ueoXmY22riK9Ja0pli3TyNga8by9WM8Qp4D2ZqkVXHwg=
|
||||
on:
|
||||
tags: true
|
||||
condition: $TOXENV = py27-django111-wagtail113
|
67
CHANGES
@ -1,3 +1,70 @@
|
||||
0.15.3
|
||||
=================
|
||||
- Add wagtail >= 2.15 support with get_context_data override instead of get_context
|
||||
|
||||
0.15.2
|
||||
=================
|
||||
- Replace staticfiles tag with static
|
||||
|
||||
0.15.1
|
||||
=================
|
||||
- Remove old versions from test matrix
|
||||
- Fix button support in wagtail admin for newer wagtail versions
|
||||
|
||||
0.15.0
|
||||
=================
|
||||
- Fix is_authenticated 'bool' object is not callable error
|
||||
- Add wagtail <=2.11 support
|
||||
- Use Github Actions to test package instead of Travis CI
|
||||
|
||||
0.14.0
|
||||
=================
|
||||
- Fix 'bool' object is not callable error
|
||||
- Fix deleting descendants with variants when deleting a page
|
||||
- Add wagtail 2.6 support
|
||||
|
||||
0.13.0
|
||||
=================
|
||||
- Merged Praekelt fork
|
||||
- Add custom javascript to segment forms
|
||||
- bugfix:exclude variant returns queryset when params is queryset
|
||||
- Added RulePanel, a subclass of InlinePanel, for Rules
|
||||
- Upgrade to Wagtail > 2.0, drop support for Wagtail < 2
|
||||
|
||||
0.12.0
|
||||
==================
|
||||
- Fix Django version classifier in setup.py
|
||||
|
||||
0.12.0
|
||||
==================
|
||||
- Merged forks of Torchbox and Praekelt
|
||||
- Wagtail 2 compatibility
|
||||
- Makefile adjustments for portability
|
||||
- Adds simple segment forcing for superusers
|
||||
- Fix excluding pages without variant
|
||||
- Fix bug on visiting a segment page in the admin
|
||||
- Use Wagtail's logic in the page count in the dash
|
||||
- Prevent corrected summary item from counting the root page
|
||||
- Delete variants of a page that is being deleted
|
||||
- Add end user and developer documentation
|
||||
- Add an option to show a personalised block to everyone
|
||||
- Add origin country rule (#190)
|
||||
- Return 404 if variant page is accessed directly (#188)
|
||||
- Do not generate sitemap entries for variants (#187)
|
||||
- Remove restrictive wagtail dependency version constraint (#192)
|
||||
|
||||
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
|
||||
|
@ -8,3 +8,13 @@ Contributors
|
||||
* Michael van Tellingen
|
||||
* Pim Vernooij
|
||||
* Tomasz Knapik
|
||||
* Kaitlyn Crawford
|
||||
* Todd Dembrey
|
||||
* Nathan Begbie
|
||||
* Rob Moorman
|
||||
* Tom Dyson
|
||||
* Bertrand Bordage
|
||||
* Alex Muller
|
||||
* Saeed Marzban
|
||||
* Milton Madanda
|
||||
* Mike Dingjan
|
||||
|
9
Makefile
@ -1,13 +1,13 @@
|
||||
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
|
||||
|
||||
all: clean requirements dist
|
||||
|
||||
default: develop
|
||||
|
||||
all: clean requirements dist
|
||||
|
||||
clean:
|
||||
find src -name '*.pyc' -delete
|
||||
find tests -name '*.pyc' -delete
|
||||
find . -name '*.egg-info' -delete
|
||||
find . -name '*.egg-info' |xargs rm -rf
|
||||
|
||||
requirements:
|
||||
pip install --upgrade -e .[docs,test]
|
||||
@ -38,7 +38,8 @@ isort:
|
||||
isort --recursive src tests
|
||||
|
||||
dist:
|
||||
./setup.py sdist bdist_wheel
|
||||
pip install wheel
|
||||
python ./setup.py sdist bdist_wheel
|
||||
|
||||
sandbox:
|
||||
pip install -r sandbox/requirements.txt
|
||||
|
7
README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Cavemanon's fork of [wagtail-personalisation](https://github.com/wagtail/wagtail-personalisation)
|
||||
For use in [RocksForMedia](https://git.cavemanon.xyz/Cavemanon/RocksForMedia) primarily.
|
||||
|
||||
## Major Changes
|
||||
* 4.2 support merged into master
|
||||
* Metadata changes in setup.py
|
||||
* Dummy template_name value given to SegmentSummaryPanel to prevent crashing with Wagtail on render
|
75
README.rst
@ -1,75 +0,0 @@
|
||||
.. start-no-pypi
|
||||
|
||||
.. image:: https://readthedocs.org/projects/wagtail-personalisation/badge/?version=latest
|
||||
:target: http://wagtail-personalisation.readthedocs.io/en/latest/?badge=latest
|
||||
|
||||
.. image:: https://travis-ci.org/LabD/wagtail-personalisation.svg?branch=master
|
||||
:target: https://travis-ci.org/LabD/wagtail-personalisation
|
||||
|
||||
.. image:: http://codecov.io/github/LabD/wagtail-personalisation/coverage.svg?branch=master
|
||||
:target: http://codecov.io/github/LabD/wagtail-personalisation?branch=master
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/wagtail-personalisation.svg
|
||||
:target: https://pypi.python.org/pypi/wagtail-personalisation/
|
||||
|
||||
.. end-no-pypi
|
||||
|
||||
Wagtail Personalisation
|
||||
=======================
|
||||
|
||||
Wagtail Personalisation is a fully-featured personalisation module for
|
||||
`Wagtail CMS`_. It enables editors to create customised pages
|
||||
- or parts of pages - based on segments whose rules are configured directly
|
||||
in the admin interface.
|
||||
|
||||
.. _Wagtail CMS: http://wagtail.io/
|
||||
|
||||
.. image:: logo.png
|
||||
:scale: 50 %
|
||||
:alt: Wagxperience
|
||||
:align: center
|
||||
|
||||
|
||||
.. image:: screenshot.png
|
||||
|
||||
|
||||
Instructions
|
||||
------------
|
||||
Wagtail Personalisation requires Wagtail 1.9 or 1.10 and Django 1.9, 1.10 or 1.11.
|
||||
|
||||
To install the package with pip::
|
||||
|
||||
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',
|
||||
# ...
|
||||
]
|
||||
|
||||
Sandbox
|
||||
-------
|
||||
|
||||
To experiment with the package you can use the sandbox provided in
|
||||
this repository. To install this you will need to create and activate a
|
||||
virtualenv and then run ``make sandbox``. This will start a fresh Wagtail
|
||||
install, with the personalisation module enabled, on http://localhost:8000
|
||||
and http://localhost:8000/cms/. The superuser credentials are
|
||||
``superuser@example.com`` with the password ``testing``.
|
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 |
82
docs/conf.py
@ -17,10 +17,18 @@
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
import os
|
||||
import sys
|
||||
|
||||
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 ------------------------------------------------
|
||||
|
||||
@ -34,46 +42,46 @@
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = 'wagtail-personalisation'
|
||||
copyright = '2017, Lab Digital BV'
|
||||
author = 'Lab Digital BV'
|
||||
project = "wagtail-personalisation"
|
||||
copyright = "2019, Lab Digital BV"
|
||||
author = "Lab Digital BV"
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.11.0'
|
||||
version = "0.15.3"
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.11.0'
|
||||
release = "0.15.3"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
# language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
@ -84,7 +92,7 @@ todo_include_todos = False
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# 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
|
||||
# further. For a list of options available for each theme, see the
|
||||
@ -92,14 +100,11 @@ html_theme = 'alabaster'
|
||||
#
|
||||
# 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,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
@ -109,7 +114,7 @@ html_theme_options = {
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'wagtail-personalisationdoc'
|
||||
htmlhelp_basename = "wagtail-personalisationdoc"
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
@ -118,15 +123,12 @@ latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
@ -136,8 +138,13 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'wagtail-personalisation.tex', 'wagtail-personalisation Documentation',
|
||||
'Lab Digital BV', 'manual'),
|
||||
(
|
||||
master_doc,
|
||||
"wagtail-personalisation.tex",
|
||||
"wagtail-personalisation Documentation",
|
||||
"Lab Digital BV",
|
||||
"manual",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@ -146,8 +153,13 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
|
||||
[author], 1)
|
||||
(
|
||||
master_doc,
|
||||
"wagtail-personalisation",
|
||||
"wagtail-personalisation Documentation",
|
||||
[author],
|
||||
1,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@ -157,7 +169,13 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
|
||||
author, 'wagtail-personalisation', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
(
|
||||
master_doc,
|
||||
"wagtail-personalisation",
|
||||
"wagtail-personalisation Documentation",
|
||||
author,
|
||||
"wagtail-personalisation",
|
||||
"One line description of project.",
|
||||
"Miscellaneous",
|
||||
),
|
||||
]
|
||||
|
@ -1,6 +1,10 @@
|
||||
Included rules
|
||||
==============
|
||||
|
||||
Wagxperience comes with a base set of rules that allow you to start segmenting
|
||||
your visitors quickly.
|
||||
|
||||
|
||||
Time rule
|
||||
---------
|
||||
|
||||
@ -16,11 +20,12 @@ End time The end time of your time frame.
|
||||
|
||||
``wagtail_personalisation.rules.TimeRule``
|
||||
|
||||
|
||||
Day rule
|
||||
--------
|
||||
|
||||
The day rule allows you to segment visitors based on the day of their visit.
|
||||
Select one or multiple days on which you'd like your segment to be applied.
|
||||
Select one or multiple days on which you would like your segment to be applied.
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
@ -36,6 +41,7 @@ Sunday Matches when the visitors visits on a sunday.
|
||||
|
||||
``wagtail_personalisation.rules.DayRule``
|
||||
|
||||
|
||||
Referral rule
|
||||
-------------
|
||||
|
||||
@ -54,6 +60,7 @@ Regex string The regex string to match the referral header to.
|
||||
|
||||
``wagtail_personalisation.rules.ReferralRule``
|
||||
|
||||
|
||||
Visit count rule
|
||||
----------------
|
||||
|
||||
@ -72,6 +79,7 @@ Operator Whether to match for more than, less than or equal to the
|
||||
|
||||
``wagtail_personalisation.rules.VisitCountRule``
|
||||
|
||||
|
||||
Query rule
|
||||
----------
|
||||
|
||||
@ -92,6 +100,7 @@ Value The second part of the query ('ourbestoffer').
|
||||
|
||||
``wagtail_personalisation.rules.QueryRule``
|
||||
|
||||
|
||||
Device rule
|
||||
-----------
|
||||
|
||||
@ -108,6 +117,7 @@ Desktop Matches when the visitor uses a desktop.
|
||||
|
||||
``wagtail_personalisation.rules.DeviceRule``
|
||||
|
||||
|
||||
User is logged in rule
|
||||
----------------------
|
||||
|
||||
@ -121,3 +131,47 @@ Is logged in Whether the user is logged in or logged out.
|
||||
================== ==========================================================
|
||||
|
||||
``wagtail_personalisation.rules.UserIsLoggedInRule``
|
||||
|
||||
|
||||
Origin country rule
|
||||
-------------------
|
||||
|
||||
The origin country rule allows you to match visitors based on the origin
|
||||
country of their request. This rule requires to have set up a way to detect
|
||||
countries beforehand.
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
================== ==========================================================
|
||||
Country What country user's request comes from.
|
||||
================== ==========================================================
|
||||
|
||||
You must have one of the following configurations set up in order to
|
||||
make it work.
|
||||
|
||||
- Cloudflare IP Geolocation - ``cf-ipcountry`` HTTP header set with a value of
|
||||
the alpha-2 country format.
|
||||
- CloudFront Geo-Targeting - ``cloudfront-viewer-country`` header set with a
|
||||
value of the alpha-2 country format.
|
||||
- The last fallback is to use GeoIP2 module that is included with Django. This
|
||||
requires setting up an IP database beforehand, see the Django's
|
||||
`GeoIP2 instructions <https://docs.djangoproject.com/en/stable/ref/contrib/gis/geoip2/>`_
|
||||
for more information. It will use IP of the request, using HTTP header
|
||||
the ``x-forwarded-for`` HTTP header and ``REMOTE_ADDR`` server value as a
|
||||
fallback. If you want to use a custom logic when obtaining IP address, please
|
||||
set the ``WAGTAIL_PERSONALISATION_IP_FUNCTION`` setting to the function that takes a
|
||||
request as an argument, e.g.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# settings.py
|
||||
|
||||
WAGTAIL_PERSONALISATION_IP_FUNCTION = 'yourproject.utils.get_client_ip'
|
||||
|
||||
|
||||
# yourproject/utils.py
|
||||
|
||||
def get_client_ip(request):
|
||||
return request['HTTP_CF_CONNECTING_IP']
|
||||
|
||||
``wagtail_personalisation.rules.OriginCountryRule``
|
||||
|
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_ 4.1+ and Django_ 3.2+
|
||||
|
||||
.. _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
|
||||
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::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
getting_started
|
||||
implementation
|
||||
usage_guide
|
||||
getting_started/index
|
||||
usage_guide/index
|
||||
editor_guide/index
|
||||
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.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
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": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/LabD/wagtail-personalisation.git"
|
||||
"url": "git+https://github.com/wagtail/wagtail-personalisation.git"
|
||||
},
|
||||
"author": "Lab Digital",
|
||||
"license": "ISC",
|
||||
"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"
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
Django>=1.11,<1.12
|
||||
wagtail>=1.10,<1.11
|
||||
django-debug-toolbar==1.8
|
||||
Django>=3.2
|
||||
wagtail>=4.1
|
||||
django-debug-toolbar==3.8.1
|
||||
-e .[docs,test]
|
||||
|
@ -1 +0,0 @@
|
||||
|
||||
|
@ -2,9 +2,8 @@
|
||||
# Generated by Django 1.11.1 on 2017-05-31 16:59
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -12,19 +11,29 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||
('wagtail_personalisation', '0011_personalisablepagemetadata'),
|
||||
("wagtailcore", "0033_remove_golive_expiry_help_text"),
|
||||
("wagtail_personalisation", "0011_personalisablepagemetadata"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='HomePage',
|
||||
name="HomePage",
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||
(
|
||||
"page_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="wagtailcore.Page",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=('wagtailcore.page', models.Model),
|
||||
bases=("wagtailcore.page", models.Model),
|
||||
),
|
||||
]
|
||||
|
@ -6,10 +6,10 @@ from django.db import migrations
|
||||
|
||||
def create_homepage(apps, schema_editor):
|
||||
# Get models
|
||||
ContentType = apps.get_model('contenttypes.ContentType')
|
||||
Page = apps.get_model('wagtailcore.Page')
|
||||
Site = apps.get_model('wagtailcore.Site')
|
||||
HomePage = apps.get_model('home.HomePage')
|
||||
ContentType = apps.get_model("contenttypes.ContentType")
|
||||
Page = apps.get_model("wagtailcore.Page")
|
||||
Site = apps.get_model("wagtailcore.Site")
|
||||
HomePage = apps.get_model("home.HomePage")
|
||||
|
||||
# Delete the default homepage
|
||||
# If migration is run multiple times, it may have already been deleted
|
||||
@ -17,41 +17,41 @@ def create_homepage(apps, schema_editor):
|
||||
|
||||
# Create content type for homepage model
|
||||
homepage_content_type, __ = ContentType.objects.get_or_create(
|
||||
model='homepage', app_label='home')
|
||||
model="homepage", app_label="home"
|
||||
)
|
||||
|
||||
# Create a new homepage
|
||||
homepage = HomePage.objects.create(
|
||||
title="Home",
|
||||
slug='home',
|
||||
slug="home",
|
||||
content_type=homepage_content_type,
|
||||
path='00010001',
|
||||
path="00010001",
|
||||
depth=2,
|
||||
numchild=0,
|
||||
url_path='/home/',
|
||||
url_path="/home/",
|
||||
)
|
||||
|
||||
# Create a site with the new homepage set as the root
|
||||
Site.objects.create(
|
||||
hostname='localhost', root_page=homepage, is_default_site=True)
|
||||
Site.objects.create(hostname="localhost", root_page=homepage, is_default_site=True)
|
||||
|
||||
|
||||
def remove_homepage(apps, schema_editor):
|
||||
# Get models
|
||||
ContentType = apps.get_model('contenttypes.ContentType')
|
||||
HomePage = apps.get_model('home.HomePage')
|
||||
ContentType = apps.get_model("contenttypes.ContentType")
|
||||
HomePage = apps.get_model("home.HomePage")
|
||||
|
||||
# Delete the default homepage
|
||||
# Page and Site objects CASCADE
|
||||
HomePage.objects.filter(slug='home', depth=2).delete()
|
||||
HomePage.objects.filter(slug="home", depth=2).delete()
|
||||
|
||||
# Delete content type for homepage model
|
||||
ContentType.objects.filter(model='homepage', app_label='home').delete()
|
||||
ContentType.objects.filter(model="homepage", app_label="home").delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0001_initial'),
|
||||
("home", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -3,28 +3,55 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import wagtail.wagtailcore.fields
|
||||
|
||||
import wagtail.blocks as wagtail_blocks
|
||||
import wagtail.fields as wagtail_fields
|
||||
|
||||
import wagtail_personalisation
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0002_create_homepage'),
|
||||
("home", "0002_create_homepage"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='homepage',
|
||||
name='intro',
|
||||
field=wagtail.wagtailcore.fields.RichTextField(
|
||||
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
|
||||
model_name="homepage",
|
||||
name="intro",
|
||||
field=wagtail_fields.RichTextField(
|
||||
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepage',
|
||||
name='body',
|
||||
field=wagtail.wagtailcore.fields.StreamField((('personalisable_paragraph', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
|
||||
model_name="homepage",
|
||||
name="body",
|
||||
field=wagtail_fields.StreamField(
|
||||
(
|
||||
(
|
||||
"personalisable_paragraph",
|
||||
wagtail_blocks.StructBlock(
|
||||
(
|
||||
(
|
||||
"segment",
|
||||
wagtail_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_blocks.RichTextBlock()),
|
||||
),
|
||||
icon="pilcrow",
|
||||
),
|
||||
),
|
||||
),
|
||||
default="",
|
||||
use_json_field=True,
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
@ -1,23 +1,32 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
|
||||
from wagtail.wagtailcore import blocks
|
||||
from wagtail.wagtailcore.fields import RichTextField, StreamField
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail import blocks
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.models import Page
|
||||
|
||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||
from wagtail_personalisation.blocks import PersonalisedStructBlock
|
||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||
|
||||
|
||||
class HomePage(PersonalisablePageMixin, Page):
|
||||
intro = RichTextField()
|
||||
body = StreamField([
|
||||
('personalisable_paragraph', PersonalisedStructBlock([
|
||||
('paragraph', blocks.RichTextBlock()),
|
||||
], icon='pilcrow'))
|
||||
])
|
||||
body = StreamField(
|
||||
[
|
||||
(
|
||||
"personalisable_paragraph",
|
||||
PersonalisedStructBlock(
|
||||
[
|
||||
("paragraph", blocks.RichTextBlock()),
|
||||
],
|
||||
icon="pilcrow",
|
||||
),
|
||||
)
|
||||
],
|
||||
use_json_field=True,
|
||||
)
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
RichTextFieldPanel('intro'),
|
||||
StreamFieldPanel('body'),
|
||||
FieldPanel("intro"),
|
||||
FieldPanel("body"),
|
||||
]
|
||||
|
@ -3,13 +3,13 @@ from __future__ import absolute_import, unicode_literals
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||
from django.shortcuts import render
|
||||
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailsearch.models import Query
|
||||
from wagtail.models import Page
|
||||
from wagtail.search.models import Query
|
||||
|
||||
|
||||
def search(request):
|
||||
search_query = request.GET.get('query', None)
|
||||
page = request.GET.get('page', 1)
|
||||
search_query = request.GET.get("query", None)
|
||||
page = request.GET.get("page", 1)
|
||||
|
||||
# Search
|
||||
if search_query:
|
||||
@ -30,7 +30,11 @@ def search(request):
|
||||
except EmptyPage:
|
||||
search_results = paginator.page(paginator.num_pages)
|
||||
|
||||
return render(request, 'search/search.html', {
|
||||
'search_query': search_query,
|
||||
'search_results': search_results,
|
||||
})
|
||||
return render(
|
||||
request,
|
||||
"search/search.html",
|
||||
{
|
||||
"search_query": search_query,
|
||||
"search_results": search_results,
|
||||
},
|
||||
)
|
||||
|
@ -12,31 +12,30 @@ class UserAdmin(BaseUserAdmin):
|
||||
# The fields to be used in displaying the User model.
|
||||
# These override the definitions on the base UserAdmin
|
||||
# that reference specific fields on auth.User.
|
||||
list_display = ['email']
|
||||
list_filter = ['is_superuser']
|
||||
list_display = ["email"]
|
||||
list_filter = ["is_superuser"]
|
||||
|
||||
fieldsets = (
|
||||
(None, {
|
||||
'fields': ['email', 'password']
|
||||
}),
|
||||
('Personal info', {
|
||||
'fields': ['first_name', 'last_name']
|
||||
}),
|
||||
('Permissions', {
|
||||
'fields': [
|
||||
'is_active', 'is_staff', 'is_superuser',
|
||||
'groups', 'user_permissions'
|
||||
(None, {"fields": ["email", "password"]}),
|
||||
("Personal info", {"fields": ["first_name", "last_name"]}),
|
||||
(
|
||||
"Permissions",
|
||||
{
|
||||
"fields": [
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"user_permissions",
|
||||
]
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
|
||||
# overrides get_fieldsets to use this attribute when creating a user.
|
||||
add_fieldsets = (
|
||||
(None, {
|
||||
'classes': ('wide',),
|
||||
'fields': ['email', 'password1', 'password2']
|
||||
}),
|
||||
(None, {"classes": ("wide",), "fields": ["email", "password1", "password2"]}),
|
||||
)
|
||||
search_fields = ['first_name', 'last_name', 'email']
|
||||
ordering = ['email']
|
||||
search_fields = ["first_name", "last_name", "email"]
|
||||
ordering = ["email"]
|
||||
filter_horizontal = []
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from sandbox.apps.user import models
|
||||
|
||||
@ -10,21 +10,22 @@ class UserCreationForm(forms.ModelForm):
|
||||
fields, plus a repeated password.
|
||||
|
||||
"""
|
||||
|
||||
password1 = forms.CharField(
|
||||
label='Password', widget=forms.PasswordInput,
|
||||
required=False)
|
||||
label="Password", widget=forms.PasswordInput, required=False
|
||||
)
|
||||
password2 = forms.CharField(
|
||||
label='Password confirmation', widget=forms.PasswordInput,
|
||||
required=False)
|
||||
label="Password confirmation", widget=forms.PasswordInput, required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = ['email']
|
||||
fields = ["email"]
|
||||
|
||||
def clean_password2(self):
|
||||
# Check that the two password entries match
|
||||
password1 = self.cleaned_data.get('password1')
|
||||
password2 = self.cleaned_data.get('password2')
|
||||
password1 = self.cleaned_data.get("password1")
|
||||
password2 = self.cleaned_data.get("password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise forms.ValidationError("Passwords don't match")
|
||||
return password2
|
||||
@ -32,7 +33,7 @@ class UserCreationForm(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
# Save the provided password in hashed format
|
||||
user = super(UserCreationForm, self).save(commit=False)
|
||||
user.set_password(self.cleaned_data['password1'])
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
@ -44,20 +45,22 @@ class UserChangeForm(forms.ModelForm):
|
||||
password hash display field.
|
||||
|
||||
"""
|
||||
|
||||
password = ReadOnlyPasswordHashField(
|
||||
label=_("Password"),
|
||||
help_text=_("Raw passwords are not stored, so there is no way to see "
|
||||
help_text=_(
|
||||
"Raw passwords are not stored, so there is no way to see "
|
||||
"this user's password, but you can change the password "
|
||||
"using <a href=\"password/\">this form</a>."))
|
||||
'using <a href="password/">this form</a>.'
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
fields = [
|
||||
'email', 'password', 'is_active', 'is_superuser'
|
||||
]
|
||||
fields = ["email", "password", "is_active", "is_superuser"]
|
||||
|
||||
def clean_password(self):
|
||||
# Regardless of what the user provides, return the initial value.
|
||||
# This is done here, rather than on the field, because the
|
||||
# field does not have access to the initial value
|
||||
return self.initial['password']
|
||||
return self.initial["password"]
|
||||
|
@ -3,8 +3,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.contrib.auth.models
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -12,32 +12,109 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0008_alter_user_username_max_length'),
|
||||
("auth", "0008_alter_user_username_max_length"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
name="User",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('first_name', models.CharField(blank=True, max_length=100, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=100, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_superuser",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates that this user has all permissions without explicitly assigning them.",
|
||||
verbose_name="superuser status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"first_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=100, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=100, verbose_name="last name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(
|
||||
blank=True,
|
||||
max_length=254,
|
||||
unique=True,
|
||||
verbose_name="email address",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_staff",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Designates whether the user can log into this admin site.",
|
||||
verbose_name="staff status",
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_active",
|
||||
models.BooleanField(
|
||||
default=True,
|
||||
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", # noqa
|
||||
verbose_name="active",
|
||||
),
|
||||
),
|
||||
(
|
||||
"date_joined",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="date joined"
|
||||
),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", # noqa
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Group",
|
||||
verbose_name="groups",
|
||||
),
|
||||
),
|
||||
(
|
||||
"user_permissions",
|
||||
models.ManyToManyField(
|
||||
blank=True,
|
||||
help_text="Specific permissions for this user.",
|
||||
related_name="user_set",
|
||||
related_query_name="user",
|
||||
to="auth.Permission",
|
||||
verbose_name="user permissions",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'user',
|
||||
'verbose_name_plural': 'users',
|
||||
"verbose_name": "user",
|
||||
"verbose_name_plural": "users",
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
@ -0,0 +1,21 @@
|
||||
# Generated by Django 2.1.7 on 2019-03-15 12:54
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
import sandbox.apps.user.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("user", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name="user",
|
||||
managers=[
|
||||
("objects", sandbox.apps.user.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,43 +1,80 @@
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser, PermissionsMixin, UserManager)
|
||||
AbstractBaseUser,
|
||||
BaseUserManager,
|
||||
PermissionsMixin,
|
||||
)
|
||||
from django.core.mail import send_mail
|
||||
from django.db import connections, models
|
||||
from django.dispatch import receiver
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
use_in_migrations = True
|
||||
|
||||
def _create_user(self, email, password, **extra_fields):
|
||||
"""
|
||||
Create and save a user with the given username, email, and password.
|
||||
"""
|
||||
if not email:
|
||||
raise ValueError("The given email address must be set")
|
||||
email = self.normalize_email(email)
|
||||
user = self.model(email=email, **extra_fields)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_user(self, email, password=None, **extra_fields):
|
||||
extra_fields.setdefault("is_staff", False)
|
||||
extra_fields.setdefault("is_superuser", False)
|
||||
return self._create_user(email, password, **extra_fields)
|
||||
|
||||
def create_superuser(self, email, password, **extra_fields):
|
||||
extra_fields.setdefault("is_staff", True)
|
||||
extra_fields.setdefault("is_superuser", True)
|
||||
|
||||
if extra_fields.get("is_staff") is not True:
|
||||
raise ValueError("Superuser must have is_staff=True.")
|
||||
if extra_fields.get("is_superuser") is not True:
|
||||
raise ValueError("Superuser must have is_superuser=True.")
|
||||
return self._create_user(email, password, **extra_fields)
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
"""Cusomtized version of the default `AbstractUser` from Django.
|
||||
"""Customized version of the default `AbstractUser` from Django."""
|
||||
|
||||
"""
|
||||
first_name = models.CharField(_('first name'), max_length=100, blank=True)
|
||||
last_name = models.CharField(_('last name'), max_length=100, blank=True)
|
||||
email = models.EmailField(_('email address'), blank=True, unique=True)
|
||||
first_name = models.CharField(_("first name"), max_length=100, blank=True)
|
||||
last_name = models.CharField(_("last name"), max_length=100, blank=True)
|
||||
email = models.EmailField(_("email address"), blank=True, unique=True)
|
||||
is_staff = models.BooleanField(
|
||||
_('staff status'), default=False,
|
||||
help_text=_('Designates whether the user can log into this admin '
|
||||
'site.'))
|
||||
_("staff status"),
|
||||
default=False,
|
||||
help_text=_("Designates whether the user can log into this admin " "site."),
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
_('active'), default=True,
|
||||
help_text=_('Designates whether this user should be treated as '
|
||||
'active. Unselect this instead of deleting accounts.'))
|
||||
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
|
||||
_("active"),
|
||||
default=True,
|
||||
help_text=_(
|
||||
"Designates whether this user should be treated as "
|
||||
"active. Unselect this instead of deleting accounts."
|
||||
),
|
||||
)
|
||||
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user')
|
||||
verbose_name_plural = _('users')
|
||||
verbose_name = _("user")
|
||||
verbose_name_plural = _("users")
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Returns the first_name plus the last_name, with a space in between.
|
||||
"""
|
||||
full_name = '%s %s' % (self.first_name, self.last_name)
|
||||
full_name = "%s %s" % (self.first_name, self.last_name)
|
||||
return full_name.strip()
|
||||
|
||||
def get_short_name(self):
|
||||
|
@ -14,6 +14,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
from importlib.util import find_spec
|
||||
|
||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||
@ -21,98 +22,98 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||
DEBUG = True
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '^anfvx$i7%wts8j=7u1h5ua$w6c76*333(@h)rrjlak1c&x0r+'
|
||||
SECRET_KEY = "^anfvx$i7%wts8j=7u1h5ua$w6c76*333(@h)rrjlak1c&x0r+"
|
||||
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'wagtail.wagtailforms',
|
||||
'wagtail.wagtailredirects',
|
||||
'wagtail.wagtailembeds',
|
||||
'wagtail.wagtailsites',
|
||||
'wagtail.wagtailusers',
|
||||
'wagtail.wagtailsnippets',
|
||||
'wagtail.wagtaildocs',
|
||||
'wagtail.wagtailimages',
|
||||
'wagtail.wagtailsearch',
|
||||
'wagtail.wagtailadmin',
|
||||
'wagtail.wagtailcore',
|
||||
'wagtail.contrib.modeladmin',
|
||||
|
||||
'wagtailfontawesome',
|
||||
'modelcluster',
|
||||
'taggit',
|
||||
'debug_toolbar',
|
||||
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'wagtail_personalisation',
|
||||
|
||||
'sandbox.apps.home',
|
||||
'sandbox.apps.search',
|
||||
'sandbox.apps.user',
|
||||
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.sites",
|
||||
"django.contrib.staticfiles",
|
||||
"wagtail.contrib.forms",
|
||||
"wagtail.contrib.redirects",
|
||||
"wagtail.embeds",
|
||||
"wagtail.sites",
|
||||
"wagtail.users",
|
||||
"wagtail.snippets",
|
||||
"wagtail.documents",
|
||||
"wagtail.images",
|
||||
"wagtail.search",
|
||||
"wagtail.admin",
|
||||
"wagtail",
|
||||
"wagtail.contrib.modeladmin",
|
||||
"wagtailfontawesome",
|
||||
"modelcluster",
|
||||
"taggit",
|
||||
"debug_toolbar",
|
||||
"wagtail_personalisation",
|
||||
"sandbox.apps.home",
|
||||
"sandbox.apps.search",
|
||||
"sandbox.apps.user",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
|
||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.http.ConditionalGetMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'sandbox.urls'
|
||||
if find_spec("wagtail.contrib.legacy"):
|
||||
MIDDLEWARE += ("wagtail.contrib.legacy.sitemiddleware.SiteMiddleware",)
|
||||
else:
|
||||
MIDDLEWARE += ("wagtail.middleware.SiteMiddleware",)
|
||||
|
||||
ROOT_URLCONF = "sandbox.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
os.path.join(PROJECT_DIR, 'templates'),
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [
|
||||
os.path.join(PROJECT_DIR, "templates"),
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'sandbox.wsgi.application'
|
||||
WSGI_APPLICATION = "sandbox.wsgi.application"
|
||||
|
||||
AUTH_USER_MODEL = 'user.User'
|
||||
AUTH_USER_MODEL = "user.User"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'db.sqlite3',
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": "db.sqlite3",
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,9 +121,9 @@ DATABASES = {
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
@ -135,19 +136,19 @@ USE_TZ = True
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATICFILES_FINDERS = [
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
"django.contrib.staticfiles.finders.FileSystemFinder",
|
||||
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
|
||||
]
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(PROJECT_DIR, 'static'),
|
||||
os.path.join(PROJECT_DIR, "static"),
|
||||
]
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
|
||||
# Wagtail settings
|
||||
@ -156,7 +157,11 @@ WAGTAIL_SITE_NAME = "sandbox"
|
||||
|
||||
# Base URL to use when referring to full URLs within the Wagtail admin backend -
|
||||
# e.g. in notification emails. Don't include '/admin' or a trailing slash
|
||||
BASE_URL = 'http://example.com'
|
||||
BASE_URL = "http://example.com"
|
||||
|
||||
|
||||
INTERNAL_IPS = ['127.0.0.1']
|
||||
INTERNAL_IPS = ["127.0.0.1"]
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
WAGTAILADMIN_BASE_URL = "http://localhost:8000/admin"
|
||||
|
@ -20,6 +20,10 @@
|
||||
</title>
|
||||
<meta name="description" content="" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{% comment %} Required in Wagtail v4+ for page previews {% endcomment %}
|
||||
{% if request.in_preview_panel %}
|
||||
<base target="_blank">
|
||||
{% endif %}
|
||||
|
||||
{# Global stylesheets #}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/sandbox.css' %}">
|
||||
|
@ -2,27 +2,25 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import debug_toolbar
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.contrib import admin
|
||||
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
||||
from wagtail.wagtailcore import urls as wagtail_urls
|
||||
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
||||
from django.urls import include, re_path
|
||||
|
||||
from wagtail import urls as wagtail_urls
|
||||
from wagtail.admin import urls as wagtailadmin_urls
|
||||
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
from sandbox.apps.search import views as search_views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
|
||||
url(r'^cms/', include(wagtailadmin_urls)),
|
||||
url(r'^documents/', include(wagtaildocs_urls)),
|
||||
|
||||
url(r'^search/$', search_views.search, name='search'),
|
||||
|
||||
re_path(r"^django-admin/", admin.site.urls),
|
||||
re_path(r"^admin/", include(wagtailadmin_urls)),
|
||||
re_path(r"^documents/", include(wagtaildocs_urls)),
|
||||
re_path(r"^search/$", search_views.search, name="search"),
|
||||
# For anything not caught by a more specific rule above, hand over to
|
||||
# Wagtail's page serving mechanism. This should be the last pattern in
|
||||
# the list:
|
||||
url(r'', include(wagtail_urls)),
|
||||
|
||||
re_path(r"", include(wagtail_urls)),
|
||||
# Alternatively, if you want Wagtail pages to be served from a subpath
|
||||
# of your site, rather than the site root:
|
||||
# url(r'^pages/', include(wagtail_urls)),
|
||||
@ -38,5 +36,5 @@ if settings.DEBUG:
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
re_path(r"^__debug__/", include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
||||
|
14
setup.cfg
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.11.0
|
||||
current_version = 0.15.3
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = {new_version}
|
||||
@ -13,19 +13,21 @@ testpaths = tests
|
||||
python_paths = .
|
||||
|
||||
[flake8]
|
||||
ignore = E731
|
||||
ignore = E731,W503
|
||||
max-line-length = 120
|
||||
exclude =
|
||||
src/**/migrations/*.py
|
||||
|
||||
[isort]
|
||||
profile = black
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[coverage:run]
|
||||
omit =
|
||||
src/**/migrations/*.py
|
||||
[coverage]
|
||||
include = src/**/
|
||||
omit = src/**/migrations/*.py
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
[bumpversion:file:docs/conf.py]
|
||||
|
||||
|
102
setup.py
@ -1,69 +1,77 @@
|
||||
import re
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
install_requires = [
|
||||
'wagtail>=1.10,<1.14',
|
||||
'user-agents>=1.0.1',
|
||||
'wagtailfontawesome>=1.0.6',
|
||||
"wagtail>=4.1",
|
||||
"user-agents>=1.1.0",
|
||||
"wagtailfontawesome>=1.2.1",
|
||||
"pycountry",
|
||||
"python-dotenv"
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
'factory_boy==2.8.1',
|
||||
'flake8',
|
||||
'flake8-blind-except',
|
||||
'flake8-debugger',
|
||||
'flake8-imports',
|
||||
'freezegun==0.3.8',
|
||||
'pytest-cov==2.4.0',
|
||||
'pytest-django==3.1.2',
|
||||
'pytest-sugar==0.7.1',
|
||||
'pytest-mock==1.6.3',
|
||||
'pytest==3.1.0',
|
||||
'wagtail_factories==0.3.0',
|
||||
"factory_boy==3.2.1",
|
||||
"flake8-blind-except",
|
||||
"flake8-debugger",
|
||||
"flake8-isort",
|
||||
"flake8",
|
||||
"freezegun==1.2.1",
|
||||
"pytest-cov==3.0.0",
|
||||
"pytest-django==4.5.2",
|
||||
"pytest-pythonpath==0.7.4",
|
||||
"pytest-sugar==0.9.4",
|
||||
"pytest==6.2.5",
|
||||
"wagtail_factories==4.0.0",
|
||||
"pytest-mock==3.8.1",
|
||||
]
|
||||
|
||||
docs_require = [
|
||||
'sphinx>=1.4.0',
|
||||
"sphinx>=1.7.6",
|
||||
"sphinx_rtd_theme>=0.4.0",
|
||||
]
|
||||
|
||||
with open('README.rst') as fh:
|
||||
long_description = re.sub(
|
||||
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
|
||||
#with open("README.rst") as fh:
|
||||
# long_description = re.sub(
|
||||
# "^.. start-no-pypi.*^.. end-no-pypi", "", fh.read(), flags=re.M | re.S
|
||||
# )
|
||||
|
||||
setup(
|
||||
name='wagtail-personalisation-molo',
|
||||
version='0.11.0',
|
||||
description='A forked version of Wagtail add-on for showing personalized content',
|
||||
author='Praekelt.org',
|
||||
author_email='dev@praekeltfoundation.org',
|
||||
url='https://github.com/praekeltfoundation/wagtail-personalisation/',
|
||||
name="wagtail-personalisation",
|
||||
version="0.15.4",
|
||||
description="A Wagtail add-on for showing personalized content maintained by Cavemanon",
|
||||
author="Lab Digital BV and others; Maintained by Michael Yick from Cavemanon",
|
||||
author_email="opensource@labdigital.nl, cavemanon@mail.snootgame.xyz",
|
||||
url="https://git.cavemanon.xyz/Cavemanon/cavemanon-wagtail-personalisation",
|
||||
install_requires=install_requires,
|
||||
tests_require=tests_require,
|
||||
extras_require={
|
||||
'docs': docs_require,
|
||||
'test': tests_require,
|
||||
"docs": docs_require,
|
||||
"test": tests_require,
|
||||
},
|
||||
packages=find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
packages=find_packages("src"),
|
||||
package_dir={"": "src"},
|
||||
include_package_data=True,
|
||||
license='MIT',
|
||||
long_description=long_description,
|
||||
license="MIT",
|
||||
#long_description=long_description,
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Web Environment",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: BSD License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Framework :: Django",
|
||||
"Framework :: Django :: 3.2",
|
||||
"Framework :: Django :: 4.0",
|
||||
"Framework :: Django :: 4.1",
|
||||
"Framework :: Wagtail",
|
||||
"Framework :: Wagtail :: 4",
|
||||
"Topic :: Internet :: WWW/HTTP :: Site Management",
|
||||
],
|
||||
)
|
||||
|
@ -1 +1 @@
|
||||
default_app_config = 'wagtail_personalisation.config.WagtailPersonalisationConfig'
|
||||
default_app_config = "wagtail_personalisation.config.WagtailPersonalisationConfig"
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.module_loading import import_string
|
||||
@ -9,7 +7,7 @@ from wagtail_personalisation.rules import AbstractBaseRule
|
||||
from wagtail_personalisation.utils import create_segment_dictionary
|
||||
|
||||
|
||||
class BaseSegmentsAdapter(object):
|
||||
class BaseSegmentsAdapter:
|
||||
"""Base segments adapter."""
|
||||
|
||||
def __init__(self, request):
|
||||
@ -63,9 +61,15 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
|
||||
def __init__(self, request):
|
||||
super(SessionSegmentsAdapter, self).__init__(request)
|
||||
self.request.session.setdefault('segments', [])
|
||||
self.request.session.setdefault("segments", [])
|
||||
self._segment_cache = None
|
||||
|
||||
def _segments(self, ids=None):
|
||||
if not ids:
|
||||
ids = []
|
||||
segments = Segment.objects.enabled().filter(persistent=True).filter(pk__in=ids)
|
||||
return segments
|
||||
|
||||
def get_segments(self, key="segments"):
|
||||
"""Return the persistent segments stored in the request session.
|
||||
|
||||
@ -81,18 +85,14 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
if key not in self.request.session:
|
||||
return []
|
||||
raw_segments = self.request.session[key]
|
||||
segment_ids = [segment['id'] for segment in raw_segments]
|
||||
segment_ids = [segment["id"] for segment in raw_segments]
|
||||
|
||||
segments = (
|
||||
Segment.objects
|
||||
.enabled()
|
||||
.filter(persistent=True)
|
||||
.in_bulk(segment_ids))
|
||||
segments = self._segments(ids=segment_ids)
|
||||
|
||||
retval = [segments[pk] for pk in segment_ids if pk in segments]
|
||||
result = list(segments)
|
||||
if key == "segments":
|
||||
self._segment_cache = retval
|
||||
return retval
|
||||
self._segment_cache = result
|
||||
return result
|
||||
|
||||
def set_segments(self, segments, key="segments"):
|
||||
"""Set the currently active segments
|
||||
@ -108,7 +108,7 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
segment_ids = set()
|
||||
for segment in segments:
|
||||
serialized = create_segment_dictionary(segment)
|
||||
if serialized['id'] in segment_ids:
|
||||
if serialized["id"] in segment_ids:
|
||||
continue
|
||||
|
||||
cache_segments.append(segment)
|
||||
@ -128,47 +128,50 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
:rtype: wagtail_personalisation.models.Segment or None
|
||||
|
||||
"""
|
||||
for segment in self.get_segments():
|
||||
if segment.pk == segment_id:
|
||||
return segment
|
||||
segments = self._segments(ids=[segment_id])
|
||||
if segments.exists():
|
||||
return segments.get()
|
||||
|
||||
def add_page_visit(self, page):
|
||||
"""Mark the page as visited by the user"""
|
||||
visit_count = self.request.session.setdefault('visit_count', [])
|
||||
page_visits = [visit for visit in visit_count if visit['id'] == page.pk]
|
||||
visit_count = self.request.session.setdefault("visit_count", [])
|
||||
page_visits = [visit for visit in visit_count if visit["id"] == page.pk]
|
||||
|
||||
if page_visits:
|
||||
for page_visit in page_visits:
|
||||
page_visit['count'] += 1
|
||||
page_visit['path'] = page.url_path if page else self.request.path
|
||||
page_visit["count"] += 1
|
||||
page_visit["path"] = page.url_path if page else self.request.path
|
||||
self.request.session.modified = True
|
||||
else:
|
||||
visit_count.append({
|
||||
'slug': page.slug,
|
||||
'id': page.pk,
|
||||
'path': page.url_path if page else self.request.path,
|
||||
'count': 1,
|
||||
})
|
||||
visit_count.append(
|
||||
{
|
||||
"slug": page.slug,
|
||||
"id": page.pk,
|
||||
"path": page.url_path if page else self.request.path,
|
||||
"count": 1,
|
||||
}
|
||||
)
|
||||
|
||||
def get_visit_count(self, page=None):
|
||||
"""Return the number of visits on the current request or given page"""
|
||||
path = page.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:
|
||||
if visit['path'] == path:
|
||||
return visit['count']
|
||||
if visit["path"] == path:
|
||||
return visit["count"]
|
||||
return 0
|
||||
|
||||
def update_visit_count(self):
|
||||
"""Update the visit count for all segments in the request session."""
|
||||
segments = self.request.session['segments']
|
||||
segment_pks = [s['id'] for s in segments]
|
||||
segments = self.request.session["segments"]
|
||||
segment_pks = [s["id"] for s in segments]
|
||||
|
||||
# Update counts
|
||||
(Segment.objects
|
||||
.enabled()
|
||||
(
|
||||
Segment.objects.enabled()
|
||||
.filter(pk__in=segment_pks)
|
||||
.update(visit_count=F('visit_count') + 1))
|
||||
.update(visit_count=F("visit_count") + 1)
|
||||
)
|
||||
|
||||
def refresh(self):
|
||||
"""Retrieve the request session segments and verify whether or not they
|
||||
@ -180,30 +183,39 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
|
||||
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.
|
||||
additional_segments = []
|
||||
for segment in enabled_segments:
|
||||
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
|
||||
if (
|
||||
segment.is_static
|
||||
and segment.static_users.filter(id=self.request.user.id).exists()
|
||||
):
|
||||
additional_segments.append(segment)
|
||||
elif (segment.excluded_users.filter(id=self.request.user.id).exists() or
|
||||
segment in excluded_segments):
|
||||
elif any(
|
||||
(
|
||||
segment.excluded_users.filter(id=self.request.user.id).exists(),
|
||||
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)
|
||||
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():
|
||||
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():
|
||||
if segment.is_static and self.request.user.is_authenticated:
|
||||
segment.excluded_users.add(self.request.user)
|
||||
else:
|
||||
excluded_segments += [segment]
|
||||
@ -213,14 +225,17 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
self.update_visit_count()
|
||||
|
||||
|
||||
SEGMENT_ADAPTER_CLASS = import_string(getattr(
|
||||
SEGMENT_ADAPTER_CLASS = import_string(
|
||||
getattr(
|
||||
settings,
|
||||
'PERSONALISATION_SEGMENTS_ADAPTER',
|
||||
'wagtail_personalisation.adapters.SessionSegmentsAdapter'))
|
||||
"PERSONALISATION_SEGMENTS_ADAPTER",
|
||||
"wagtail_personalisation.adapters.SessionSegmentsAdapter",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_segment_adapter(request):
|
||||
"""Return the Segment Adapter for the given request"""
|
||||
if not hasattr(request, 'segment_adapter'):
|
||||
if not hasattr(request, "segment_adapter"):
|
||||
request.segment_adapter = SEGMENT_ADAPTER_CLASS(request)
|
||||
return request.segment_adapter
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from wagtail_personalisation import models, rules
|
||||
@ -10,6 +8,7 @@ class UserIsLoggedInRuleAdminInline(admin.TabularInline):
|
||||
administration interface for segments.
|
||||
|
||||
"""
|
||||
|
||||
model = rules.UserIsLoggedInRule
|
||||
|
||||
|
||||
@ -18,6 +17,7 @@ class TimeRuleAdminInline(admin.TabularInline):
|
||||
administration interface for segments.
|
||||
|
||||
"""
|
||||
|
||||
model = rules.TimeRule
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ class ReferralRuleAdminInline(admin.TabularInline):
|
||||
administration interface for segments.
|
||||
|
||||
"""
|
||||
|
||||
model = rules.ReferralRule
|
||||
|
||||
|
||||
@ -34,13 +35,19 @@ class VisitCountRuleAdminInline(admin.TabularInline):
|
||||
administration interface for segments.
|
||||
|
||||
"""
|
||||
|
||||
model = rules.VisitCountRule
|
||||
|
||||
|
||||
class SegmentAdmin(admin.ModelAdmin):
|
||||
"""Add the inline models to the Segment admin interface."""
|
||||
inlines = (UserIsLoggedInRuleAdminInline, TimeRuleAdminInline,
|
||||
ReferralRuleAdminInline, VisitCountRuleAdminInline)
|
||||
|
||||
inlines = (
|
||||
UserIsLoggedInRuleAdminInline,
|
||||
TimeRuleAdminInline,
|
||||
ReferralRuleAdminInline,
|
||||
VisitCountRuleAdminInline,
|
||||
)
|
||||
|
||||
|
||||
admin.site.register(models.Segment, SegmentAdmin)
|
||||
|
@ -1,18 +1,24 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.urls import re_path
|
||||
|
||||
from wagtail_personalisation import views
|
||||
|
||||
app_name = 'segment'
|
||||
app_name = "segment"
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^segment/(?P<segment_id>[0-9]+)/toggle/$',
|
||||
views.toggle, name='toggle'),
|
||||
url(r'^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$',
|
||||
views.copy_page_view, name='copy_page'),
|
||||
url(r'^segment/toggle_segment_view/$',
|
||||
views.toggle_segment_view, name='toggle_segment_view'),
|
||||
url(r'^segment/users/(?P<segment_id>[0-9]+)$',
|
||||
views.segment_user_data, name='segment_user_data'),
|
||||
re_path(r"^segment/(?P<segment_id>[0-9]+)/toggle/$", views.toggle, name="toggle"),
|
||||
re_path(
|
||||
r"^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$",
|
||||
views.copy_page_view,
|
||||
name="copy_page",
|
||||
),
|
||||
re_path(
|
||||
r"^segment/toggle_segment_view/$",
|
||||
views.toggle_segment_view,
|
||||
name="toggle_segment_view",
|
||||
),
|
||||
re_path(
|
||||
r"^segment/users/(?P<segment_id>[0-9]+)$",
|
||||
views.segment_user_data,
|
||||
name="segment_user_data",
|
||||
),
|
||||
]
|
||||
|
@ -1,14 +1,13 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailcore import blocks
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail import blocks
|
||||
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import Segment
|
||||
|
||||
|
||||
def list_segment_choices():
|
||||
for pk, name in Segment.objects.values_list('pk', 'name'):
|
||||
yield -1, ("Show to everyone")
|
||||
for pk, name in Segment.objects.values_list("pk", "name"):
|
||||
yield pk, name
|
||||
|
||||
|
||||
@ -17,8 +16,10 @@ class PersonalisedStructBlock(blocks.StructBlock):
|
||||
|
||||
segment = blocks.ChoiceBlock(
|
||||
choices=list_segment_choices,
|
||||
required=False, label=_("Personalisation segment"),
|
||||
help_text=_("Only show this content block for users in this segment"))
|
||||
required=False,
|
||||
label=_("Personalisation segment"),
|
||||
help_text=_("Only show this content block for users in this segment"),
|
||||
)
|
||||
|
||||
def render(self, value, context=None):
|
||||
"""Only render this content block for users in this segment.
|
||||
@ -31,14 +32,21 @@ class PersonalisedStructBlock(blocks.StructBlock):
|
||||
:rtype: blocks.StructBlock or empty str
|
||||
|
||||
"""
|
||||
request = context['request']
|
||||
request = context["request"]
|
||||
adapter = get_segment_adapter(request)
|
||||
user_segments = adapter.get_segments()
|
||||
|
||||
if value['segment']:
|
||||
try:
|
||||
segment_id = int(value["segment"])
|
||||
except (ValueError, TypeError):
|
||||
return ""
|
||||
|
||||
if segment_id > 0:
|
||||
for segment in user_segments:
|
||||
if segment.id == int(value['segment']):
|
||||
return super(PersonalisedStructBlock, self).render(
|
||||
value, context)
|
||||
if segment.id == segment_id:
|
||||
return super(PersonalisedStructBlock, self).render(value, context)
|
||||
|
||||
if segment_id == -1:
|
||||
return super(PersonalisedStructBlock, self).render(value, context)
|
||||
|
||||
return ""
|
||||
|
@ -1,11 +1,12 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class WagtailPersonalisationConfig(AppConfig):
|
||||
label = 'wagtail_personalisation'
|
||||
name = 'wagtail_personalisation'
|
||||
verbose_name = _('Wagtail Personalisation')
|
||||
label = "wagtail_personalisation"
|
||||
name = "wagtail_personalisation"
|
||||
verbose_name = _("Wagtail Personalisation")
|
||||
default_auto_field = "django.db.models.AutoField"
|
||||
|
||||
def ready(self):
|
||||
from wagtail_personalisation import receivers
|
||||
|
@ -1,23 +1,19 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import functools
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
from itertools import takewhile
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||
from django.templatetags.static 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.wagtailadmin.forms import WagtailAdminModelForm
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from wagtail.admin.forms import WagtailAdminModelForm
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def user_from_data(user_id):
|
||||
User = get_user_model()
|
||||
try:
|
||||
@ -27,10 +23,8 @@ def user_from_data(user_id):
|
||||
|
||||
|
||||
class SegmentAdminForm(WagtailAdminModelForm):
|
||||
|
||||
def count_matching_users(self, rules, match_any):
|
||||
""" Calculates how many users match the given static rules
|
||||
"""
|
||||
"""Calculates how many users match the given static rules"""
|
||||
count = 0
|
||||
|
||||
static_rules = [rule for rule in rules if rule.static]
|
||||
@ -55,18 +49,28 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
Segment = self._meta.model
|
||||
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
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 (
|
||||
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'])
|
||||
self.add_error_to_fields(self, excluded=["name", "enabled"])
|
||||
|
||||
for formset in self.formsets.values():
|
||||
if formset.has_changed():
|
||||
@ -79,7 +83,7 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
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'))
|
||||
form.add_error(field, _("Cannot update a static segment"))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
@ -87,14 +91,16 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
if not self.instance.is_static:
|
||||
self.instance.count = 0
|
||||
|
||||
if is_new:
|
||||
if is_new and self.instance.is_static and not self.instance.all_rules_static:
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
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)
|
||||
rules, self.instance.match_any
|
||||
)
|
||||
self.instance.matched_count_updated_at = datetime.now()
|
||||
|
||||
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
|
||||
@ -102,29 +108,32 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
if is_new and instance.is_static and instance.all_rules_static:
|
||||
from .adapters import get_segment_adapter
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request = RequestFactory().get("/")
|
||||
request.session = SessionStore()
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
users_to_add = []
|
||||
users_to_exclude = []
|
||||
sessions = Session.objects.iterator()
|
||||
take_session = takewhile(
|
||||
lambda x: instance.count == 0 or len(users_to_add) <= instance.count,
|
||||
sessions
|
||||
)
|
||||
for session in take_session:
|
||||
session_data = session.get_decoded()
|
||||
user = user_from_data(session_data.get('_auth_user_id'))
|
||||
if user.is_authenticated():
|
||||
|
||||
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
|
||||
request.session = SessionStore(session_key=session.session_key)
|
||||
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
|
||||
if passes and instance.randomise_into_segment():
|
||||
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)
|
||||
elif passes:
|
||||
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)
|
||||
|
||||
@ -133,7 +142,5 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
@property
|
||||
def media(self):
|
||||
media = super(SegmentAdminForm, self).media
|
||||
media.add_js(
|
||||
[static('js/segment_form_control.js')]
|
||||
)
|
||||
media.add_js([static("js/segment_form_control.js")])
|
||||
return media
|
||||
|
@ -8,85 +8,192 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0001_initial'),
|
||||
("wagtailcore", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PersonalisablePage',
|
||||
name="PersonalisablePage",
|
||||
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')),
|
||||
('is_segmented', models.BooleanField(default=False)),
|
||||
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='wagtail_personalisation.PersonalisablePage')),
|
||||
(
|
||||
"page_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="wagtailcore.Page",
|
||||
),
|
||||
),
|
||||
("is_segmented", models.BooleanField(default=False)),
|
||||
(
|
||||
"canonical_page",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="variants",
|
||||
to="wagtail_personalisation.PersonalisablePage",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
bases=("wagtailcore.page",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReferralRule',
|
||||
name="ReferralRule",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('regex_string', models.TextField(verbose_name='Regex string to match the referer with')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"regex_string",
|
||||
models.TextField(
|
||||
verbose_name="Regex string to match the referer with"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Segment',
|
||||
name="Segment",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('create_date', models.DateTimeField(auto_now_add=True)),
|
||||
('edit_date', models.DateTimeField(auto_now=True)),
|
||||
('enable_date', models.DateTimeField(editable=False, null=True)),
|
||||
('disable_date', models.DateTimeField(editable=False, null=True)),
|
||||
('visit_count', models.PositiveIntegerField(default=0, editable=False)),
|
||||
('status', models.CharField(choices=[('enabled', 'Enabled'), ('disabled', 'Disabled')], default='enabled', max_length=20)),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("create_date", models.DateTimeField(auto_now_add=True)),
|
||||
("edit_date", models.DateTimeField(auto_now=True)),
|
||||
("enable_date", models.DateTimeField(editable=False, null=True)),
|
||||
("disable_date", models.DateTimeField(editable=False, null=True)),
|
||||
("visit_count", models.PositiveIntegerField(default=0, editable=False)),
|
||||
(
|
||||
"status",
|
||||
models.CharField(
|
||||
choices=[("enabled", "Enabled"), ("disabled", "Disabled")],
|
||||
default="enabled",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TimeRule',
|
||||
name="TimeRule",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('start_time', models.TimeField(verbose_name='Starting time')),
|
||||
('end_time', models.TimeField(verbose_name='Ending time')),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_timerule_related', related_query_name='wagtail_personalisation_timerules', to='wagtail_personalisation.Segment')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("start_time", models.TimeField(verbose_name="Starting time")),
|
||||
("end_time", models.TimeField(verbose_name="Ending time")),
|
||||
(
|
||||
"segment",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wagtail_personalisation_timerule_related",
|
||||
related_query_name="wagtail_personalisation_timerules",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='VisitCountRule',
|
||||
name="VisitCountRule",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('operator', models.CharField(choices=[('more_than', 'More than'), ('less_than', 'Less than'), ('equal_to', 'Equal to')], default='ht', max_length=20)),
|
||||
('count', models.PositiveSmallIntegerField(default=0, null=True)),
|
||||
('counted_page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page')),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_visitcountrule_related', related_query_name='wagtail_personalisation_visitcountrules', to='wagtail_personalisation.Segment')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"operator",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("more_than", "More than"),
|
||||
("less_than", "Less than"),
|
||||
("equal_to", "Equal to"),
|
||||
],
|
||||
default="ht",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
("count", models.PositiveSmallIntegerField(default=0, null=True)),
|
||||
(
|
||||
"counted_page",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="+",
|
||||
to="wagtailcore.Page",
|
||||
),
|
||||
),
|
||||
(
|
||||
"segment",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wagtail_personalisation_visitcountrule_related",
|
||||
related_query_name="wagtail_personalisation_visitcountrules",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='referralrule',
|
||||
name='segment',
|
||||
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_referralrule_related', related_query_name='wagtail_personalisation_referralrules', to='wagtail_personalisation.Segment'),
|
||||
model_name="referralrule",
|
||||
name="segment",
|
||||
field=modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wagtail_personalisation_referralrule_related",
|
||||
related_query_name="wagtail_personalisation_referralrules",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='personalisablepage',
|
||||
name='segment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='segments', to='wagtail_personalisation.Segment'),
|
||||
model_name="personalisablepage",
|
||||
name="segment",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="segments",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -8,27 +8,58 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0001_initial'),
|
||||
("wagtail_personalisation", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='QueryRule',
|
||||
name="QueryRule",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('query_parameter', models.TextField(verbose_name='The query parameter to search for')),
|
||||
('query_value', models.TextField(verbose_name='The value of the parameter to match')),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_queryrule_related', related_query_name='wagtail_personalisation_queryrules', to='wagtail_personalisation.Segment')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"query_parameter",
|
||||
models.TextField(verbose_name="The query parameter to search for"),
|
||||
),
|
||||
(
|
||||
"query_value",
|
||||
models.TextField(
|
||||
verbose_name="The value of the parameter to match"
|
||||
),
|
||||
),
|
||||
(
|
||||
"segment",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wagtail_personalisation_queryrule_related",
|
||||
related_query_name="wagtail_personalisation_queryrules",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='visitcountrule',
|
||||
name='operator',
|
||||
field=models.CharField(choices=[('more_than', 'More than'), ('less_than', 'Less than'), ('equal_to', 'Equal to')], default='more_than', max_length=20),
|
||||
model_name="visitcountrule",
|
||||
name="operator",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("more_than", "More than"),
|
||||
("less_than", "Less than"),
|
||||
("equal_to", "Equal to"),
|
||||
],
|
||||
default="more_than",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -6,30 +6,37 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0002_auto_20161205_1623'),
|
||||
("wagtail_personalisation", "0002_auto_20161205_1623"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='queryrule',
|
||||
name='query_parameter',
|
||||
model_name="queryrule",
|
||||
name="query_parameter",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='queryrule',
|
||||
name='query_value',
|
||||
model_name="queryrule",
|
||||
name="query_value",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='queryrule',
|
||||
name='parameter',
|
||||
field=models.SlugField(default='test', max_length=20, verbose_name='The query parameter to search for'),
|
||||
model_name="queryrule",
|
||||
name="parameter",
|
||||
field=models.SlugField(
|
||||
default="test",
|
||||
max_length=20,
|
||||
verbose_name="The query parameter to search for",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='queryrule',
|
||||
name='value',
|
||||
field=models.SlugField(default='test', max_length=20, verbose_name='The value of the parameter to match'),
|
||||
model_name="queryrule",
|
||||
name="value",
|
||||
field=models.SlugField(
|
||||
default="test",
|
||||
max_length=20,
|
||||
verbose_name="The value of the parameter to match",
|
||||
),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
@ -6,15 +6,16 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0003_auto_20161206_1005'),
|
||||
("wagtail_personalisation", "0003_auto_20161206_1005"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='persistent',
|
||||
field=models.BooleanField(default=False, help_text='Should the segment persist between visits?'),
|
||||
model_name="segment",
|
||||
name="persistent",
|
||||
field=models.BooleanField(
|
||||
default=False, help_text="Should the segment persist between visits?"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -8,21 +8,36 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0004_segment_persistent'),
|
||||
("wagtail_personalisation", "0004_segment_persistent"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserIsLoggedInRule',
|
||||
name="UserIsLoggedInRule",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_logged_in', models.BooleanField(default=False)),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_userisloggedinrule_related', related_query_name='wagtail_personalisation_userisloggedinrules', to='wagtail_personalisation.Segment')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("is_logged_in", models.BooleanField(default=False)),
|
||||
(
|
||||
"segment",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wagtail_personalisation_userisloggedinrule_related",
|
||||
related_query_name="wagtail_personalisation_userisloggedinrules",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -6,15 +6,17 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0005_userisloggedinrule'),
|
||||
("wagtail_personalisation", "0005_userisloggedinrule"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='match_any',
|
||||
field=models.BooleanField(default=False, help_text='Should the segment match all the rules or just one of them?'),
|
||||
model_name="segment",
|
||||
name="match_any",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Should the segment match all the rules or just one of them?",
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -8,27 +8,42 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0006_segment_match_any'),
|
||||
("wagtail_personalisation", "0006_segment_match_any"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DayRule',
|
||||
name="DayRule",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mon', models.BooleanField(default=False, verbose_name='Monday')),
|
||||
('tue', models.BooleanField(default=False, verbose_name='Tuesday')),
|
||||
('wed', models.BooleanField(default=False, verbose_name='Wednesday')),
|
||||
('thu', models.BooleanField(default=False, verbose_name='Thursday')),
|
||||
('fri', models.BooleanField(default=False, verbose_name='Friday')),
|
||||
('sat', models.BooleanField(default=False, verbose_name='Saturday')),
|
||||
('sun', models.BooleanField(default=False, verbose_name='Sunday')),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_dayrule_related', related_query_name='wagtail_personalisation_dayrules', to='wagtail_personalisation.Segment')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("mon", models.BooleanField(default=False, verbose_name="Monday")),
|
||||
("tue", models.BooleanField(default=False, verbose_name="Tuesday")),
|
||||
("wed", models.BooleanField(default=False, verbose_name="Wednesday")),
|
||||
("thu", models.BooleanField(default=False, verbose_name="Thursday")),
|
||||
("fri", models.BooleanField(default=False, verbose_name="Friday")),
|
||||
("sat", models.BooleanField(default=False, verbose_name="Saturday")),
|
||||
("sun", models.BooleanField(default=False, verbose_name="Sunday")),
|
||||
(
|
||||
"segment",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wagtail_personalisation_dayrule_related",
|
||||
related_query_name="wagtail_personalisation_dayrules",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -8,23 +8,41 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0007_dayrule'),
|
||||
("wagtail_personalisation", "0007_dayrule"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DeviceRule',
|
||||
name="DeviceRule",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mobile', models.BooleanField(default=False, verbose_name='Mobile phone')),
|
||||
('tablet', models.BooleanField(default=False, verbose_name='Tablet')),
|
||||
('desktop', models.BooleanField(default=False, verbose_name='Desktop')),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_devicerule_related', related_query_name='wagtail_personalisation_devicerules', to='wagtail_personalisation.Segment')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"mobile",
|
||||
models.BooleanField(default=False, verbose_name="Mobile phone"),
|
||||
),
|
||||
("tablet", models.BooleanField(default=False, verbose_name="Tablet")),
|
||||
("desktop", models.BooleanField(default=False, verbose_name="Desktop")),
|
||||
(
|
||||
"segment",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wagtail_personalisation_devicerule_related",
|
||||
related_query_name="wagtail_personalisation_devicerules",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -6,25 +6,24 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0008_devicerule'),
|
||||
("wagtail_personalisation", "0008_devicerule"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='personalisablepage',
|
||||
name='canonical_page',
|
||||
model_name="personalisablepage",
|
||||
name="canonical_page",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='personalisablepage',
|
||||
name='page_ptr',
|
||||
model_name="personalisablepage",
|
||||
name="page_ptr",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='personalisablepage',
|
||||
name='segment',
|
||||
model_name="personalisablepage",
|
||||
name="segment",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PersonalisablePage',
|
||||
name="PersonalisablePage",
|
||||
),
|
||||
]
|
||||
|
@ -6,43 +6,44 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0009_auto_20170531_0428'),
|
||||
("wagtail_personalisation", "0009_auto_20170531_0428"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='dayrule',
|
||||
options={'verbose_name': 'Day Rule'},
|
||||
name="dayrule",
|
||||
options={"verbose_name": "Day Rule"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='devicerule',
|
||||
options={'verbose_name': 'Device Rule'},
|
||||
name="devicerule",
|
||||
options={"verbose_name": "Device Rule"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='queryrule',
|
||||
options={'verbose_name': 'Query Rule'},
|
||||
name="queryrule",
|
||||
options={"verbose_name": "Query Rule"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='referralrule',
|
||||
options={'verbose_name': 'Referral Rule'},
|
||||
name="referralrule",
|
||||
options={"verbose_name": "Referral Rule"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='timerule',
|
||||
options={'verbose_name': 'Time Rule'},
|
||||
name="timerule",
|
||||
options={"verbose_name": "Time Rule"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='userisloggedinrule',
|
||||
options={'verbose_name': 'Logged in Rule'},
|
||||
name="userisloggedinrule",
|
||||
options={"verbose_name": "Logged in Rule"},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='visitcountrule',
|
||||
options={'verbose_name': 'Visit count Rule'},
|
||||
name="visitcountrule",
|
||||
options={"verbose_name": "Visit count Rule"},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='referralrule',
|
||||
name='regex_string',
|
||||
field=models.TextField(verbose_name='Regular expression to match the referrer'),
|
||||
model_name="referralrule",
|
||||
name="regex_string",
|
||||
field=models.TextField(
|
||||
verbose_name="Regular expression to match the referrer"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -7,24 +7,56 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0001_initial'),
|
||||
('wagtail_personalisation', '0010_auto_20170531_1101'),
|
||||
("wagtailcore", "0001_initial"),
|
||||
("wagtail_personalisation", "0010_auto_20170531_1101"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PersonalisablePageMetadata',
|
||||
name="PersonalisablePageMetadata",
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_segmented', models.BooleanField(default=False)),
|
||||
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='personalisable_canonical_metadata', to='wagtailcore.Page')),
|
||||
('segment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='page_metadata', to='wagtail_personalisation.Segment')),
|
||||
('variant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page')),
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("is_segmented", models.BooleanField(default=False)),
|
||||
(
|
||||
"canonical_page",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="personalisable_canonical_metadata",
|
||||
to="wagtailcore.Page",
|
||||
),
|
||||
),
|
||||
(
|
||||
"segment",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="page_metadata",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
(
|
||||
"variant",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="_personalisable_page_metadata",
|
||||
to="wagtailcore.Page",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
@ -6,14 +6,13 @@ from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0011_personalisablepagemetadata'),
|
||||
("wagtail_personalisation", "0011_personalisablepagemetadata"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='personalisablepagemetadata',
|
||||
name='is_segmented',
|
||||
model_name="personalisablepagemetadata",
|
||||
name="is_segmented",
|
||||
),
|
||||
]
|
||||
|
@ -6,26 +6,36 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sessions', '0001_initial'),
|
||||
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
|
||||
("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.'),
|
||||
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'),
|
||||
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),
|
||||
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,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -7,20 +7,19 @@ 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'),
|
||||
("wagtail_personalisation", "0013_add_dynamic_static_to_segment"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
model_name="segment",
|
||||
name="sessions",
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='static_users',
|
||||
model_name="segment",
|
||||
name="static_users",
|
||||
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
|
@ -6,20 +6,19 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0015_static_users'),
|
||||
("wagtail_personalisation", "0015_static_users"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='matched_count_updated_at',
|
||||
model_name="segment",
|
||||
name="matched_count_updated_at",
|
||||
field=models.DateTimeField(editable=False, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='matched_users_count',
|
||||
model_name="segment",
|
||||
name="matched_users_count",
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
),
|
||||
]
|
||||
|
@ -7,15 +7,23 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0016_auto_20180125_0918'),
|
||||
("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)]),
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -7,16 +7,19 @@ from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0017_segment_randomisation_percent'),
|
||||
("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),
|
||||
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,24 @@
|
||||
# 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,77 @@
|
||||
# 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,23 @@
|
||||
# Generated by Django 2.0.7 on 2018-07-04 15:26
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
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,26 @@
|
||||
# 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,26 @@
|
||||
# Generated by Django 2.0.5 on 2018-07-19 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
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",
|
||||
),
|
||||
),
|
||||
]
|
297
src/wagtail_personalisation/migrations/0024_origincountryrule.py
Normal file
@ -0,0 +1,297 @@
|
||||
# Generated by Django 2.0.6 on 2018-08-10 14:39
|
||||
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("wagtail_personalisation", "0023_personalisablepagemetadata_variant_cascade"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="OriginCountryRule",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"country",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("aw", "Aruba"),
|
||||
("af", "Afghanistan"),
|
||||
("ao", "Angola"),
|
||||
("ai", "Anguilla"),
|
||||
("ax", "Åland Islands"),
|
||||
("al", "Albania"),
|
||||
("ad", "Andorra"),
|
||||
("ae", "United Arab Emirates"),
|
||||
("ar", "Argentina"),
|
||||
("am", "Armenia"),
|
||||
("as", "American Samoa"),
|
||||
("aq", "Antarctica"),
|
||||
("tf", "French Southern Territories"),
|
||||
("ag", "Antigua and Barbuda"),
|
||||
("au", "Australia"),
|
||||
("at", "Austria"),
|
||||
("az", "Azerbaijan"),
|
||||
("bi", "Burundi"),
|
||||
("be", "Belgium"),
|
||||
("bj", "Benin"),
|
||||
("bq", "Bonaire, Sint Eustatius and Saba"),
|
||||
("bf", "Burkina Faso"),
|
||||
("bd", "Bangladesh"),
|
||||
("bg", "Bulgaria"),
|
||||
("bh", "Bahrain"),
|
||||
("bs", "Bahamas"),
|
||||
("ba", "Bosnia and Herzegovina"),
|
||||
("bl", "Saint Barthélemy"),
|
||||
("by", "Belarus"),
|
||||
("bz", "Belize"),
|
||||
("bm", "Bermuda"),
|
||||
("bo", "Bolivia, Plurinational State of"),
|
||||
("br", "Brazil"),
|
||||
("bb", "Barbados"),
|
||||
("bn", "Brunei Darussalam"),
|
||||
("bt", "Bhutan"),
|
||||
("bv", "Bouvet Island"),
|
||||
("bw", "Botswana"),
|
||||
("cf", "Central African Republic"),
|
||||
("ca", "Canada"),
|
||||
("cc", "Cocos (Keeling) Islands"),
|
||||
("ch", "Switzerland"),
|
||||
("cl", "Chile"),
|
||||
("cn", "China"),
|
||||
("ci", "Côte d'Ivoire"),
|
||||
("cm", "Cameroon"),
|
||||
("cd", "Congo, The Democratic Republic of the"),
|
||||
("cg", "Congo"),
|
||||
("ck", "Cook Islands"),
|
||||
("co", "Colombia"),
|
||||
("km", "Comoros"),
|
||||
("cv", "Cabo Verde"),
|
||||
("cr", "Costa Rica"),
|
||||
("cu", "Cuba"),
|
||||
("cw", "Curaçao"),
|
||||
("cx", "Christmas Island"),
|
||||
("ky", "Cayman Islands"),
|
||||
("cy", "Cyprus"),
|
||||
("cz", "Czechia"),
|
||||
("de", "Germany"),
|
||||
("dj", "Djibouti"),
|
||||
("dm", "Dominica"),
|
||||
("dk", "Denmark"),
|
||||
("do", "Dominican Republic"),
|
||||
("dz", "Algeria"),
|
||||
("ec", "Ecuador"),
|
||||
("eg", "Egypt"),
|
||||
("er", "Eritrea"),
|
||||
("eh", "Western Sahara"),
|
||||
("es", "Spain"),
|
||||
("ee", "Estonia"),
|
||||
("et", "Ethiopia"),
|
||||
("fi", "Finland"),
|
||||
("fj", "Fiji"),
|
||||
("fk", "Falkland Islands (Malvinas)"),
|
||||
("fr", "France"),
|
||||
("fo", "Faroe Islands"),
|
||||
("fm", "Micronesia, Federated States of"),
|
||||
("ga", "Gabon"),
|
||||
("gb", "United Kingdom"),
|
||||
("ge", "Georgia"),
|
||||
("gg", "Guernsey"),
|
||||
("gh", "Ghana"),
|
||||
("gi", "Gibraltar"),
|
||||
("gn", "Guinea"),
|
||||
("gp", "Guadeloupe"),
|
||||
("gm", "Gambia"),
|
||||
("gw", "Guinea-Bissau"),
|
||||
("gq", "Equatorial Guinea"),
|
||||
("gr", "Greece"),
|
||||
("gd", "Grenada"),
|
||||
("gl", "Greenland"),
|
||||
("gt", "Guatemala"),
|
||||
("gf", "French Guiana"),
|
||||
("gu", "Guam"),
|
||||
("gy", "Guyana"),
|
||||
("hk", "Hong Kong"),
|
||||
("hm", "Heard Island and McDonald Islands"),
|
||||
("hn", "Honduras"),
|
||||
("hr", "Croatia"),
|
||||
("ht", "Haiti"),
|
||||
("hu", "Hungary"),
|
||||
("id", "Indonesia"),
|
||||
("im", "Isle of Man"),
|
||||
("in", "India"),
|
||||
("io", "British Indian Ocean Territory"),
|
||||
("ie", "Ireland"),
|
||||
("ir", "Iran, Islamic Republic of"),
|
||||
("iq", "Iraq"),
|
||||
("is", "Iceland"),
|
||||
("il", "Israel"),
|
||||
("it", "Italy"),
|
||||
("jm", "Jamaica"),
|
||||
("je", "Jersey"),
|
||||
("jo", "Jordan"),
|
||||
("jp", "Japan"),
|
||||
("kz", "Kazakhstan"),
|
||||
("ke", "Kenya"),
|
||||
("kg", "Kyrgyzstan"),
|
||||
("kh", "Cambodia"),
|
||||
("ki", "Kiribati"),
|
||||
("kn", "Saint Kitts and Nevis"),
|
||||
("kr", "Korea, Republic of"),
|
||||
("kw", "Kuwait"),
|
||||
("la", "Lao People's Democratic Republic"),
|
||||
("lb", "Lebanon"),
|
||||
("lr", "Liberia"),
|
||||
("ly", "Libya"),
|
||||
("lc", "Saint Lucia"),
|
||||
("li", "Liechtenstein"),
|
||||
("lk", "Sri Lanka"),
|
||||
("ls", "Lesotho"),
|
||||
("lt", "Lithuania"),
|
||||
("lu", "Luxembourg"),
|
||||
("lv", "Latvia"),
|
||||
("mo", "Macao"),
|
||||
("mf", "Saint Martin (French part)"),
|
||||
("ma", "Morocco"),
|
||||
("mc", "Monaco"),
|
||||
("md", "Moldova, Republic of"),
|
||||
("mg", "Madagascar"),
|
||||
("mv", "Maldives"),
|
||||
("mx", "Mexico"),
|
||||
("mh", "Marshall Islands"),
|
||||
("mk", "Macedonia, Republic of"),
|
||||
("ml", "Mali"),
|
||||
("mt", "Malta"),
|
||||
("mm", "Myanmar"),
|
||||
("me", "Montenegro"),
|
||||
("mn", "Mongolia"),
|
||||
("mp", "Northern Mariana Islands"),
|
||||
("mz", "Mozambique"),
|
||||
("mr", "Mauritania"),
|
||||
("ms", "Montserrat"),
|
||||
("mq", "Martinique"),
|
||||
("mu", "Mauritius"),
|
||||
("mw", "Malawi"),
|
||||
("my", "Malaysia"),
|
||||
("yt", "Mayotte"),
|
||||
("na", "Namibia"),
|
||||
("nc", "New Caledonia"),
|
||||
("ne", "Niger"),
|
||||
("nf", "Norfolk Island"),
|
||||
("ng", "Nigeria"),
|
||||
("ni", "Nicaragua"),
|
||||
("nu", "Niue"),
|
||||
("nl", "Netherlands"),
|
||||
("no", "Norway"),
|
||||
("np", "Nepal"),
|
||||
("nr", "Nauru"),
|
||||
("nz", "New Zealand"),
|
||||
("om", "Oman"),
|
||||
("pk", "Pakistan"),
|
||||
("pa", "Panama"),
|
||||
("pn", "Pitcairn"),
|
||||
("pe", "Peru"),
|
||||
("ph", "Philippines"),
|
||||
("pw", "Palau"),
|
||||
("pg", "Papua New Guinea"),
|
||||
("pl", "Poland"),
|
||||
("pr", "Puerto Rico"),
|
||||
("kp", "Korea, Democratic People's Republic of"),
|
||||
("pt", "Portugal"),
|
||||
("py", "Paraguay"),
|
||||
("ps", "Palestine, State of"),
|
||||
("pf", "French Polynesia"),
|
||||
("qa", "Qatar"),
|
||||
("re", "Réunion"),
|
||||
("ro", "Romania"),
|
||||
("ru", "Russian Federation"),
|
||||
("rw", "Rwanda"),
|
||||
("sa", "Saudi Arabia"),
|
||||
("sd", "Sudan"),
|
||||
("sn", "Senegal"),
|
||||
("sg", "Singapore"),
|
||||
("gs", "South Georgia and the South Sandwich Islands"),
|
||||
("sh", "Saint Helena, Ascension and Tristan da Cunha"),
|
||||
("sj", "Svalbard and Jan Mayen"),
|
||||
("sb", "Solomon Islands"),
|
||||
("sl", "Sierra Leone"),
|
||||
("sv", "El Salvador"),
|
||||
("sm", "San Marino"),
|
||||
("so", "Somalia"),
|
||||
("pm", "Saint Pierre and Miquelon"),
|
||||
("rs", "Serbia"),
|
||||
("ss", "South Sudan"),
|
||||
("st", "Sao Tome and Principe"),
|
||||
("sr", "Suriname"),
|
||||
("sk", "Slovakia"),
|
||||
("si", "Slovenia"),
|
||||
("se", "Sweden"),
|
||||
("sz", "Swaziland"),
|
||||
("sx", "Sint Maarten (Dutch part)"),
|
||||
("sc", "Seychelles"),
|
||||
("sy", "Syrian Arab Republic"),
|
||||
("tc", "Turks and Caicos Islands"),
|
||||
("td", "Chad"),
|
||||
("tg", "Togo"),
|
||||
("th", "Thailand"),
|
||||
("tj", "Tajikistan"),
|
||||
("tk", "Tokelau"),
|
||||
("tm", "Turkmenistan"),
|
||||
("tl", "Timor-Leste"),
|
||||
("to", "Tonga"),
|
||||
("tt", "Trinidad and Tobago"),
|
||||
("tn", "Tunisia"),
|
||||
("tr", "Turkey"),
|
||||
("tv", "Tuvalu"),
|
||||
("tw", "Taiwan, Province of China"),
|
||||
("tz", "Tanzania, United Republic of"),
|
||||
("ug", "Uganda"),
|
||||
("ua", "Ukraine"),
|
||||
("um", "United States Minor Outlying Islands"),
|
||||
("uy", "Uruguay"),
|
||||
("us", "United States"),
|
||||
("uz", "Uzbekistan"),
|
||||
("va", "Holy See (Vatican City State)"),
|
||||
("vc", "Saint Vincent and the Grenadines"),
|
||||
("ve", "Venezuela, Bolivarian Republic of"),
|
||||
("vg", "Virgin Islands, British"),
|
||||
("vi", "Virgin Islands, U.S."),
|
||||
("vn", "Viet Nam"),
|
||||
("vu", "Vanuatu"),
|
||||
("wf", "Wallis and Futuna"),
|
||||
("ws", "Samoa"),
|
||||
("ye", "Yemen"),
|
||||
("za", "South Africa"),
|
||||
("zm", "Zambia"),
|
||||
("zw", "Zimbabwe"),
|
||||
],
|
||||
help_text="Select origin country of the request that this rule will match against. This rule will only work if you use Cloudflare or CloudFront IP geolocation or if GeoIP2 module is configured.",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
(
|
||||
"segment",
|
||||
modelcluster.fields.ParentalKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="wagtail_personalisation_origincountryrules",
|
||||
to="wagtail_personalisation.Segment",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "origin country rule",
|
||||
},
|
||||
),
|
||||
]
|
@ -0,0 +1,271 @@
|
||||
# Generated by Django 2.1.11 on 2019-08-22 06:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("wagtail_personalisation", "0024_origincountryrule"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="origincountryrule",
|
||||
name="country",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("aw", "Aruba"),
|
||||
("af", "Afghanistan"),
|
||||
("ao", "Angola"),
|
||||
("ai", "Anguilla"),
|
||||
("ax", "Åland Islands"),
|
||||
("al", "Albania"),
|
||||
("ad", "Andorra"),
|
||||
("ae", "United Arab Emirates"),
|
||||
("ar", "Argentina"),
|
||||
("am", "Armenia"),
|
||||
("as", "American Samoa"),
|
||||
("aq", "Antarctica"),
|
||||
("tf", "French Southern Territories"),
|
||||
("ag", "Antigua and Barbuda"),
|
||||
("au", "Australia"),
|
||||
("at", "Austria"),
|
||||
("az", "Azerbaijan"),
|
||||
("bi", "Burundi"),
|
||||
("be", "Belgium"),
|
||||
("bj", "Benin"),
|
||||
("bq", "Bonaire, Sint Eustatius and Saba"),
|
||||
("bf", "Burkina Faso"),
|
||||
("bd", "Bangladesh"),
|
||||
("bg", "Bulgaria"),
|
||||
("bh", "Bahrain"),
|
||||
("bs", "Bahamas"),
|
||||
("ba", "Bosnia and Herzegovina"),
|
||||
("bl", "Saint Barthélemy"),
|
||||
("by", "Belarus"),
|
||||
("bz", "Belize"),
|
||||
("bm", "Bermuda"),
|
||||
("bo", "Bolivia, Plurinational State of"),
|
||||
("br", "Brazil"),
|
||||
("bb", "Barbados"),
|
||||
("bn", "Brunei Darussalam"),
|
||||
("bt", "Bhutan"),
|
||||
("bv", "Bouvet Island"),
|
||||
("bw", "Botswana"),
|
||||
("cf", "Central African Republic"),
|
||||
("ca", "Canada"),
|
||||
("cc", "Cocos (Keeling) Islands"),
|
||||
("ch", "Switzerland"),
|
||||
("cl", "Chile"),
|
||||
("cn", "China"),
|
||||
("ci", "Côte d'Ivoire"),
|
||||
("cm", "Cameroon"),
|
||||
("cd", "Congo, The Democratic Republic of the"),
|
||||
("cg", "Congo"),
|
||||
("ck", "Cook Islands"),
|
||||
("co", "Colombia"),
|
||||
("km", "Comoros"),
|
||||
("cv", "Cabo Verde"),
|
||||
("cr", "Costa Rica"),
|
||||
("cu", "Cuba"),
|
||||
("cw", "Curaçao"),
|
||||
("cx", "Christmas Island"),
|
||||
("ky", "Cayman Islands"),
|
||||
("cy", "Cyprus"),
|
||||
("cz", "Czechia"),
|
||||
("de", "Germany"),
|
||||
("dj", "Djibouti"),
|
||||
("dm", "Dominica"),
|
||||
("dk", "Denmark"),
|
||||
("do", "Dominican Republic"),
|
||||
("dz", "Algeria"),
|
||||
("ec", "Ecuador"),
|
||||
("eg", "Egypt"),
|
||||
("er", "Eritrea"),
|
||||
("eh", "Western Sahara"),
|
||||
("es", "Spain"),
|
||||
("ee", "Estonia"),
|
||||
("et", "Ethiopia"),
|
||||
("fi", "Finland"),
|
||||
("fj", "Fiji"),
|
||||
("fk", "Falkland Islands (Malvinas)"),
|
||||
("fr", "France"),
|
||||
("fo", "Faroe Islands"),
|
||||
("fm", "Micronesia, Federated States of"),
|
||||
("ga", "Gabon"),
|
||||
("gb", "United Kingdom"),
|
||||
("ge", "Georgia"),
|
||||
("gg", "Guernsey"),
|
||||
("gh", "Ghana"),
|
||||
("gi", "Gibraltar"),
|
||||
("gn", "Guinea"),
|
||||
("gp", "Guadeloupe"),
|
||||
("gm", "Gambia"),
|
||||
("gw", "Guinea-Bissau"),
|
||||
("gq", "Equatorial Guinea"),
|
||||
("gr", "Greece"),
|
||||
("gd", "Grenada"),
|
||||
("gl", "Greenland"),
|
||||
("gt", "Guatemala"),
|
||||
("gf", "French Guiana"),
|
||||
("gu", "Guam"),
|
||||
("gy", "Guyana"),
|
||||
("hk", "Hong Kong"),
|
||||
("hm", "Heard Island and McDonald Islands"),
|
||||
("hn", "Honduras"),
|
||||
("hr", "Croatia"),
|
||||
("ht", "Haiti"),
|
||||
("hu", "Hungary"),
|
||||
("id", "Indonesia"),
|
||||
("im", "Isle of Man"),
|
||||
("in", "India"),
|
||||
("io", "British Indian Ocean Territory"),
|
||||
("ie", "Ireland"),
|
||||
("ir", "Iran, Islamic Republic of"),
|
||||
("iq", "Iraq"),
|
||||
("is", "Iceland"),
|
||||
("il", "Israel"),
|
||||
("it", "Italy"),
|
||||
("jm", "Jamaica"),
|
||||
("je", "Jersey"),
|
||||
("jo", "Jordan"),
|
||||
("jp", "Japan"),
|
||||
("kz", "Kazakhstan"),
|
||||
("ke", "Kenya"),
|
||||
("kg", "Kyrgyzstan"),
|
||||
("kh", "Cambodia"),
|
||||
("ki", "Kiribati"),
|
||||
("kn", "Saint Kitts and Nevis"),
|
||||
("kr", "Korea, Republic of"),
|
||||
("kw", "Kuwait"),
|
||||
("la", "Lao People's Democratic Republic"),
|
||||
("lb", "Lebanon"),
|
||||
("lr", "Liberia"),
|
||||
("ly", "Libya"),
|
||||
("lc", "Saint Lucia"),
|
||||
("li", "Liechtenstein"),
|
||||
("lk", "Sri Lanka"),
|
||||
("ls", "Lesotho"),
|
||||
("lt", "Lithuania"),
|
||||
("lu", "Luxembourg"),
|
||||
("lv", "Latvia"),
|
||||
("mo", "Macao"),
|
||||
("mf", "Saint Martin (French part)"),
|
||||
("ma", "Morocco"),
|
||||
("mc", "Monaco"),
|
||||
("md", "Moldova, Republic of"),
|
||||
("mg", "Madagascar"),
|
||||
("mv", "Maldives"),
|
||||
("mx", "Mexico"),
|
||||
("mh", "Marshall Islands"),
|
||||
("mk", "North Macedonia"),
|
||||
("ml", "Mali"),
|
||||
("mt", "Malta"),
|
||||
("mm", "Myanmar"),
|
||||
("me", "Montenegro"),
|
||||
("mn", "Mongolia"),
|
||||
("mp", "Northern Mariana Islands"),
|
||||
("mz", "Mozambique"),
|
||||
("mr", "Mauritania"),
|
||||
("ms", "Montserrat"),
|
||||
("mq", "Martinique"),
|
||||
("mu", "Mauritius"),
|
||||
("mw", "Malawi"),
|
||||
("my", "Malaysia"),
|
||||
("yt", "Mayotte"),
|
||||
("na", "Namibia"),
|
||||
("nc", "New Caledonia"),
|
||||
("ne", "Niger"),
|
||||
("nf", "Norfolk Island"),
|
||||
("ng", "Nigeria"),
|
||||
("ni", "Nicaragua"),
|
||||
("nu", "Niue"),
|
||||
("nl", "Netherlands"),
|
||||
("no", "Norway"),
|
||||
("np", "Nepal"),
|
||||
("nr", "Nauru"),
|
||||
("nz", "New Zealand"),
|
||||
("om", "Oman"),
|
||||
("pk", "Pakistan"),
|
||||
("pa", "Panama"),
|
||||
("pn", "Pitcairn"),
|
||||
("pe", "Peru"),
|
||||
("ph", "Philippines"),
|
||||
("pw", "Palau"),
|
||||
("pg", "Papua New Guinea"),
|
||||
("pl", "Poland"),
|
||||
("pr", "Puerto Rico"),
|
||||
("kp", "Korea, Democratic People's Republic of"),
|
||||
("pt", "Portugal"),
|
||||
("py", "Paraguay"),
|
||||
("ps", "Palestine, State of"),
|
||||
("pf", "French Polynesia"),
|
||||
("qa", "Qatar"),
|
||||
("re", "Réunion"),
|
||||
("ro", "Romania"),
|
||||
("ru", "Russian Federation"),
|
||||
("rw", "Rwanda"),
|
||||
("sa", "Saudi Arabia"),
|
||||
("sd", "Sudan"),
|
||||
("sn", "Senegal"),
|
||||
("sg", "Singapore"),
|
||||
("gs", "South Georgia and the South Sandwich Islands"),
|
||||
("sh", "Saint Helena, Ascension and Tristan da Cunha"),
|
||||
("sj", "Svalbard and Jan Mayen"),
|
||||
("sb", "Solomon Islands"),
|
||||
("sl", "Sierra Leone"),
|
||||
("sv", "El Salvador"),
|
||||
("sm", "San Marino"),
|
||||
("so", "Somalia"),
|
||||
("pm", "Saint Pierre and Miquelon"),
|
||||
("rs", "Serbia"),
|
||||
("ss", "South Sudan"),
|
||||
("st", "Sao Tome and Principe"),
|
||||
("sr", "Suriname"),
|
||||
("sk", "Slovakia"),
|
||||
("si", "Slovenia"),
|
||||
("se", "Sweden"),
|
||||
("sz", "Eswatini"),
|
||||
("sx", "Sint Maarten (Dutch part)"),
|
||||
("sc", "Seychelles"),
|
||||
("sy", "Syrian Arab Republic"),
|
||||
("tc", "Turks and Caicos Islands"),
|
||||
("td", "Chad"),
|
||||
("tg", "Togo"),
|
||||
("th", "Thailand"),
|
||||
("tj", "Tajikistan"),
|
||||
("tk", "Tokelau"),
|
||||
("tm", "Turkmenistan"),
|
||||
("tl", "Timor-Leste"),
|
||||
("to", "Tonga"),
|
||||
("tt", "Trinidad and Tobago"),
|
||||
("tn", "Tunisia"),
|
||||
("tr", "Turkey"),
|
||||
("tv", "Tuvalu"),
|
||||
("tw", "Taiwan, Province of China"),
|
||||
("tz", "Tanzania, United Republic of"),
|
||||
("ug", "Uganda"),
|
||||
("ua", "Ukraine"),
|
||||
("um", "United States Minor Outlying Islands"),
|
||||
("uy", "Uruguay"),
|
||||
("us", "United States"),
|
||||
("uz", "Uzbekistan"),
|
||||
("va", "Holy See (Vatican City State)"),
|
||||
("vc", "Saint Vincent and the Grenadines"),
|
||||
("ve", "Venezuela, Bolivarian Republic of"),
|
||||
("vg", "Virgin Islands, British"),
|
||||
("vi", "Virgin Islands, U.S."),
|
||||
("vn", "Viet Nam"),
|
||||
("vu", "Vanuatu"),
|
||||
("wf", "Wallis and Futuna"),
|
||||
("ws", "Samoa"),
|
||||
("ye", "Yemen"),
|
||||
("za", "South Africa"),
|
||||
("zm", "Zambia"),
|
||||
("zw", "Zimbabwe"),
|
||||
],
|
||||
help_text="Select origin country of the request that this rule will match against. This rule will only work if you use Cloudflare or CloudFront IP geolocation or if GeoIP2 module is configured.",
|
||||
max_length=2,
|
||||
),
|
||||
),
|
||||
]
|
@ -1,19 +1,17 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import random
|
||||
|
||||
import wagtail
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from modelcluster.models import ClusterableModel
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.admin.panels import FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel
|
||||
from wagtail.models import Page
|
||||
|
||||
from wagtail_personalisation.rules import AbstractBaseRule
|
||||
from wagtail_personalisation.utils import count_active_days
|
||||
@ -21,28 +19,36 @@ from wagtail_personalisation.utils import count_active_days
|
||||
from .forms import SegmentAdminForm
|
||||
|
||||
|
||||
class RulePanel(InlinePanel):
|
||||
def on_model_bound(self):
|
||||
self.relation_name = self.relation_name.replace("_related", "s")
|
||||
self.db_field = self.model._meta.get_field(self.relation_name)
|
||||
manager = getattr(self.model, self.relation_name)
|
||||
self.related = manager.rel
|
||||
|
||||
|
||||
class SegmentQuerySet(models.QuerySet):
|
||||
def enabled(self):
|
||||
return self.filter(status=self.model.STATUS_ENABLED)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Segment(ClusterableModel):
|
||||
"""The segment model."""
|
||||
STATUS_ENABLED = 'enabled'
|
||||
STATUS_DISABLED = 'disabled'
|
||||
|
||||
STATUS_ENABLED = "enabled"
|
||||
STATUS_DISABLED = "disabled"
|
||||
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_ENABLED, _('Enabled')),
|
||||
(STATUS_DISABLED, _('Disabled')),
|
||||
(STATUS_ENABLED, _("Enabled")),
|
||||
(STATUS_DISABLED, _("Disabled")),
|
||||
)
|
||||
|
||||
TYPE_DYNAMIC = 'dynamic'
|
||||
TYPE_STATIC = 'static'
|
||||
TYPE_DYNAMIC = "dynamic"
|
||||
TYPE_STATIC = "static"
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_DYNAMIC, _('Dynamic')),
|
||||
(TYPE_STATIC, _('Static')),
|
||||
(TYPE_DYNAMIC, _("Dynamic")),
|
||||
(TYPE_STATIC, _("Static")),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
@ -52,18 +58,22 @@ class Segment(ClusterableModel):
|
||||
disable_date = models.DateTimeField(null=True, editable=False)
|
||||
visit_count = models.PositiveIntegerField(default=0, editable=False)
|
||||
status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED)
|
||||
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED
|
||||
)
|
||||
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(
|
||||
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(_("""
|
||||
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
|
||||
@ -71,37 +81,42 @@ class Segment(ClusterableModel):
|
||||
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"
|
||||
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,
|
||||
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)
|
||||
])
|
||||
),
|
||||
validators=[MaxValueValidator(100), MinValueValidator(0)],
|
||||
)
|
||||
|
||||
objects = SegmentQuerySet.as_manager()
|
||||
|
||||
@ -109,26 +124,37 @@ class Segment(ClusterableModel):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Segment.panels = [
|
||||
MultiFieldPanel([
|
||||
FieldPanel('name', classname="title"),
|
||||
FieldRowPanel([
|
||||
FieldPanel('status'),
|
||||
FieldPanel('persistent'),
|
||||
]),
|
||||
FieldPanel('match_any'),
|
||||
FieldPanel('type', widget=forms.RadioSelect),
|
||||
FieldPanel('count', classname='count_field'),
|
||||
FieldPanel('randomisation_percent', classname='percent_field'),
|
||||
], heading="Segment"),
|
||||
MultiFieldPanel([
|
||||
InlinePanel(
|
||||
"{}_related".format(rule_model._meta.db_table),
|
||||
label='{}{}'.format(
|
||||
rule_model._meta.verbose_name,
|
||||
' ({})'.format(_('Static compatible')) if rule_model.static else ''
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldPanel("name", classname="title"),
|
||||
FieldRowPanel(
|
||||
[
|
||||
FieldPanel("status"),
|
||||
FieldPanel("persistent"),
|
||||
]
|
||||
),
|
||||
FieldPanel("match_any"),
|
||||
FieldPanel("type", widget=forms.RadioSelect),
|
||||
FieldPanel("count", classname="count_field"),
|
||||
FieldPanel("randomisation_percent", classname="percent_field"),
|
||||
],
|
||||
heading="Segment",
|
||||
),
|
||||
MultiFieldPanel(
|
||||
[
|
||||
RulePanel(
|
||||
"{}_related".format(rule_model._meta.db_table),
|
||||
label="{}{}".format(
|
||||
rule_model._meta.verbose_name,
|
||||
" ({})".format(_("Static compatible"))
|
||||
if rule_model.static
|
||||
else "",
|
||||
),
|
||||
)
|
||||
for rule_model in AbstractBaseRule.__subclasses__()
|
||||
],
|
||||
heading=_("Rules"),
|
||||
),
|
||||
) for rule_model in AbstractBaseRule.__subclasses__()
|
||||
], heading=_("Rules")),
|
||||
]
|
||||
|
||||
super(Segment, self).__init__(*args, **kwargs)
|
||||
@ -163,29 +189,26 @@ class Segment(ClusterableModel):
|
||||
|
||||
def get_used_pages(self):
|
||||
"""Return the pages that have variants using this segment."""
|
||||
pages = list(PersonalisablePageMetadata.objects.filter(segment=self))
|
||||
|
||||
return pages
|
||||
return PersonalisablePageMetadata.objects.filter(segment=self)
|
||||
|
||||
def get_created_variants(self):
|
||||
"""Return the variants using this segment."""
|
||||
pages = Page.objects.filter(_personalisable_page_metadata__segment=self)
|
||||
|
||||
return pages
|
||||
return Page.objects.filter(_personalisable_page_metadata__segment=self)
|
||||
|
||||
def get_rules(self):
|
||||
"""Retrieve all rules in the segment."""
|
||||
segment_rules = []
|
||||
for rule_model in AbstractBaseRule.get_descendant_models():
|
||||
segment_rules.extend(
|
||||
rule_model._default_manager.filter(segment=self))
|
||||
segment_rules.extend(rule_model._default_manager.filter(segment=self))
|
||||
|
||||
return segment_rules
|
||||
|
||||
def toggle(self, save=True):
|
||||
self.status = (
|
||||
self.STATUS_ENABLED if self.status == self.STATUS_DISABLED
|
||||
else self.STATUS_DISABLED)
|
||||
self.STATUS_ENABLED
|
||||
if self.status == self.STATUS_DISABLED
|
||||
else self.STATUS_DISABLED
|
||||
)
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
@ -208,17 +231,24 @@ class PersonalisablePageMetadata(ClusterableModel):
|
||||
segments.
|
||||
|
||||
"""
|
||||
|
||||
# Canonical pages should not ever be deleted if they have variants
|
||||
# because the variants will be orphaned.
|
||||
canonical_page = models.ForeignKey(
|
||||
Page, related_name='personalisable_canonical_metadata',
|
||||
on_delete=models.SET_NULL,
|
||||
blank=True, null=True
|
||||
Page,
|
||||
models.PROTECT,
|
||||
related_name="personalisable_canonical_metadata",
|
||||
null=True,
|
||||
)
|
||||
|
||||
# Delete metadata of the variant if its page gets deleted.
|
||||
variant = models.OneToOneField(
|
||||
Page, related_name='_personalisable_page_metadata')
|
||||
Page, models.CASCADE, related_name="_personalisable_page_metadata", null=True
|
||||
)
|
||||
|
||||
segment = models.ForeignKey(
|
||||
Segment, related_name='page_metadata', null=True, blank=True)
|
||||
Segment, models.PROTECT, null=True, related_name="page_metadata"
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def has_variants(self):
|
||||
@ -235,10 +265,12 @@ class PersonalisablePageMetadata(ClusterableModel):
|
||||
@cached_property
|
||||
def variants_metadata(self):
|
||||
return (
|
||||
PersonalisablePageMetadata.objects
|
||||
.filter(canonical_page_id=self.canonical_page_id)
|
||||
PersonalisablePageMetadata.objects.filter(
|
||||
canonical_page_id=self.canonical_page_id
|
||||
)
|
||||
.exclude(variant_id=self.variant_id)
|
||||
.exclude(variant_id=self.canonical_page_id))
|
||||
.exclude(variant_id=self.canonical_page_id)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def is_canonical(self):
|
||||
@ -259,37 +291,35 @@ class PersonalisablePageMetadata(ClusterableModel):
|
||||
slug = "{}-{}".format(page.slug, segment.encoded_name())
|
||||
title = "{} ({})".format(page.title, segment.name)
|
||||
update_attrs = {
|
||||
'title': title,
|
||||
'slug': slug,
|
||||
'live': False,
|
||||
"title": title,
|
||||
"slug": slug,
|
||||
"live": False,
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
new_page = self.canonical_page.copy(
|
||||
update_attrs=update_attrs, copy_revisions=False)
|
||||
update_attrs=update_attrs, copy_revisions=False
|
||||
)
|
||||
|
||||
PersonalisablePageMetadata.objects.create(
|
||||
canonical_page=page,
|
||||
variant=new_page,
|
||||
segment=segment)
|
||||
canonical_page=page, variant=new_page, segment=segment
|
||||
)
|
||||
return new_page
|
||||
|
||||
def metadata_for_segments(self, segments):
|
||||
return (
|
||||
self.__class__.objects
|
||||
.filter(
|
||||
canonical_page_id=self.canonical_page_id,
|
||||
segment__in=segments))
|
||||
return self.__class__.objects.filter(
|
||||
canonical_page_id=self.canonical_page_id, segment__in=segments
|
||||
)
|
||||
|
||||
def get_unused_segments(self):
|
||||
if self.is_canonical:
|
||||
return (
|
||||
Segment.objects
|
||||
.exclude(page_metadata__canonical_page_id=self.canonical_page_id))
|
||||
return Segment.objects.exclude(
|
||||
page_metadata__canonical_page_id=self.canonical_page_id
|
||||
)
|
||||
return Segment.objects.none()
|
||||
|
||||
|
||||
class PersonalisablePageMixin(object):
|
||||
class PersonalisablePageMixin:
|
||||
"""The personalisable page model. Allows creation of variants with linked
|
||||
segments.
|
||||
|
||||
@ -301,5 +331,18 @@ class PersonalisablePageMixin(object):
|
||||
metadata = self._personalisable_page_metadata
|
||||
except AttributeError:
|
||||
metadata = PersonalisablePageMetadata.objects.create(
|
||||
canonical_page=self, variant=self)
|
||||
canonical_page=self, variant=self
|
||||
)
|
||||
return metadata
|
||||
|
||||
def get_sitemap_urls(self, request=None):
|
||||
# Do not generate sitemap entries for variants.
|
||||
if not self.personalisation_metadata.is_canonical:
|
||||
return []
|
||||
if wagtail.VERSION >= (2, 2):
|
||||
# Since Wagtail 2.2 you can pass request to the get_sitemap_urls
|
||||
# method.
|
||||
return super(PersonalisablePageMixin, self).get_sitemap_urls(
|
||||
request=request
|
||||
)
|
||||
return super(PersonalisablePageMixin, self).get_sitemap_urls()
|
||||
|
@ -1,43 +1,63 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
|
||||
import pycountry
|
||||
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.template.defaultfilters import slugify
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from modelcluster.fields import ParentalKey
|
||||
from user_agents import parse
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel, PageChooserPanel)
|
||||
from wagtail.admin.panels import FieldPanel, FieldRowPanel
|
||||
|
||||
from wagtail_personalisation.utils import get_client_ip
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_geoip_module():
|
||||
try:
|
||||
from django.contrib.gis.geoip2 import GeoIP2
|
||||
|
||||
return GeoIP2
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"GeoIP module is disabled. To use GeoIP for the origin\n"
|
||||
"country personaliastion rule please set it up as per "
|
||||
"documentation:\n"
|
||||
"https://docs.djangoproject.com/en/stable/ref/contrib/gis/"
|
||||
"geoip2/.\n"
|
||||
"Wagtail-personalisation also works with Cloudflare and\n"
|
||||
"CloudFront country detection, so you should not see this\n"
|
||||
"warning if you use one of those."
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class AbstractBaseRule(models.Model):
|
||||
"""Base for creating rules to segment users with."""
|
||||
icon = 'fa-circle-o'
|
||||
|
||||
icon = "fa-circle-o"
|
||||
static = False
|
||||
|
||||
segment = ParentalKey(
|
||||
'wagtail_personalisation.Segment',
|
||||
related_name="%(app_label)s_%(class)s_related",
|
||||
related_query_name="%(app_label)s_%(class)ss"
|
||||
"wagtail_personalisation.Segment",
|
||||
related_name="%(app_label)s_%(class)ss",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = 'Abstract segmentation rule'
|
||||
verbose_name = "Abstract segmentation rule"
|
||||
|
||||
def __str__(self):
|
||||
return force_text(self._meta.verbose_name)
|
||||
return str(self._meta.verbose_name)
|
||||
|
||||
def test_user(self):
|
||||
"""Test if the user matches this rule."""
|
||||
@ -45,7 +65,7 @@ class AbstractBaseRule(models.Model):
|
||||
|
||||
def encoded_name(self):
|
||||
"""Return a string with a slug for the rule."""
|
||||
return slugify(force_text(self).lower())
|
||||
return slugify(str(self).lower())
|
||||
|
||||
def description(self):
|
||||
"""Return a description explaining the functionality of the rule.
|
||||
@ -56,16 +76,17 @@ class AbstractBaseRule(models.Model):
|
||||
|
||||
"""
|
||||
description = {
|
||||
'title': _('Abstract segmentation rule'),
|
||||
'value': '',
|
||||
"title": _("Abstract segmentation rule"),
|
||||
"value": "",
|
||||
}
|
||||
|
||||
return description
|
||||
|
||||
@classmethod
|
||||
def get_descendant_models(cls):
|
||||
return [model for model in apps.get_models()
|
||||
if issubclass(model, AbstractBaseRule)]
|
||||
return [
|
||||
model for model in apps.get_models() if issubclass(model, AbstractBaseRule)
|
||||
]
|
||||
|
||||
|
||||
class TimeRule(AbstractBaseRule):
|
||||
@ -75,30 +96,32 @@ class TimeRule(AbstractBaseRule):
|
||||
set start time and end time.
|
||||
|
||||
"""
|
||||
icon = 'fa-clock-o'
|
||||
|
||||
icon = "fa-clock-o"
|
||||
|
||||
start_time = models.TimeField(_("Starting time"))
|
||||
end_time = models.TimeField(_("Ending time"))
|
||||
|
||||
panels = [
|
||||
FieldRowPanel([
|
||||
FieldPanel('start_time'),
|
||||
FieldPanel('end_time'),
|
||||
]),
|
||||
FieldRowPanel(
|
||||
[
|
||||
FieldPanel("start_time"),
|
||||
FieldPanel("end_time"),
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Time Rule')
|
||||
verbose_name = _("Time Rule")
|
||||
|
||||
def test_user(self, request=None):
|
||||
return self.start_time <= datetime.now().time() <= self.end_time
|
||||
return self.start_time <= timezone.now().time() <= self.end_time
|
||||
|
||||
def description(self):
|
||||
return {
|
||||
'title': _('These users visit between'),
|
||||
'value': _('{} and {}').format(
|
||||
self.start_time.strftime("%H:%M"),
|
||||
self.end_time.strftime("%H:%M")
|
||||
"title": _("These users visit between"),
|
||||
"value": _("{} and {}").format(
|
||||
self.start_time.strftime("%H:%M"), self.end_time.strftime("%H:%M")
|
||||
),
|
||||
}
|
||||
|
||||
@ -110,7 +133,8 @@ class DayRule(AbstractBaseRule):
|
||||
set in the rule.
|
||||
|
||||
"""
|
||||
icon = 'fa-calendar-check-o'
|
||||
|
||||
icon = "fa-calendar-check-o"
|
||||
|
||||
mon = models.BooleanField(_("Monday"), default=False)
|
||||
tue = models.BooleanField(_("Tuesday"), default=False)
|
||||
@ -121,34 +145,39 @@ class DayRule(AbstractBaseRule):
|
||||
sun = models.BooleanField(_("Sunday"), default=False)
|
||||
|
||||
panels = [
|
||||
FieldPanel('mon'),
|
||||
FieldPanel('tue'),
|
||||
FieldPanel('wed'),
|
||||
FieldPanel('thu'),
|
||||
FieldPanel('fri'),
|
||||
FieldPanel('sat'),
|
||||
FieldPanel('sun'),
|
||||
FieldPanel("mon"),
|
||||
FieldPanel("tue"),
|
||||
FieldPanel("wed"),
|
||||
FieldPanel("thu"),
|
||||
FieldPanel("fri"),
|
||||
FieldPanel("sat"),
|
||||
FieldPanel("sun"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Day Rule')
|
||||
verbose_name = _("Day Rule")
|
||||
|
||||
def test_user(self, request=None):
|
||||
return [self.mon, self.tue, self.wed, self.thu,
|
||||
self.fri, self.sat, self.sun][datetime.today().weekday()]
|
||||
return [self.mon, self.tue, self.wed, self.thu, self.fri, self.sat, self.sun][
|
||||
timezone.now().date().weekday()
|
||||
]
|
||||
|
||||
def description(self):
|
||||
days = (
|
||||
('mon', self.mon), ('tue', self.tue), ('wed', self.wed),
|
||||
('thu', self.thu), ('fri', self.fri), ('sat', self.sat),
|
||||
('sun', self.sun),
|
||||
("mon", self.mon),
|
||||
("tue", self.tue),
|
||||
("wed", self.wed),
|
||||
("thu", self.thu),
|
||||
("fri", self.fri),
|
||||
("sat", self.sat),
|
||||
("sun", self.sun),
|
||||
)
|
||||
|
||||
chosen_days = [day_name for day_name, chosen in days if chosen]
|
||||
|
||||
return {
|
||||
'title': _('These users visit on'),
|
||||
'value': ", ".join([day for day in chosen_days]).title(),
|
||||
"title": _("These users visit on"),
|
||||
"value": ", ".join([day for day in chosen_days]).title(),
|
||||
}
|
||||
|
||||
|
||||
@ -159,32 +188,32 @@ class ReferralRule(AbstractBaseRule):
|
||||
the set regex test.
|
||||
|
||||
"""
|
||||
icon = 'fa-globe'
|
||||
|
||||
regex_string = models.TextField(
|
||||
_("Regular expression to match the referrer"))
|
||||
icon = "fa-globe"
|
||||
|
||||
regex_string = models.TextField(_("Regular expression to match the referrer"))
|
||||
|
||||
panels = [
|
||||
FieldPanel('regex_string'),
|
||||
FieldPanel("regex_string"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Referral Rule')
|
||||
verbose_name = _("Referral Rule")
|
||||
|
||||
def test_user(self, request):
|
||||
pattern = re.compile(self.regex_string)
|
||||
|
||||
if 'HTTP_REFERER' in request.META:
|
||||
referer = request.META['HTTP_REFERER']
|
||||
if "HTTP_REFERER" in request.META:
|
||||
referer = request.META["HTTP_REFERER"]
|
||||
if pattern.search(referer):
|
||||
return True
|
||||
return False
|
||||
|
||||
def description(self):
|
||||
return {
|
||||
'title': _('These visits originate from'),
|
||||
'value': self.regex_string,
|
||||
'code': True
|
||||
"title": _("These visits originate from"),
|
||||
"value": self.regex_string,
|
||||
"code": True,
|
||||
}
|
||||
|
||||
|
||||
@ -196,52 +225,65 @@ class VisitCountRule(AbstractBaseRule):
|
||||
when visiting the set page.
|
||||
|
||||
"""
|
||||
icon = 'fa-calculator'
|
||||
|
||||
icon = "fa-calculator"
|
||||
static = True
|
||||
|
||||
OPERATOR_CHOICES = (
|
||||
('more_than', _("More than")),
|
||||
('less_than', _("Less than")),
|
||||
('equal_to', _("Equal to")),
|
||||
("more_than", _("More than")),
|
||||
("less_than", _("Less than")),
|
||||
("equal_to", _("Equal to")),
|
||||
)
|
||||
operator = models.CharField(
|
||||
max_length=20, choices=OPERATOR_CHOICES, default="more_than"
|
||||
)
|
||||
operator = models.CharField(max_length=20,
|
||||
choices=OPERATOR_CHOICES, default="more_than")
|
||||
count = models.PositiveSmallIntegerField(default=0, null=True)
|
||||
counted_page = models.ForeignKey(
|
||||
'wagtailcore.Page',
|
||||
"wagtailcore.Page",
|
||||
null=False,
|
||||
blank=False,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='+',
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
panels = [
|
||||
PageChooserPanel('counted_page'),
|
||||
FieldRowPanel([
|
||||
FieldPanel('operator'),
|
||||
FieldPanel('count'),
|
||||
]),
|
||||
FieldPanel("counted_page"),
|
||||
FieldRowPanel(
|
||||
[
|
||||
FieldPanel("operator"),
|
||||
FieldPanel("count"),
|
||||
]
|
||||
),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Visit count Rule')
|
||||
verbose_name = _("Visit count Rule")
|
||||
|
||||
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):
|
||||
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)
|
||||
SEGMENT_ADAPTER_CLASS,
|
||||
SessionSegmentsAdapter,
|
||||
get_segment_adapter,
|
||||
)
|
||||
|
||||
# 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 = RequestFactory().get("/")
|
||||
request.user = user
|
||||
|
||||
# If we're using the session adapter check for an active session
|
||||
@ -273,13 +315,8 @@ class VisitCountRule(AbstractBaseRule):
|
||||
|
||||
def description(self):
|
||||
return {
|
||||
'title': _('These users visited {}').format(
|
||||
self.counted_page
|
||||
),
|
||||
'value': _('{} {} times').format(
|
||||
self.get_operator_display(),
|
||||
self.count
|
||||
),
|
||||
"title": _("These users visited {}").format(self.counted_page),
|
||||
"value": _("{} {} times").format(self.get_operator_display(), self.count),
|
||||
}
|
||||
|
||||
def get_column_header(self):
|
||||
@ -288,10 +325,13 @@ class VisitCountRule(AbstractBaseRule):
|
||||
def get_user_info_string(self, user):
|
||||
# Local import for cyclic import
|
||||
from wagtail_personalisation.adapters import (
|
||||
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
|
||||
SEGMENT_ADAPTER_CLASS,
|
||||
SessionSegmentsAdapter,
|
||||
get_segment_adapter,
|
||||
)
|
||||
|
||||
# Create a fake request so we can use the adapter
|
||||
request = RequestFactory().get('/')
|
||||
request = RequestFactory().get("/")
|
||||
request.user = user
|
||||
|
||||
# If we're using the session adapter check for an active session
|
||||
@ -312,32 +352,28 @@ class QueryRule(AbstractBaseRule):
|
||||
present in the request query.
|
||||
|
||||
"""
|
||||
icon = 'fa-link'
|
||||
|
||||
parameter = models.SlugField(_("The query parameter to search for"),
|
||||
max_length=20)
|
||||
value = models.SlugField(_("The value of the parameter to match"),
|
||||
max_length=20)
|
||||
icon = "fa-link"
|
||||
|
||||
parameter = models.SlugField(_("The query parameter to search for"), max_length=20)
|
||||
value = models.SlugField(_("The value of the parameter to match"), max_length=20)
|
||||
|
||||
panels = [
|
||||
FieldPanel('parameter'),
|
||||
FieldPanel('value'),
|
||||
FieldPanel("parameter"),
|
||||
FieldPanel("value"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Query Rule')
|
||||
verbose_name = _("Query Rule")
|
||||
|
||||
def test_user(self, request):
|
||||
return request.GET.get(self.parameter, '') == self.value
|
||||
return request.GET.get(self.parameter, "") == self.value
|
||||
|
||||
def description(self):
|
||||
return {
|
||||
'title': _('These users used a URL with the query'),
|
||||
'value': _('?{}={}').format(
|
||||
self.parameter,
|
||||
self.value
|
||||
),
|
||||
'code': True
|
||||
"title": _("These users used a URL with the query"),
|
||||
"value": _("?{}={}").format(self.parameter, self.value),
|
||||
"code": True,
|
||||
}
|
||||
|
||||
|
||||
@ -348,23 +384,24 @@ class DeviceRule(AbstractBaseRule):
|
||||
in the request user agent headers.
|
||||
|
||||
"""
|
||||
icon = 'fa-tablet'
|
||||
|
||||
icon = "fa-tablet"
|
||||
|
||||
mobile = models.BooleanField(_("Mobile phone"), default=False)
|
||||
tablet = models.BooleanField(_("Tablet"), default=False)
|
||||
desktop = models.BooleanField(_("Desktop"), default=False)
|
||||
|
||||
panels = [
|
||||
FieldPanel('mobile'),
|
||||
FieldPanel('tablet'),
|
||||
FieldPanel('desktop'),
|
||||
FieldPanel("mobile"),
|
||||
FieldPanel("tablet"),
|
||||
FieldPanel("desktop"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Device Rule')
|
||||
verbose_name = _("Device Rule")
|
||||
|
||||
def test_user(self, request=None):
|
||||
ua_header = request.META['HTTP_USER_AGENT']
|
||||
ua_header = request.META["HTTP_USER_AGENT"]
|
||||
user_agent = parse(ua_header)
|
||||
|
||||
if user_agent.is_mobile:
|
||||
@ -383,22 +420,90 @@ class UserIsLoggedInRule(AbstractBaseRule):
|
||||
Matches when the user is authenticated.
|
||||
|
||||
"""
|
||||
icon = 'fa-user'
|
||||
|
||||
icon = "fa-user"
|
||||
|
||||
is_logged_in = models.BooleanField(default=False)
|
||||
|
||||
panels = [
|
||||
FieldPanel('is_logged_in'),
|
||||
FieldPanel("is_logged_in"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Logged in Rule')
|
||||
verbose_name = _("Logged in Rule")
|
||||
|
||||
def test_user(self, request=None):
|
||||
return request.user.is_authenticated() == self.is_logged_in
|
||||
return request.user.is_authenticated == self.is_logged_in
|
||||
|
||||
def description(self):
|
||||
return {
|
||||
'title': _('These visitors are'),
|
||||
'value': _('Logged in') if self.is_logged_in else _('Not logged in'),
|
||||
"title": _("These visitors are"),
|
||||
"value": _("Logged in") if self.is_logged_in else _("Not logged in"),
|
||||
}
|
||||
|
||||
|
||||
COUNTRY_CHOICES = [
|
||||
(country.alpha_2.lower(), country.name) for country in pycountry.countries
|
||||
]
|
||||
|
||||
|
||||
class OriginCountryRule(AbstractBaseRule):
|
||||
"""
|
||||
Test user against the country or origin of their request.
|
||||
|
||||
Using this rule requires setting up GeoIP2 on Django or using
|
||||
CloudFlare or CloudFront geolocation detection.
|
||||
"""
|
||||
|
||||
country = models.CharField(
|
||||
max_length=2,
|
||||
choices=COUNTRY_CHOICES,
|
||||
help_text=_(
|
||||
"Select origin country of the request that this rule will "
|
||||
"match against. This rule will only work if you use "
|
||||
"Cloudflare or CloudFront IP geolocation or if GeoIP2 "
|
||||
"module is configured."
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("origin country rule")
|
||||
|
||||
def get_cloudflare_country(self, request):
|
||||
"""
|
||||
Get country code that has been detected by Cloudflare.
|
||||
|
||||
Guide to the functionality:
|
||||
https://support.cloudflare.com/hc/en-us/articles/200168236-What-does-Cloudflare-IP-Geolocation-do-
|
||||
"""
|
||||
try:
|
||||
return request.META["HTTP_CF_IPCOUNTRY"].lower()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_cloudfront_country(self, request):
|
||||
try:
|
||||
return request.META["HTTP_CLOUDFRONT_VIEWER_COUNTRY"].lower()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_geoip_country(self, request):
|
||||
GeoIP2 = get_geoip_module()
|
||||
if GeoIP2 is None:
|
||||
return False
|
||||
return GeoIP2().country_code(get_client_ip(request)).lower()
|
||||
|
||||
def get_country(self, request):
|
||||
# Prioritise CloudFlare and CloudFront country detection over GeoIP.
|
||||
functions = (
|
||||
self.get_cloudflare_country,
|
||||
self.get_cloudfront_country,
|
||||
self.get_geoip_country,
|
||||
)
|
||||
for function in functions:
|
||||
result = function(request)
|
||||
if result:
|
||||
return result
|
||||
|
||||
def test_user(self, request=None):
|
||||
return (self.get_country(request) or "") == self.country.lower()
|
||||
|
0
src/wagtail_personalisation/static/img/.gitkeep
Normal file
@ -37,8 +37,6 @@
|
||||
/******/ 3: 0
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ var resolvedPromise = new Promise(function(resolve) { resolve(); });
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
@ -66,20 +64,21 @@
|
||||
/******/ // This file contains only the entry chunk.
|
||||
/******/ // The chunk loading function for additional chunks
|
||||
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
|
||||
/******/ if(installedChunks[chunkId] === 0) {
|
||||
/******/ return resolvedPromise;
|
||||
/******/ var installedChunkData = installedChunks[chunkId];
|
||||
/******/ if(installedChunkData === 0) {
|
||||
/******/ return new Promise(function(resolve) { resolve(); });
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // a Promise means "currently loading".
|
||||
/******/ if(installedChunks[chunkId]) {
|
||||
/******/ return installedChunks[chunkId][2];
|
||||
/******/ if(installedChunkData) {
|
||||
/******/ return installedChunkData[2];
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // setup Promise in chunk cache
|
||||
/******/ var promise = new Promise(function(resolve, reject) {
|
||||
/******/ installedChunks[chunkId] = [resolve, reject];
|
||||
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
|
||||
/******/ });
|
||||
/******/ installedChunks[chunkId][2] = promise;
|
||||
/******/ installedChunkData[2] = promise;
|
||||
/******/
|
||||
/******/ // start chunk loading
|
||||
/******/ var head = document.getElementsByTagName('head')[0];
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "modeladmin/index.html" %}
|
||||
{% load i18n l10n staticfiles modeladmin_tags %}
|
||||
{% load i18n l10n static modeladmin_tags %}
|
||||
|
||||
{% block titletag %}{{ view.get_meta_title }}{% endblock %}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "modeladmin/wagtail_personalisation/segment/base.html" %}
|
||||
{% load i18n l10n staticfiles modeladmin_tags wagtail_personalisation_filters %}
|
||||
{% load i18n l10n static modeladmin_tags wagtail_personalisation_filters %}
|
||||
|
||||
{% block toggle_view %}to List {% endblock%}
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
{% for segment in object_list %}
|
||||
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
|
||||
<h2>{{ segment }}</h2>
|
||||
|
||||
<div class="inspect_container">
|
||||
<ul class="inspect segment_stats">
|
||||
<li class="stat_card">
|
||||
|
@ -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 %}
|
@ -1,5 +1,4 @@
|
||||
{% extends "modeladmin/wagtail_personalisation/segment/base.html" %}
|
||||
{% load i18n l10n staticfiles modeladmin_tags wagtail_personalisation_filters %}
|
||||
{% load i18n l10n static modeladmin_tags wagtail_personalisation_filters %}
|
||||
|
||||
{% block toggle_view %}to Dashboard {% 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 %}
|
@ -5,6 +5,6 @@ from wagtail_personalisation.utils import count_active_days
|
||||
register = Library()
|
||||
|
||||
|
||||
@register.filter(name='days_since')
|
||||
@register.filter(name="days_since")
|
||||
def active_days(enable_date, disable_date):
|
||||
return count_active_days(enable_date, disable_date)
|
||||
|
@ -15,17 +15,17 @@ def do_segment(parser, token):
|
||||
tag_name, _, kwargs = parse_tag(token, parser)
|
||||
|
||||
# If no segment is provided this block will raise an error
|
||||
if set(kwargs.keys()) != {'name'}:
|
||||
if set(kwargs.keys()) != {"name"}:
|
||||
usage = '{% segment name="segmentname" %} ... {% endsegment %}'
|
||||
raise TemplateSyntaxError("Usage: %s" % usage)
|
||||
|
||||
nodelist = parser.parse(('endsegment',))
|
||||
nodelist = parser.parse(("endsegment",))
|
||||
parser.delete_first_token()
|
||||
|
||||
return SegmentNode(nodelist, name=kwargs['name'])
|
||||
return SegmentNode(nodelist, name=kwargs["name"])
|
||||
|
||||
|
||||
register.tag('segment', do_segment)
|
||||
register.tag("segment", do_segment)
|
||||
|
||||
|
||||
class SegmentNode(template.Node):
|
||||
@ -36,6 +36,7 @@ class SegmentNode(template.Node):
|
||||
If not it will return nothing
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, nodelist, name):
|
||||
self.nodelist = nodelist
|
||||
self.name = name
|
||||
@ -48,10 +49,10 @@ class SegmentNode(template.Node):
|
||||
return ""
|
||||
|
||||
# Check if user has segment
|
||||
adapter = get_segment_adapter(context['request'])
|
||||
adapter = get_segment_adapter(context["request"])
|
||||
user_segment = adapter.get_segment_by_id(segment_id=segment.pk)
|
||||
if not user_segment:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
content = self.nodelist.render(context)
|
||||
content = mark_safe(content)
|
||||
|
@ -1,7 +1,10 @@
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.template.base import FilterExpression, kwarg_re
|
||||
from django.utils import timezone
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
|
||||
def impersonate_other_page(page, other_page):
|
||||
@ -33,7 +36,7 @@ def create_segment_dictionary(segment):
|
||||
"encoded_name": segment.encoded_name(),
|
||||
"id": segment.pk,
|
||||
"timestamp": int(time.time()),
|
||||
"persistent": segment.persistent
|
||||
"persistent": segment.persistent,
|
||||
}
|
||||
|
||||
|
||||
@ -98,22 +101,35 @@ def parse_tag(token, parser):
|
||||
def exclude_variants(pages):
|
||||
"""Checks if page is not a variant
|
||||
|
||||
:param pages: List of pages to check
|
||||
:type pages: list
|
||||
:return: List of pages that aren't variants
|
||||
:rtype: list
|
||||
:param pages: Set of pages to check
|
||||
:type pages: QuerySet
|
||||
:return: Queryset of pages that aren't variants
|
||||
:rtype: QuerySet
|
||||
"""
|
||||
return [
|
||||
page for page in pages
|
||||
if (
|
||||
(
|
||||
hasattr(page, 'personalisation_metadata') is False
|
||||
) or
|
||||
(
|
||||
hasattr(page, 'personalisation_metadata') and page.personalisation_metadata is None
|
||||
) or
|
||||
(
|
||||
hasattr(page, 'personalisation_metadata') and page.personalisation_metadata.is_canonical
|
||||
)
|
||||
)
|
||||
]
|
||||
from wagtail_personalisation.models import PersonalisablePageMetadata
|
||||
|
||||
excluded_variant_pages = PersonalisablePageMetadata.objects.exclude(
|
||||
canonical_page_id=F("variant_id")
|
||||
).values_list("variant_id")
|
||||
return pages.exclude(pk__in=excluded_variant_pages)
|
||||
|
||||
|
||||
def can_delete_pages(pages, user):
|
||||
for variant in pages:
|
||||
if not variant.permissions_for_user(user).can_delete():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_client_ip(request):
|
||||
try:
|
||||
func = import_string(settings.WAGTAIL_PERSONALISATION_IP_FUNCTION)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
return func(request)
|
||||
try:
|
||||
x_forwarded_for = request.META["HTTP_X_FORWARDED_FOR"]
|
||||
return x_forwarded_for.split(",")[-1].strip()
|
||||
except KeyError:
|
||||
return request.META["REMOTE_ADDR"]
|
||||
|