7

Compare commits

..

2 Commits

Author SHA1 Message Date
808cc7d9ed adding missing migration 2016-12-12 20:47:27 +01:00
7c52d318ff adding cloudfront device detection rule 2016-12-12 20:44:21 +01:00
206 changed files with 1451 additions and 16398 deletions

View File

@ -11,12 +11,10 @@ line_length = 79
multi_line_output = 4
balanced_wrapping = true
use_parentheses = true
default_section = THIRDPARTY
known_first_party = wagtail_personalisation,tests
[*.json, *.yml, *rc]
indent_size = 2
[Makefile]
indent_style = tab
indent_size = 4
indent_size = 4

View File

@ -1,3 +0,0 @@
{
"extends": "airbnb"
}

View File

@ -1,89 +0,0 @@
---
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: 4
matrix:
tox_env:
- py36-dj22-wt211
- py36-dj22-wt212
- py36-dj22-wt213
- py37-dj22-wt211
- py37-dj22-wt212
- py37-dj22-wt213
- py38-dj22-wt211
- py38-dj22-wt212
- py38-dj22-wt213
- py37-dj30-wt211
- py37-dj30-wt212
- py37-dj30-wt213
- py38-dj30-wt211
- py38-dj30-wt212
- py38-dj30-wt213
include:
- python-version: 3.6
tox_env: py36-dj22-wt211
- python-version: 3.6
tox_env: py36-dj22-wt212
- python-version: 3.6
tox_env: py36-dj22-wt213
- python-version: 3.7
tox_env: py37-dj22-wt211
- python-version: 3.7
tox_env: py37-dj22-wt212
- python-version: 3.7
tox_env: py37-dj22-wt213
- python-version: 3.8
tox_env: py38-dj22-wt211
- python-version: 3.8
tox_env: py38-dj22-wt212
- python-version: 3.8
tox_env: py38-dj22-wt213
- python-version: 3.7
tox_env: py37-dj30-wt211
- python-version: 3.7
tox_env: py37-dj30-wt212
- python-version: 3.7
tox_env: py37-dj30-wt213
- python-version: 3.8
tox_env: py38-dj30-wt211
- python-version: 3.8
tox_env: py38-dj30-wt212
- python-version: 3.8
tox_env: py38-dj30-wt213
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox -e ${{ matrix.tox_env }} --index-url=https://pypi.python.org/simple/
- name: Prepare artifacts
run: mkdir -p .coverage-data && mv .coverage.* .coverage-data/
- uses: actions/upload-artifact@master
with:
name: coverage-data
path: .coverage-data/

13
.gitignore vendored
View File

@ -3,26 +3,19 @@
*.swo
*.python-version
*.coverage
.coverage.*
*.egg-info/
.cache/
.idea/
.tox/
.vscode/
build/
ve/
dist/
tests/sandbox/assets
htmlcov/
docs/_build
coverage.xml
db.sqlite3
tests/sandbox/assets
node_modules
.DS_Store
.pytest_cache/
.vscode/settings.json

View File

@ -1,9 +0,0 @@
[main]
host = https://www.transifex.com
[wagtail_personalisation]
file_filter = src/wagtail_personalisation/locale/<lang>/LC_MESSAGES/django.po
source_file = src/wagtail_personalisation/locale/en/LC_MESSAGES/django.po
source_lang = en
type = PO

121
CHANGES
View File

@ -1,118 +1,3 @@
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
- Enable retrieval of user data for static rules through csv download
0.10.9
==================
- Bug Fix: Display the number of users in a static segment on dashboard
0.10.8
==================
- Don't add users to exclude list for dynamic segments
- Store segments a user is excluded from in the session
0.10.7
==================
- Bug Fix: Ensure static segment members are show the survey immediately
- Records users excluded by randomisation on the segment
- Don't re-check excluded users
0.10.6
==================
- Accepts and stores randomisation percentage for segment
- Adds users to segment based on random number relative to percentage
0.10.5
==================
- Count how many users match a segments rules before saving the segment
- Stores count on the segment and displays in the dashboard
- Enables testing users against rules if there isn't an active request
0.10.0
==================
- Adds static and dynamic segments
0.9.1 (tbd)
==================
- Fixes import for reverse resolver for older Django versions (<1.10)
- Bases migrations off of older wagtail dependencies
- Adds more dashboard panels and fixes exclude variants function
0.9.0 (2017-06-02)
==================
Initial release of wagtail-personalisation. This Wagtail module provides basic
personalisation based on pre-defined rules in the backend.
This module was developed by Boris Besemer (@blurrah) and Jasper Berghoef
(@jberghoef) for Lab Digital (http://labdigital.nl)
0.1 (TBD)
====================
- Initial release

View File

@ -1,20 +0,0 @@
Authors
=======
* Jasper Berghoef
* Boris Besemer
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

21
LICENSE
View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016-2017 Lab Digital
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,6 +1,3 @@
include README.rst
recursive-include src *
recursive-exclude src __pycache__
recursive-exclude src *.py[co]
recursive-include src

View File

@ -1,18 +1,16 @@
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
default: develop
.PHONY: all clean requirements develop test lint flake8 isort dist
all: clean requirements dist
default: develop
clean:
find src -name '*.pyc' -delete
find tests -name '*.pyc' -delete
find . -name '*.egg-info' |xargs rm -rf
find . -name '*.egg-info' -delete
requirements:
pip install --upgrade -e .[docs,test]
install: develop
pip install --upgrade -e .
develop: clean requirements
@ -23,33 +21,18 @@ retest:
py.test --nomigrations --reuse-db tests/ -vvv
coverage:
py.test --nomigrations --reuse-db tests/ --cov=wagtail_personalisation --cov-report=term-missing --cov-report=html
docs:
$(MAKE) -C docs html
py.test --nomigrations --reuse-db tests/ --cov=personalisation --cov-report=term-missing --cov-report=html
lint: flake8 isort
flake8:
flake8 src/ tests/
pip install flake8 flake8-debugger flake8-blind-except
flake8 src/
isort:
pip install isort
isort --recursive src tests
dist:
pip install wheel
python ./setup.py sdist bdist_wheel
sandbox:
pip install -r sandbox/requirements.txt
sandbox/manage.py migrate
sandbox/manage.py loaddata sandbox/exampledata/users.json
sandbox/manage.py loaddata sandbox/exampledata/personalisation.json
sandbox/manage.py runserver
release:
pip install twine wheel
rm -rf dist/*
python setup.py sdist bdist_wheel
twine upload -s dist/*
./setup.py sdist bdist_wheel

View File

@ -1,89 +1,25 @@
.. 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/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/
.. end-no-pypi
.. image:: logo.png
:height: 261
:width: 300
:scale: 50
:alt: Wagxperience
:align: center
Wagtail Personalisation
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:: screenshot.png
Wagtail personalisation enables simple content personalisation through segmenting for Wagtail.
Instructions
------------
Wagtail Personalisation requires Wagtail 2.0 or 2.1 and Django 1.11 or 2.0.
To install the package with pip:
.. code-block:: console
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``:
Next, include the ``personalisation`` and ``wagtail.contrib.modeladmin`` app in your project's ``INSTALLED_APPS``:
.. code-block:: python
INSTALLED_APPS = [
# ...
'wagtail.contrib.modeladmin',
'wagtail_personalisation',
'wagtailfontawesome',
'personalisation',
# ...
]
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',
# ...
]
Documentation
-------------
You can find more information about installing, extending and using this module
on `Read the Docs`_.
.. _Read the Docs: http://wagtail-personalisation.readthedocs.io
Sandbox
-------
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``.
Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has been added in first, this is a prerequisite for this project.

View File

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
SPHINXPROJ = wagtail-personalisation
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

View File

@ -1,167 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# wagtail-personalisation documentation build configuration file, created by
# sphinx-quickstart on Mon Dec 19 15:12:32 2016.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import sys
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 ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'wagtail-personalisation'
copyright = '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.15.3'
# The full version, including alpha/beta/rc tags.
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
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
# html_theme = 'alabaster'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
# html_theme_options = {}
html_theme_options = {
'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".
# html_static_path = ['_static']
# -- Options for HTMLHelp output ------------------------------------------
# Output file base name for HTML help builder.
htmlhelp_basename = 'wagtail-personalisationdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'wagtail-personalisation.tex', 'wagtail-personalisation Documentation',
'Lab Digital BV', 'manual'),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
[author], 1)
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
author, 'wagtail-personalisation', 'One line description of project.',
'Miscellaneous'),
]

View File

@ -1,177 +0,0 @@
Included rules
==============
Wagxperience comes with a base set of rules that allow you to start segmenting
your visitors quickly.
Time rule
---------
The time rule allows you to segment visitors based on the time of their visit.
Define a time frame in which visitors are matched to this segment.
================== ==========================================================
Option Description
================== ==========================================================
Start time The start time of your time frame.
End time The end time of your time frame.
================== ==========================================================
``wagtail_personalisation.rules.TimeRule``
Day rule
--------
The day rule allows you to segment visitors based on the day of their visit.
Select one or multiple days on which you would like your segment to be applied.
================== ==========================================================
Option Description
================== ==========================================================
Monday Matches when the visitors visits on a monday.
Tuesday Matches when the visitors visits on a tuesday.
Wednesday Matches when the visitors visits on a wednesday.
Thursday Matches when the visitors visits on a thursday.
Friday Matches when the visitors visits on a friday.
Saturday Matches when the visitors visits on a saturday.
Sunday Matches when the visitors visits on a sunday.
================== ==========================================================
``wagtail_personalisation.rules.DayRule``
Referral rule
-------------
The referral rule allows you to match visitors based on the website they were
referred from. For example:
.. code-block:: bash
example\.com|secondexample\.com|.*subdomain\.com
================== ==========================================================
Option Description
================== ==========================================================
Regex string The regex string to match the referral header to.
================== ==========================================================
``wagtail_personalisation.rules.ReferralRule``
Visit count rule
----------------
The visit count rule allows you to segment a visitor based on the amount of
visits per page. Use the operator to to set a maximum, minimum or equal
amount of visits.
================== ==========================================================
Option Description
================== ==========================================================
Page The page on which visits will be counted.
Count The amount of visits to match.
Operator Whether to match for more than, less than or equal to the
specified visit count.
================== ==========================================================
``wagtail_personalisation.rules.VisitCountRule``
Query rule
----------
The query rule allows you to match a visitor based on the query included in
the url. It let's you define both the parameter and the value. It will look
something like this:
.. code-block:: bash
example.com/?campaign=ourbestoffer
================== ==========================================================
Option Description
================== ==========================================================
Parameter The first part of the query ('campaign').
Value The second part of the query ('ourbestoffer').
================== ==========================================================
``wagtail_personalisation.rules.QueryRule``
Device rule
-----------
The device rule allows you to match visitors by the type of device they are
using. You can select any combination you want.
================== ==========================================================
Option Description
================== ==========================================================
Mobile phone Matches when the visitor uses a mobile phone.
Tablet Matches when the visitor uses a tablet.
Desktop Matches when the visitor uses a desktop.
================== ==========================================================
``wagtail_personalisation.rules.DeviceRule``
User is logged in rule
----------------------
The user is logged in rule allows you to match visitors that are authenticated
and logged in to your app.
================== ==========================================================
Option Description
================== ==========================================================
Is logged in Whether the user is logged in or logged out.
================== ==========================================================
``wagtail_personalisation.rules.UserIsLoggedInRule``
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``

View File

@ -1,91 +0,0 @@
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.

View File

@ -1,84 +0,0 @@
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.

View File

@ -1,13 +0,0 @@
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

View File

@ -1,22 +0,0 @@
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!

View File

@ -1,84 +0,0 @@
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.

View File

@ -1,8 +0,0 @@
Getting Started
===============
.. toctree::
:maxdepth: 3
installation
sandbox

View File

@ -1,37 +0,0 @@
Installing Wagxperience
=======================
Wagtail Personalisation requires Wagtail_ 2.0 or 2.1 and Django_ 1.11 or 2.0.
.. _Wagtail: https://github.com/wagtail/wagtail
.. _Django: https://github.com/django/django
To install the package with pip:
.. code-block:: console
pip install wagtail-personalisation
Next, include the ``wagtail_personalisation``, ``wagtail.contrib.modeladmin``
and ``wagtailfontawesome`` apps in your project's ``INSTALLED_APPS``:
.. code-block:: python
INSTALLED_APPS = [
# ...
'wagtail.contrib.modeladmin',
'wagtail_personalisation',
'wagtailfontawesome',
# ...
]
Make sure that ``django.contrib.sessions.middleware.SessionMiddleware`` has
been added in first, this is a prerequisite for this project.
.. code-block:: python
MIDDLEWARE = [
'django.contrib.sessions.middleware.SessionMiddleware',
# ...
]

View File

@ -1,14 +0,0 @@
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

View File

@ -1,51 +0,0 @@
.. wagtail-personalisation documentation master file, created by
sphinx-quickstart on Mon Dec 19 15:12:32 2016.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to the Wagxperience documentation
=========================================
.. 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
getting_started/index
usage_guide/index
editor_guide/index
default_rules

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View File

@ -1,36 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
set SPHINXPROJ=wagtail-personalisation
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
:end
popd

View File

@ -1,17 +0,0 @@
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!

View File

@ -1,71 +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:
.. code-block:: python
from wagtail.wagtailcore.models import Page
from wagtail_personalisation.models import PersonalisablePageMixin
class HomePage(PersonalisablePageMixin, Page):
pass
All you need is the ``PersonalisablePageMixin`` mixin and a Wagtail ``Page``
class of your liking.
.. _implementing_streamfield_blocks:
Adding personalisable StreamField blocks
----------------------------------------
Taking things a step further, you can also add personalisable StreamField blocks
to your page models. Below is the full Homepage model used in the sandbox.
.. literalinclude:: ../../sandbox/sandbox/apps/home/models.py
.. _implementing_template_blocks:
Using template blocks for personalisation
-----------------------------------------
*Please note that using the personalisable template tag is not the recommended
method for adding personalisation to your content, as it is largely decoupled
from the administration interface. Use responsibly.*
You can add a template block that only shows its contents to users of a
specific segment. This is done using the "segment" block.
When editing templates make sure to load the ``wagtail_personalisation_tags``
tags library in the template:
.. code-block:: jinja
{% load wagtail_personalisation_tags %}
After that you can add a template block with the name of the segment you want
the content to show up for:
.. code-block:: jinja
{% segment name="My Segment" %}
<p>Only users within "My Segment" see this!</p>
{% endsegment %}
The template block currently only supports one segment at a time. If you want
to target multiple segments you will have to make multiple blocks with the
same content.

View File

@ -1,8 +0,0 @@
Usage Guide
===========
.. toctree::
:maxdepth: 3
implementation
custom_rules

View File

View File

@ -1 +0,0 @@
import '../scss/dashboard.scss';

View File

@ -1 +0,0 @@
import '../scss/form.scss';

View File

@ -1 +0,0 @@
import '../scss/index.scss';

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

22
manage.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.sandbox.settings")
try:
from django.core.management import execute_from_command_line
except ImportError:
# The above import may fail for some other reason. Ensure that the
# issue is really that Django is missing to avoid masking other
# exceptions on Python 2.
try:
import django
except ImportError:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
)
raise
execute_from_command_line(sys.argv)

View File

@ -1,53 +0,0 @@
{
"devDependencies": {
"autoprefixer": "^7.1.1",
"babel-core": "^6.24.1",
"babel-loader": "^7.0.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-0": "^6.24.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.2",
"eslint": "^3.19.0",
"eslint-config-airbnb": "^15.0.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jsx-a11y": "^5.0.3",
"eslint-plugin-react": "^7.0.1",
"extract-text-webpack-plugin": "^2.1.0",
"file-loader": "^0.11.1",
"imagemin-webpack-plugin": "^1.4.4",
"jshint": "^2.9.4",
"mocha": "^3.4.1",
"node-sass": "^4.5.3",
"postcss-loader": "^2.0.5",
"sass-loader": "^6.0.5",
"style-loader": "^0.18.0",
"uglify-js": "^3.0.10",
"uglifyjs-webpack-plugin": "^0.4.3",
"webpack": "^2.6.0"
},
"name": "wagtail-personalisation",
"description": "Wagxperience personalisation module for Wagtail.",
"version": "1.0.0",
"main": "webpack.config.js",
"directories": {
"doc": "docs",
"test": "tests"
},
"scripts": {
"start": "yarn compile && yarn watch",
"compile": "webpack --bail",
"watch": "webpack --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/wagtail/wagtail-personalisation.git"
},
"author": "Lab Digital",
"license": "ISC",
"bugs": {
"url": "https://github.com/wagtail/wagtail-personalisation/issues"
},
"homepage": "https://github.com/wagtail/wagtail-personalisation#readme"
}

View File

@ -1,148 +0,0 @@
[{
"model": "wagtail_personalisation.timerule",
"pk": 1,
"fields": {
"segment": 2,
"start_time": "06:00:00",
"end_time": "11:00:00"
}
}, {
"model": "wagtail_personalisation.visitcountrule",
"pk": 1,
"fields": {
"segment": 1,
"operator": "more_than",
"count": 3,
"counted_page": 3
}
}, {
"model": "wagtail_personalisation.segment",
"pk": 1,
"fields": {
"name": "Returning Rook",
"create_date": "2017-06-02T05:38:02.304Z",
"edit_date": "2017-06-02T10:58:39.399Z",
"enable_date": "2017-06-02T10:58:39.389Z",
"disable_date": "2017-06-02T10:34:51.722Z",
"visit_count": 0,
"status": "enabled",
"persistent": false,
"match_any": false
}
}, {
"model": "wagtail_personalisation.segment",
"pk": 2,
"fields": {
"name": "Early Birds",
"create_date": "2017-06-02T05:38:14.749Z",
"edit_date": "2017-06-02T10:57:44.504Z",
"enable_date": "2017-06-02T10:57:44.497Z",
"disable_date": "2017-06-02T10:57:39.984Z",
"visit_count": 1,
"status": "enabled",
"persistent": false,
"match_any": false
}
}, {
"model": "wagtail_personalisation.personalisablepagemetadata",
"pk": 1,
"fields": {
"canonical_page": 3,
"variant": 3,
"segment": null
}
}, {
"model": "wagtail_personalisation.personalisablepagemetadata",
"pk": 2,
"fields": {
"canonical_page": 3,
"variant": 4,
"segment": 1
}
}, {
"model": "home.homepage",
"pk": 3,
"fields": {
"intro": "<p>Thank you for trying <a href=\"http://wagxperience.io\">Wagxperience</a>!</p>",
"body": "[{\"type\": \"personalisable_paragraph\", \"value\": {\"segment\": \"2\", \"paragraph\": \"<p>You are an early bird!</p>\"}}]"
}
}, {
"model": "home.homepage",
"pk": 4,
"fields": {
"intro": "<p>Thank you for trying <a href=\"http://wagxperience.io\">Wagxperience</a>!</p><p>You've visited the homepage more than 3 times!</p>",
"body": "[]"
}
}, {
"model": "wagtailcore.page",
"pk": 1,
"fields": {
"path": "0001",
"depth": 1,
"numchild": 1,
"title": "Root",
"slug": "root",
"content_type": 1,
"live": true,
"has_unpublished_changes": false,
"url_path": "/",
"owner": null,
"seo_title": "",
"show_in_menus": false,
"search_description": "",
"go_live_at": null,
"expire_at": null,
"expired": false,
"locked": false,
"first_published_at": null,
"latest_revision_created_at": null
}
}, {
"model": "wagtailcore.page",
"pk": 3,
"fields": {
"path": "00010001",
"depth": 2,
"numchild": 0,
"title": "Home",
"slug": "home",
"content_type": 2,
"live": true,
"has_unpublished_changes": false,
"url_path": "/home/",
"owner": null,
"seo_title": "",
"show_in_menus": false,
"search_description": "",
"go_live_at": null,
"expire_at": null,
"expired": false,
"locked": false,
"first_published_at": "2017-06-02T10:35:34.706Z",
"latest_revision_created_at": "2017-06-02T10:35:34.565Z"
}
}, {
"model": "wagtailcore.page",
"pk": 4,
"fields": {
"path": "00010002",
"depth": 2,
"numchild": 0,
"title": "Home (Returning Rook)",
"slug": "home-returning-rook",
"content_type": 2,
"live": true,
"has_unpublished_changes": false,
"url_path": "/home-returning-rook/",
"owner": null,
"seo_title": "",
"show_in_menus": false,
"search_description": "",
"go_live_at": null,
"expire_at": null,
"expired": false,
"locked": false,
"first_published_at": "2017-06-02T05:38:53.568Z",
"latest_revision_created_at": "2017-06-02T05:38:53.390Z"
}
}]

View File

@ -1,19 +0,0 @@
[
{
"model": "user.user",
"pk": 1,
"fields": {
"password": "pbkdf2_sha256$36000$jW4Pr4OWkdVf$1KeQmcYPL1/qZvRX9ECQvoYuXTRbs+tlV480K2AqFUM=",
"last_login": "2017-05-08T07:38:49.391Z",
"is_superuser": true,
"first_name": "S.",
"last_name": "Uper",
"email": "superuser@example.com",
"is_staff": true,
"is_active": true,
"date_joined": "2015-10-17T11:32:57.969Z",
"groups": [],
"user_permissions": []
}
}
]

View File

@ -1,12 +0,0 @@
#!/usr/bin/env python
from __future__ import absolute_import, unicode_literals
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

View File

@ -1,4 +0,0 @@
Django>=2.2,<2.3
wagtail>=2.6,<2.7
django-debug-toolbar==2.0
-e .[docs,test]

View File

@ -1 +0,0 @@

View File

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 16:59
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0033_remove_golive_expiry_help_text'),
('wagtail_personalisation', '0011_personalisablepagemetadata'),
]
operations = [
migrations.CreateModel(
name='HomePage',
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
],
options={
'abstract': False,
},
bases=('wagtailcore.page', models.Model),
),
]

View File

@ -1,59 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def create_homepage(apps, schema_editor):
# Get models
ContentType = apps.get_model('contenttypes.ContentType')
Page = apps.get_model('wagtailcore.Page')
Site = apps.get_model('wagtailcore.Site')
HomePage = apps.get_model('home.HomePage')
# Delete the default homepage
# If migration is run multiple times, it may have already been deleted
Page.objects.filter(id=2).delete()
# Create content type for homepage model
homepage_content_type, __ = ContentType.objects.get_or_create(
model='homepage', app_label='home')
# Create a new homepage
homepage = HomePage.objects.create(
title="Home",
slug='home',
content_type=homepage_content_type,
path='00010001',
depth=2,
numchild=0,
url_path='/home/',
)
# Create a site with the new homepage set as the root
Site.objects.create(
hostname='localhost', root_page=homepage, is_default_site=True)
def remove_homepage(apps, schema_editor):
# Get models
ContentType = apps.get_model('contenttypes.ContentType')
HomePage = apps.get_model('home.HomePage')
# Delete the default homepage
# Page and Site objects CASCADE
HomePage.objects.filter(slug='home', depth=2).delete()
# Delete content type for homepage model
ContentType.objects.filter(model='homepage', app_label='home').delete()
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
]
operations = [
migrations.RunPython(create_homepage, remove_homepage),
]

View File

@ -1,30 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 19:36
from __future__ import unicode_literals
from django.db import migrations
import wagtail.core.fields
import wagtail_personalisation
class Migration(migrations.Migration):
dependencies = [
('home', '0002_create_homepage'),
]
operations = [
migrations.AddField(
model_name='homepage',
name='intro',
field=wagtail.core.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.core.fields.StreamField((('personalisable_paragraph', wagtail.core.blocks.StructBlock((('segment', wagtail.core.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.core.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
preserve_default=False,
),
]

View File

@ -1,23 +0,0 @@
from __future__ import absolute_import, unicode_literals
from wagtail.admin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
from wagtail.core import blocks
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Page
from wagtail_personalisation.models import PersonalisablePageMixin
from wagtail_personalisation.blocks import PersonalisedStructBlock
class HomePage(PersonalisablePageMixin, Page):
intro = RichTextField()
body = StreamField([
('personalisable_paragraph', PersonalisedStructBlock([
('paragraph', blocks.RichTextBlock()),
], icon='pilcrow'))
])
content_panels = Page.content_panels + [
RichTextFieldPanel('intro'),
StreamFieldPanel('body'),
]

View File

@ -1,18 +0,0 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block body_class %}template-homepage{% endblock %}
{% block content %}
<h1>Wagtail Personalisation</h1>
<hr>
<h2>{{ self.title }}</h2>
{{ self.intro|richtext }}
{% for block in page.body %}
<div>{% include_block block %}</div>
{% endfor %}
{% endblock %}

View File

@ -1,38 +0,0 @@
{% extends "base.html" %}
{% load static wagtailcore_tags %}
{% block body_class %}template-searchresults{% endblock %}
{% block title %}Search{% endblock %}
{% block content %}
<h1>Search</h1>
<form action="{% url 'search' %}" method="get">
<input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}>
<input type="submit" value="Search" class="button">
</form>
{% if search_results %}
<ul>
{% for result in search_results %}
<li>
<h4><a href="{% pageurl result %}">{{ result }}</a></h4>
{% if result.search_description %}
{{ result.search_description }}
{% endif %}
</li>
{% endfor %}
</ul>
{% if search_results.has_previous %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.previous_page_number }}">Previous</a>
{% endif %}
{% if search_results.has_next %}
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.next_page_number }}">Next</a>
{% endif %}
{% elif search_query %}
No results found
{% endif %}
{% endblock %}

View File

@ -1,36 +0,0 @@
from __future__ import absolute_import, unicode_literals
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import render
from wagtail.core.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
if search_query:
search_results = Page.objects.live().search(search_query)
query = Query.get(search_query)
# Record hit
query.add_hit()
else:
search_results = Page.objects.none()
# Pagination
paginator = Paginator(search_results, 10)
try:
search_results = paginator.page(page)
except PageNotAnInteger:
search_results = paginator.page(1)
except EmptyPage:
search_results = paginator.page(paginator.num_pages)
return render(request, 'search/search.html', {
'search_query': search_query,
'search_results': search_results,
})

View File

@ -1,42 +0,0 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from sandbox.apps.user import forms, models
@admin.register(models.User)
class UserAdmin(BaseUserAdmin):
form = forms.UserChangeForm
add_form = forms.UserCreationForm
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ['email']
list_filter = ['is_superuser']
fieldsets = (
(None, {
'fields': ['email', 'password']
}),
('Personal info', {
'fields': ['first_name', 'last_name']
}),
('Permissions', {
'fields': [
'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions'
]
}),
)
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ['email', 'password1', 'password2']
}),
)
search_fields = ['first_name', 'last_name', 'email']
ordering = ['email']
filter_horizontal = []

View File

@ -1,63 +0,0 @@
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.utils.translation import ugettext_lazy as _
from sandbox.apps.user import models
class UserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password.
"""
password1 = forms.CharField(
label='Password', widget=forms.PasswordInput,
required=False)
password2 = forms.CharField(
label='Password confirmation', widget=forms.PasswordInput,
required=False)
class Meta:
model = models.User
fields = ['email']
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data['password1'])
if commit:
user.save()
return user
class UserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
password hash display field.
"""
password = ReadOnlyPasswordHashField(
label=_("Password"),
help_text=_("Raw passwords are not stored, so there is no way to see "
"this user's password, but you can change the password "
"using <a href=\"password/\">this form</a>."))
class Meta:
model = models.User
fields = [
'email', 'password', 'is_active', 'is_superuser'
]
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial['password']

View File

@ -1,43 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-05-31 12:22
from __future__ import unicode_literals
import django.contrib.auth.models
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=100, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=100, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -1,20 +0,0 @@
# 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()),
],
),
]

View File

@ -1,81 +0,0 @@
from django.contrib.auth.models import (
AbstractBaseUser, PermissionsMixin, BaseUserManager)
from django.core.mail import send_mail
from django.db import models
from django.utils import timezone
from django.utils.translation import ugettext_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):
"""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)
is_staff = models.BooleanField(
_('staff status'), default=False,
help_text=_('Designates whether the user can log into this admin '
'site.'))
is_active = models.BooleanField(
_('active'), default=True,
help_text=_('Designates whether this user should be treated as '
'active. Unselect this instead of deleting accounts.'))
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
objects = UserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
def get_full_name(self):
"""
Returns the first_name plus the last_name, with a space in between.
"""
full_name = '%s %s' % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):
"Returns the short name for the user."
return self.first_name
def email_user(self, subject, message, from_email=None, **kwargs):
"""
Sends an email to this User.
"""
send_mail(subject, message, from_email, [self.email], **kwargs)

View File

@ -1,168 +0,0 @@
"""
Django settings for sandbox project.
Generated by 'django-admin startproject' using Django 1.11.1.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.11/ref/settings/
"""
from __future__ import absolute_import, unicode_literals
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
from importlib.util import find_spec
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(PROJECT_DIR)
DEBUG = True
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '^anfvx$i7%wts8j=7u1h5ua$w6c76*333(@h)rrjlak1c&x0r+'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
SITE_ID = 1
# Application definition
INSTALLED_APPS = [
'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.core',
'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.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',
]
if find_spec('wagtail.contrib.legacy'):
MIDDLEWARE += ('wagtail.contrib.legacy.sitemiddleware.SiteMiddleware',)
else:
MIDDLEWARE += ('wagtail.core.middleware.SiteMiddleware', )
ROOT_URLCONF = 'sandbox.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(PROJECT_DIR, 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'sandbox.wsgi.application'
AUTH_USER_MODEL = 'user.User'
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
]
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
# Wagtail settings
WAGTAIL_SITE_NAME = "sandbox"
# Base URL to use when referring to full URLs within the Wagtail admin backend -
# e.g. in notification emails. Don't include '/admin' or a trailing slash
BASE_URL = 'http://example.com'
INTERNAL_IPS = ['127.0.0.1']

View File

@ -1,9 +0,0 @@
{% extends "base.html" %}
{% block body_class %}template-404{% endblock %}
{% block content %}
<h1>Page not found</h1>
<h2>Sorry, this page could not be found.</h2>
{% endblock %}

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>Internal server error</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<h1>Internal server error</h1>
<h2>Sorry, there seems to be an error. Please try again soon.</h2>
</body>
</html>

View File

@ -1,44 +0,0 @@
{% load static wagtailuserbar %}
<!DOCTYPE html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>
{% block title %}
{% if self.seo_title %}{{ self.seo_title }}{% else %}{{ self.title }}{% endif %}
{% endblock %}
{% block title_suffix %}
{% with self.get_site.site_name as site_name %}
{% if site_name %}- {{ site_name }}{% endif %}
{% endwith %}
{% endblock %}
</title>
<meta name="description" content="" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
{# Global stylesheets #}
<link rel="stylesheet" type="text/css" href="{% static 'css/sandbox.css' %}">
{% block extra_css %}
{# Override this in templates to add extra stylesheets #}
{% endblock %}
</head>
<body class="{% block body_class %}{% endblock %}">
{% wagtailuserbar %}
{% block content %}{% endblock %}
{# Global javascript #}
<script type="text/javascript" src="{% static 'js/sandbox.js' %}"></script>
{% block extra_js %}
{# Override this in templates to add extra javascript #}
{% endblock %}
</body>
</html>

View File

@ -1,42 +0,0 @@
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.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from sandbox.apps.search import views as search_views
urlpatterns = [
url(r'^django-admin/', admin.site.urls),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
url(r'^search/$', search_views.search, name='search'),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's page serving mechanism. This should be the last pattern in
# the list:
url(r'', include(wagtail_urls)),
# Alternatively, if you want Wagtail pages to be served from a subpath
# of your site, rather than the site root:
# url(r'^pages/', include(wagtail_urls)),
]
if settings.DEBUG:
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
# Serve static and media files from development server
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

View File

@ -1,18 +0,0 @@
"""
WSGI config for sandbox project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
"""
from __future__ import absolute_import, unicode_literals
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "sandbox.settings")
application = get_wsgi_application()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

View File

@ -1,30 +1,8 @@
[bumpversion]
current_version = 0.15.3
commit = true
tag = true
tag_name = {new_version}
[tool:pytest]
DJANGO_SETTINGS_MODULE = tests.settings
minversion = 3.0
strict = true
django_find_project = false
testpaths = tests
python_paths = .
DJANGO_SETTINGS_MODULE = tests.sandbox.settings
norecursedirs = .tox .git
[flake8]
ignore = E731
max-line-length = 120
exclude =
src/**/migrations/*.py
[wheel]
universal = 1
[coverage]
include = src/**/
omit = src/**/migrations/*.py
[bumpversion:file:setup.py]
[bumpversion:file:docs/conf.py]
ignore=E731
exclude=
src/**/migrations/*.py

View File

@ -1,66 +1,50 @@
import re
from setuptools import find_packages, setup
install_requires = [
'wagtail>=2.0',
'user-agents>=1.1.0',
'wagtailfontawesome>=1.1.3',
'pycountry',
'django-polymorphic==1.0.2',
'wagtail>=1.7',
]
tests_require = [
'factory_boy==2.8.1',
'flake8-blind-except',
'flake8-debugger',
'flake8-isort',
'flake8',
'pytest==3.0.4',
'pytest-cov==2.4.0',
'pytest-django==3.0.0',
'pytest-sugar==0.7.1',
'freezegun==0.3.8',
'pytest-cov==2.5.1',
'pytest-django==4.1.0',
'pytest-pythonpath==0.7.2',
'pytest-sugar==0.9.1',
'pytest==6.1.2',
'wagtail_factories==1.1.0',
'pytest-mock==1.6.3',
'factory_boy==2.7.0',
]
docs_require = [
'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)
setup(
name='wagtail-personalisation',
version='0.15.3',
version='0.1.0',
description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV and others',
author_email='opensource@labdigital.nl',
url='https://labdigital.nl/',
author='Lab Digital BV',
author_email='b.besemer@labdigital.nl',
url='http://labdigital.nl',
install_requires=install_requires,
tests_require=tests_require,
extras_require={
'docs': docs_require,
'test': tests_require,
},
packages=find_packages('src'),
package_dir={'': 'src'},
include_package_data=True,
license='MIT',
long_description=long_description,
license='BSD',
long_description=open('README.rst').read(),
classifiers=[
'Development Status :: 5 - Production/Stable',
'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Framework :: Django',
'Framework :: Django :: 2.0',
'Framework :: Django :: 1.8',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Topic :: Internet :: WWW/HTTP :: Site Management',
],
)

View File

@ -0,0 +1,40 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import admin
from personalisation import models
class TimeRuleAdminInline(admin.TabularInline):
"""Inline the Time Rule into the administration interface for segments"""
model = models.TimeRule
extra = 0
class ReferralRuleAdminInline(admin.TabularInline):
"""Inline the Referral Rule into the
administration interface for segments"""
model = models.ReferralRule
extra = 0
class VisitCountRuleAdminInline(admin.TabularInline):
"""Inline the Visit Count Rule into the
administration interface for segments"""
model = models.VisitCountRule
extra = 0
class CloudfrontDeviceTypeRuleAdminInline(admin.TabularInline):
"""Inline the Cloudfront DeviceType rule into the
administration interface for segments"""
model = models.CloudfrontDeviceTypeRule
extra = 0
class SegmentAdmin(admin.ModelAdmin):
"""Add the inlines to the Segment admin interface"""
inlines = (TimeRuleAdminInline, CloudfrontDeviceTypeRuleAdminInline,
ReferralRuleAdminInline, VisitCountRuleAdminInline)
admin.site.register(models.Segment, SegmentAdmin)

View File

@ -0,0 +1,15 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from personalisation import views
app_name = 'segment'
urlpatterns = [
url(r'^segment/(?P<segment_id>[0-9]+)/enable/$', views.enable,
name='enable'),
url(r'^segment/(?P<segment_id>[0-9]+)/disable/$', views.disable,
name='disable'),
url(r'^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$',
views.copy_page_view, name='copy_page')
]

View File

@ -12,7 +12,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0001_initial'),
('wagtailcore', '0030_index_on_pagerevision_created_at'),
]
operations = [
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
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')),
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variations', to='personalisation.PersonalisablePage')),
],
options={
'abstract': False,
@ -60,7 +60,7 @@ class Migration(migrations.Migration):
('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')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_timerule_related', related_query_name='personalisation_timerules', to='personalisation.Segment')),
],
options={
'abstract': False,
@ -73,7 +73,7 @@ class Migration(migrations.Migration):
('operator', models.CharField(choices=[('more_than', 'More than'), ('less_than', 'Less than'), ('equal_to', 'Equal to')], default='ht', max_length=20)),
('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')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_visitcountrule_related', related_query_name='personalisation_visitcountrules', to='personalisation.Segment')),
],
options={
'abstract': False,
@ -82,11 +82,11 @@ class Migration(migrations.Migration):
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'),
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_referralrule_related', related_query_name='personalisation_referralrules', to='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'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='segments', to='personalisation.Segment'),
),
]

View File

@ -10,7 +10,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0001_initial'),
('personalisation', '0001_initial'),
]
operations = [
@ -20,7 +20,7 @@ class Migration(migrations.Migration):
('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')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_queryrule_related', related_query_name='personalisation_queryrules', to='personalisation.Segment')),
],
options={
'abstract': False,

View File

@ -8,7 +8,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0002_auto_20161205_1623'),
('personalisation', '0002_auto_20161205_1623'),
]
operations = [

View File

@ -8,7 +8,7 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0003_auto_20161206_1005'),
('personalisation', '0003_auto_20161206_1005'),
]
operations = [

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-12-12 18:45
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
class Migration(migrations.Migration):
dependencies = [
('personalisation', '0004_segment_persistent'),
]
operations = [
migrations.CreateModel(
name='CloudfrontDeviceTypeRule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_tablet', models.BooleanField(default=False)),
('is_smartphone', models.BooleanField(default=False)),
('is_desktop', models.BooleanField(default=False)),
('is_smarttv', models.BooleanField(default=False)),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='personalisation_cloudfrontdevicetyperule_related', related_query_name='personalisation_cloudfrontdevicetyperules', to='personalisation.Segment')),
],
options={
'abstract': False,
},
),
]

View File

@ -0,0 +1,379 @@
from __future__ import absolute_import, unicode_literals
import re
from datetime import datetime
from django.db import models
from django.db.models.signals import pre_save
from django.template.defaultfilters import slugify
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from modelcluster.fields import ParentalKey
from modelcluster.models import ClusterableModel
from wagtail.utils.decorators import cached_classmethod
from wagtail.wagtailadmin.edit_handlers import (
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel, ObjectList,
PageChooserPanel, TabbedInterface)
from wagtail.wagtailadmin.forms import WagtailAdminPageForm
from wagtail.wagtailcore.models import Page
@python_2_unicode_compatible
class AbstractBaseRule(models.Model):
"""Base for creating rules to segment users with"""
segment = ParentalKey(
'personalisation.Segment',
related_name="%(app_label)s_%(class)s_related",
related_query_name="%(app_label)s_%(class)ss"
)
def test_user(self):
"""Test if the user matches this rule"""
return True
def __str__(self):
return "Abstract segmentation rule"
class Meta:
abstract = True
@python_2_unicode_compatible
class TimeRule(AbstractBaseRule):
"""Time rule to segment users based on a start and end time"""
start_time = models.TimeField(_("Starting time"))
end_time = models.TimeField(_("Ending time"))
panels = [
FieldRowPanel([
FieldPanel('start_time'),
FieldPanel('end_time'),
]),
]
def __init__(self, *args, **kwargs):
super(TimeRule, self).__init__(*args, **kwargs)
def test_user(self, request=None):
current_time = datetime.now().time()
starting_time = self.start_time
ending_time = self.end_time
return starting_time <= current_time <= ending_time
def __str__(self):
return 'Time Rule'
@python_2_unicode_compatible
class ReferralRule(AbstractBaseRule):
"""Referral rule to segment users based on a regex test"""
regex_string = models.TextField(
_("Regex string to match the referer with"))
panels = [
FieldPanel('regex_string'),
]
def __init__(self, *args, **kwargs):
super(ReferralRule, self).__init__(*args, **kwargs)
def test_user(self, request):
pattern = re.compile(self.regex_string)
if 'HTTP_REFERER' in request.META:
referer = request.META['HTTP_REFERER']
if pattern.search(referer):
return True
return False
def __str__(self):
return 'Referral Rule'
@python_2_unicode_compatible
class CloudfrontDeviceTypeRule(AbstractBaseRule):
"""Referral rule to segment users based on a their device type as it was
detected by Cloudfront"""
is_tablet = models.BooleanField(default=False)
is_smartphone = models.BooleanField(default=False)
is_desktop = models.BooleanField(default=False)
is_smarttv = models.BooleanField(default=False)
panels = [
FieldPanel('is_tablet'),
FieldPanel('is_smartphone'),
FieldPanel('is_desktop'),
FieldPanel('is_smarttv'),
]
def __init__(self, *args, **kwargs):
super(CloudfrontDeviceTypeRule, self).__init__(*args, **kwargs)
def test_user(self, request):
"""test different cloudfront headers. If those are not available,
False will be returned"""
return (
self.is_smartphone == self._header_value(request,
'HTTP_CLOUDFRONT_IS_MOBILE_VIEWER')
or self.is_tablet == self._header_value(request,
'HTTP_CLOUDFRONT_IS_TABLET_VIEWER')
or self.is_desktop == self._header_value(request,
'HTTP_CLOUDFRONT_IS_DESKTOP_VIEWER')
or self.is_smarttv == self._header_value(request,
'HTTP_CLOUDFRONT_IS_SMARTTV_VIEWER')
)
def _header_value(self, request, header):
header_value = request.META.get(header, None),
if None not in header_value:
return True if 'true' in header_value else False
return None
def __str__(self):
return 'Cloudfront Device Type Rule'
@python_2_unicode_compatible
class VisitCountRule(AbstractBaseRule):
"""Visit count rule to segment users based on amount of visits"""
OPERATOR_CHOICES = (
('more_than', _("More than")),
('less_than', _("Less than")),
('equal_to', _("Equal to")),
)
operator = models.CharField(max_length=20,
choices=OPERATOR_CHOICES, default="more_than")
count = models.PositiveSmallIntegerField(default=0, null=True)
counted_page = models.ForeignKey(
'wagtailcore.Page',
null=False,
blank=False,
on_delete=models.CASCADE,
related_name='+',
)
panels = [
PageChooserPanel('counted_page'),
FieldRowPanel([
FieldPanel('operator'),
FieldPanel('count'),
]),
]
def __init__(self, *args, **kwargs):
super(VisitCountRule, self).__init__(*args, **kwargs)
def test_user(self, request):
operator = self.operator
segment_count = self.count
def get_visit_count(request):
"""Search through the sessions to get the page visit count
corresponding to the request."""
for page in request.session['visit_count']:
if page['path'] == request.path:
return page['count']
visit_count = get_visit_count(request)
if visit_count and operator == "more_than":
if visit_count > segment_count:
return True
elif visit_count and operator == "less_than":
if visit_count < segment_count:
return True
elif visit_count and operator == "equal_to":
if visit_count == segment_count:
return True
return False
def __str__(self):
return 'Visit count Rule'
@python_2_unicode_compatible
class QueryRule(AbstractBaseRule):
"""Query rule to segment users based on matching queries"""
parameter = models.SlugField(_("The query parameter to search for"),
max_length=20)
value = models.SlugField(_("The value of the parameter to match"),
max_length=20)
panels = [
FieldPanel('parameter'),
FieldPanel('value'),
]
def __init__(self, *args, **kwargs):
super(QueryRule, self).__init__(*args, **kwargs)
def test_user(self, request):
parameter = self.parameter
value = self.value
req_value = request.GET.get(parameter, '')
if req_value == value:
return True
return False
def __str__(self):
return 'Query Rule'
@python_2_unicode_compatible
class Segment(ClusterableModel):
"""Model for a new segment"""
name = models.CharField(max_length=255)
create_date = models.DateTimeField(auto_now_add=True)
edit_date = models.DateTimeField(auto_now=True)
enable_date = models.DateTimeField(null=True, editable=False)
disable_date = models.DateTimeField(null=True, editable=False)
visit_count = models.PositiveIntegerField(default=0, editable=False)
STATUS_CHOICES = (
('enabled', 'Enabled'),
('disabled', 'Disabled'),
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES,
default="enabled")
persistent = models.BooleanField(
default=False, help_text=_("Should the segment persist between visits?"))
panels = [
MultiFieldPanel([
FieldPanel('name', classname="title"),
FieldRowPanel([
FieldPanel('status'),
FieldPanel('persistent'),
]),
], heading="Segment"),
MultiFieldPanel([
InlinePanel(
"{}_related".format(rule._meta.db_table),
label=rule.__str__,
min_num=0,
max_num=1,
) for rule in AbstractBaseRule.__subclasses__()
], heading="Rules"),
]
def __str__(self):
return self.name
def encoded_name(self):
"""Returns a string with a slug for the segment"""
return slugify(self.name.lower())
def check_status_change(sender, instance, *args, **kwargs):
"""Check if the status has changed. Alter dates accordingly."""
try:
original_status = sender.objects.get(pk=instance.id).status
except sender.DoesNotExist:
original_status = ""
if original_status != instance.status:
if instance.status == "enabled":
instance.enable_date = timezone.now()
instance.visit_count = 0
return instance
if instance.status == "disabled":
instance.disable_date = timezone.now()
pre_save.connect(check_status_change, sender=Segment)
class AdminPersonalisablePageForm(WagtailAdminPageForm):
def __init__(self, *args, **kwargs):
super(AdminPersonalisablePageForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
page = super(AdminPersonalisablePageForm, self).save(commit=False)
if page.segment:
segment = page.segment
slug = "{}-{}".format(page.slug, segment.encoded_name())
title = "{} ({})".format(page.title, segment.name)
update_attrs = {
'title': title,
'slug': slug,
'segment': segment,
'live': False,
'canonical_page': page,
'is_segmented': True,
}
if page.is_segmented:
slug = "{}-{}".format(
page.canonical_page.slug, segment.encoded_name())
title = "{} ({})".format(
page.canonical_page.title, segment.name)
page.slug = slug
page.title = title
page.save()
return page
else:
new_page = page.copy(
update_attrs=update_attrs, copy_revisions=False)
return new_page
return page
class PersonalisablePage(Page):
canonical_page = models.ForeignKey(
'self', related_name='variations', on_delete=models.SET_NULL,
blank=True, null=True
)
segment = models.ForeignKey(
Segment, related_name='segments', on_delete=models.PROTECT,
blank=True, null=True
)
is_segmented = models.BooleanField(default=False)
variation_panels = [
MultiFieldPanel([
FieldPanel('segment'),
PageChooserPanel('canonical_page', page_type=None),
])
]
base_form_class = AdminPersonalisablePageForm
def __str__(self):
return "{}".format(self.title)
@cached_property
def has_variations(self):
return self.variations.exists()
@cached_property
def is_canonical(self):
return not self.canonical_page and self.has_variations
@cached_classmethod
def get_edit_handler(cls):
tabs = []
if cls.content_panels:
tabs.append(ObjectList(cls.content_panels, heading=_("Content")))
if cls.variation_panels:
tabs.append(ObjectList(cls.variation_panels, heading=_("Variations")))
if cls.promote_panels:
tabs.append(ObjectList(cls.promote_panels, heading=_("Promote")))
if cls.settings_panels:
tabs.append(ObjectList(cls.settings_panels, heading=_("Settings"),
classname='settings'))
edit_handler = TabbedInterface(tabs, base_form_class=cls.base_form_class)
return edit_handler.bind_to_model(cls)
PersonalisablePage.get_edit_handler = get_edit_handler

View File

Before

Width:  |  Height:  |  Size: 794 B

After

Width:  |  Height:  |  Size: 794 B

View File

@ -25,11 +25,6 @@
cursor: pointer;
}
.block_container .block--disabled h2,
.block_container .block--disabled .inspect_container {
opacity: 0.5;
}
.block_container .block h2 {
display: inline-block;
width: auto;
@ -86,16 +81,11 @@
padding: 0;
margin: 0;
list-style: none;
.stat_card {
display: inline-block;
margin-bottom: 5px;
margin-right: 10px;
}
}
.block_container .block span.icon::before {
margin-right: 0.3em;
vertical-align: bottom;
.block_container .block .inspect_container .inspect li {
display: inline-block;
margin-bottom: 5px;
}
.block_container .block .inspect_container .inspect li span {
@ -106,6 +96,35 @@
overflow-wrap: break-word;
}
.block_container .block .inspect_container .inspect li span::before {
display: inline-block;
content: "";
width: 16px;
height: 16px;
margin-right: 5px;
background-size: contain;
}
.block_container .block .inspect_container .segment_stats .visit_stat span::before {
background-image: url("./rocket_icon.png");
}
.block_container .block .inspect_container .segment_stats .days_stat span::before {
background-image: url("./calendar_icon.png");
}
.block_container .block .inspect_container .segment_rules .persistent_state span::before {
background-image: url("./persistent_icon.png");
}
.block_container .block .inspect_container .segment_rules .persistent_state.fleeting span::before {
transform: rotate(45deg) translateY(-2px);
}
.block_container .block .inspect_container .segment_rules .time_rule span::before {
background-image: url("./time_icon.png");
}
.block_container .block .inspect_container .segment_rules .visit_count_rule span::before {
background-image: url("./visit_count_icon.png");
}
.block_container .block .inspect_container .inspect li pre {
position: relative;
box-sizing: border-box;
@ -119,6 +138,25 @@
border-radius: 3px;
}
.block_container .block .inspect_container .inspect li pre::before {
display: inline-block;
position: absolute;
content: "";
left: -21px;
top: 6px;
width: 16px;
height: 16px;
margin-right: 5px;
background-size: contain;
}
.block_container .block .inspect_container .segment_rules .referral_rule pre::before {
background-image: url("./referral_icon.png");
}
.block_container .block .inspect_container .segment_rules .query_rule pre::before {
background-image: url("./referral_icon.png");
}
.block_container .block.suggestion .suggestive_text {
display: block;
position: absolute;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,117 @@
{% extends "modeladmin/index.html" %}
{% load i18n l10n staticfiles modeladmin_tags personalisation_filters %}
{% block content_main %}
<div>
<div class="row">
{% block content_cols %}
{% block filters %}
{% if view.has_filters and all_count %}
<div class="changelist-filter col3">
<h2>{% trans 'Filter' %}</h2>
{% for spec in view.filter_specs %}{% admin_list_filter view spec %}{% endfor %}
</div>
{% endif %}
{% endblock %}
<div>
{% block result_list %}
<div class="nice-padding block_container">
{% if all_count %}
{% for segment in object_list %}
<div class="block" onclick="location.href = 'edit/{{ segment.pk }}'">
<h2>{{ segment }}</h2>
<div class="inspect_container">
<ul class="inspect segment_stats">
<li class="visit_stat">
{% trans "This segmented has been visited" %}
<span>{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
</li>
<li class="days_stat">
{% trans "This segment has been active for" %}
<span>{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
</li>
</ul>
<ul class="inspect segment_rules">
{% for rule in segment.personalisation_timerule_related.all %}
<li class="time_rule">
These users visit between
<span>{{ rule.start_time }} and {{ rule.end_time }}</span>
</li>
{% endfor %}
{% for rule in segment.personalisation_referralrule_related.all %}
<li class="referral_rule">
These visits originate from
<pre>{{ rule.regex_string }}</pre>
</li>
{% endfor %}
{% for rule in segment.personalisation_visitcountrule_related.all %}
<li class="visit_count_rule">
These users visited {{ rule.counted_page }}
<span>{{ rule.get_operator_display }} {{ rule.count }} times</span>
</li>
{% endfor %}
{% for rule in segment.personalisation_queryrule_related.all %}
<li class="query_rule">
These users used a url with the query
<pre>?{{ rule.parameter }}={{ rule.value }}</pre>
</li>
{% endfor %}
<li class="persistent_state {{ segment.persistent|yesno:"persistent,fleeting" }}">
{% trans "The persistence of this segment is" %}
{% if segment.persistent %}
<span title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
{% else %}
<span title="{% trans "This segment is reevaluated on every visit" %}">{% trans "Fleeting" %}</span>
{% endif %}
</li>
</ul>
</div>
{% if user_can_create %}
<ul class="block_actions">
{% if segment.status == "disabled" %}
<li><a href="{% url 'segment:enable' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
{% elif segment.status == "enabled" %}
<li><a href="{% url 'segment:disable' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
{% endif %}
<li><a href="edit/{{ segment.pk }}" title="{% trans "Configure this segment" %}">configure this</a></li>
</ul>
{% endif %}
</div>
{% endfor %}
{% endif %}
{% if user_can_create %}
{% blocktrans with url=view.create_url name=view.verbose_name %}
<a class="block suggestion" href="{{ url }}">
<span class="suggestive_text">Add a new {{name}}</span>
</a>
{% endblocktrans %}
{% endif %}
</div>
{% endblock %}
</div>
{% block pagination %}
{% if paginator.num_pages > 1 %}
<div class="pagination {% if view.has_filters and all_count %}col9{% else %}col12{% endif %}">
<p>{% blocktrans with page_obj.number as current_page and paginator.num_pages as num_pages %}Page {{ current_page }} of {{ num_pages }}.{% endblocktrans %}</p>
<ul>
{% pagination_link_previous page_obj view %}
{% pagination_link_next page_obj view %}
</ul>
</div>
{% endif %}
{% endblock %}
{% endblock %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,19 @@
from django.template import Library
from django.utils import timezone
register = Library()
@register.filter(name='days_since')
def active_days(enable_date, disable_date):
"""Returns the number of days the segment has been active"""
if enable_date is not None:
if disable_date is None or disable_date <= enable_date:
# There is no disable date, or it is not relevant.
delta = timezone.now() - enable_date
return delta.days
if disable_date > enable_date:
# There is a disable date and it is relevant.
delta = disable_date - enable_date
return delta.days
return 0

View File

@ -0,0 +1,5 @@
def impersonate_other_page(page, other_page):
page.path = other_page.path
page.depth = other_page.depth
page.url_path = other_page.url_path
page.title = other_page.title

View File

@ -0,0 +1,46 @@
from __future__ import absolute_import, unicode_literals
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, reverse
from personalisation.models import PersonalisablePage, Segment
def enable(request, segment_id):
"""Enable the selected segment"""
segment = get_object_or_404(Segment, pk=segment_id)
segment.status = 'enabled'
segment.save()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
def disable(request, segment_id):
"""Disable the selected segment"""
segment = get_object_or_404(Segment, pk=segment_id)
segment.status = 'disabled'
segment.save()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
def copy_page_view(request, page_id, segment_id):
"""Copy page with selected segment"""
segment = get_object_or_404(Segment, pk=segment_id)
page = get_object_or_404(PersonalisablePage, pk=page_id)
slug = "{}-{}".format(page.slug, segment.encoded_name())
title = "{} ({})".format(page.title, segment.name)
update_attrs = {
'title': title,
'slug': slug,
'segment': segment,
'live': False,
'canonical_page': page,
'is_segmented': True,
}
new_page = page.copy(update_attrs=update_attrs, copy_revisions=False)
edit_url = reverse('wagtailadmin_pages:edit', args=[new_page.id])
return HttpResponseRedirect(edit_url)

View File

@ -0,0 +1,228 @@
from __future__ import absolute_import, unicode_literals
import logging
import time
from django.conf.urls import include, url
from django.shortcuts import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.wagtailadmin.widgets import (
Button, ButtonWithDropdownFromHook)
from wagtail.wagtailcore import hooks
from personalisation import admin_urls
from personalisation.models import (AbstractBaseRule, PersonalisablePage,
Segment)
from personalisation.utils import impersonate_other_page
logger = logging.getLogger()
@hooks.register('register_admin_urls')
def register_admin_urls():
"""Adds the administration urls for the personalisation apps."""
return [
url(r'^personalisation/', include(
admin_urls,
app_name='personalisation',
namespace='personalisation')),
]
class SegmentModelAdmin(ModelAdmin):
"""The base model for the Segments administration interface."""
model = Segment
menu_icon = 'group'
add_to_settings_menu = False
list_display = ('status', 'name', 'create_date', 'edit_date')
index_view_extra_css = ['personalisation/segment/index.css']
form_view_extra_css = ['personalisation/segment/form.css']
modeladmin_register(SegmentModelAdmin)
@hooks.register('before_serve_page')
def set_visit_count(page, request, serve_args, serve_kwargs):
if 'visit_count' not in request.session:
request.session['visit_count'] = []
# Update the page visit count
def create_new_counter(page, request):
"""Create a new counter dict and place it in session storage."""
countdict = {
"slug": page.slug,
"id": page.pk,
"path": request.path,
"count": 1,
}
request.session['visit_count'].append(countdict)
if len(request.session['visit_count']) > 0:
for index, counter in enumerate(request.session['visit_count']):
if counter['id'] == page.pk:
# Counter already exists. Increase the count value by 1.
newcount = counter['count'] + 1
request.session['visit_count'][index]['count'] = newcount
request.session.modified = True
else:
# Counter doesn't exist.
# Create a new counter with count value 1.
create_new_counter(page, request)
else:
# No counters exist. Create a new counter with count value 1.
create_new_counter(page, request)
@hooks.register('before_serve_page')
def segment_user(page, request, serve_args, serve_kwargs):
if 'segments' not in request.session:
request.session['segments'] = []
current_segments = request.session['segments']
persistent_segments = Segment.objects.filter(persistent=True)
current_segments = [item for item in current_segments if any(seg.pk for seg in persistent_segments) == item['id']]
request.session['segments'] = current_segments
segments = Segment.objects.all().filter(status='enabled')
for segment in segments:
rules = AbstractBaseRule.__subclasses__()
segment_rules = []
for rule in rules:
queried_rules = rule.objects.filter(segment=segment)
for result in queried_rules:
segment_rules.append(result)
result = _test_rules(segment_rules, request)
if result:
_add_segment_to_user(segment, request)
if request.session['segments']:
logger.info("User has been added to the following segments: {}"
.format(request.session['segments']))
for seg in request.session['segments']:
segment = Segment.objects.get(pk=seg['id'])
segment.visit_count = segment.visit_count + 1
segment.save()
def _test_rules(rules, request):
"""Test whether the user matches a segment's rules'"""
if len(rules) > 0:
for rule in rules:
result = rule.test_user(request)
if result is False:
return False
return True
return False
def _add_segment_to_user(segment, request):
"""Save the segment in the user session"""
def check_if_segmented(segment):
"""Check if the user has been segmented"""
for seg in request.session['segments']:
if seg['encoded_name'] == segment.encoded_name():
return True
return False
if not check_if_segmented(segment):
segdict = {
"encoded_name": segment.encoded_name(),
"id": segment.pk,
"timestamp": int(time.time()),
"persistent": segment.persistent,
}
request.session['segments'].append(segdict)
@hooks.register('before_serve_page')
def serve_variation(page, request, serve_args, serve_kwargs):
user_segments = []
for segment in request.session['segments']:
try:
user_segment = Segment.objects.get(pk=segment['id'],
status='enabled')
except Segment.DoesNotExist:
user_segment = None
if user_segment:
user_segments.append(user_segment)
if len(user_segments) > 0:
variations = _check_for_variations(user_segments, page)
if variations:
variation = variations[0]
impersonate_other_page(variation, page)
return variation.serve(request, *serve_args, **serve_kwargs)
def _check_for_variations(segments, page):
for segment in segments:
page_class = page.__class__
if not any(item == PersonalisablePage for item in page_class.__bases__):
page_class = PersonalisablePage
variation = page_class.objects.filter(
canonical_page=page, segment=segment)
if variation:
return variation
return None
@hooks.register('register_page_listing_buttons')
def page_listing_variant_buttons(page, page_perms, is_parent=False):
personalisable_page = PersonalisablePage.objects.filter(pk=page.pk)
segments = Segment.objects.all()
if personalisable_page and len(segments) > 0 and not (any(item.segment for item in personalisable_page)):
yield ButtonWithDropdownFromHook(
_('Variants'),
hook_name='register_page_listing_variant_buttons',
page=page,
page_perms=page_perms,
is_parent=is_parent,
attrs={'target': '_blank', 'title': _('Create a new variant')}, priority=100)
@hooks.register('register_page_listing_variant_buttons')
def page_listing_more_buttons(page, page_perms, is_parent=False):
segments = Segment.objects.all()
available_segments = [item for item in segments if not PersonalisablePage.objects.filter(segment=item, pk=page.pk)]
for segment in available_segments:
yield Button(segment.name,
reverse('segment:copy_page', args=[page.id, segment.id]),
attrs={"title": _('Create this variant')})
class SegmentSummaryPanel(object):
order = 500
def render(self):
segment_count = Segment.objects.count()
target_url = reverse('personalisation_segment_modeladmin_index')
title = _("Segments")
return mark_safe("""
<li class="icon icon-group">
<a href="{}"><span>{}</span>{}</a>
</li>""".format(target_url, segment_count, title))
@hooks.register('construct_homepage_summary_items')
def add_segment_summary_panel(request, summary_items):
return summary_items.append(SegmentSummaryPanel())

View File

@ -1 +0,0 @@
default_app_config = 'wagtail_personalisation.config.WagtailPersonalisationConfig'

Some files were not shown because too many files have changed in this diff Show More