Compare commits
2 Commits
0.11.1
...
feature/de
Author | SHA1 | Date | |
---|---|---|---|
808cc7d9ed | |||
7c52d318ff |
@ -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
|
10
.gitignore
vendored
@ -3,23 +3,19 @@
|
||||
*.swo
|
||||
*.python-version
|
||||
*.coverage
|
||||
.coverage.*
|
||||
|
||||
*.egg-info/
|
||||
|
||||
.cache/
|
||||
.idea/
|
||||
.tox/
|
||||
.vscode/
|
||||
|
||||
build/
|
||||
dist/
|
||||
tests/sandbox/assets
|
||||
htmlcov/
|
||||
docs/_build
|
||||
|
||||
coverage.xml
|
||||
db.sqlite3
|
||||
|
||||
tests/sandbox/assets
|
||||
node_modules
|
||||
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
|
31
.travis.yml
@ -1,31 +0,0 @@
|
||||
---
|
||||
sudo: false
|
||||
language: python
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
env: lint
|
||||
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django111-wagtail113
|
||||
|
||||
install:
|
||||
- pip install tox codecov
|
||||
|
||||
script:
|
||||
- tox
|
||||
|
||||
after_success:
|
||||
- tox -e coverage-report
|
||||
- codecov
|
||||
|
||||
deploy:
|
||||
provider: pypi
|
||||
distributions: sdist bdist_wheel
|
||||
user: praekelt.org
|
||||
password:
|
||||
secure: IxPnc95OFNQsl7kFfLlLc38KfHh/W79VXnhEkdb2x1GZznOsG167QlhpAnyXIJysCQxgMMwLMuPOOdk1WIxOpGNM1/M80hNzPAfxMYWPuwposDdwoIc8SyREPJt16BXWAY+rAH8SHxld9p1YdRbOEPSSglODe4dCmQWsCbKjV3aKv+gZxBvf6pMxUglp2fBIlCwrE77MyMYh9iW0AGzD6atC2xN9NgAm4f+2WFlKCUL/MztLhNWdvWEiibQav6Tts7p8tWrsBVzywDW2IOy3O0ihPgRdISZ7QrxhiJTjlHYPAy4eRGOnYSQXvp6Dy8ONE5a6Uv5g3Xw2UmQo85sSMUs2VxR0G7d+PQgL0A7ENBQ5i7eSAFHYs8EswiGilW2A7mM4qRXwg9URLelYSdkM+aNXvR+25dCsXakiO4NjCz/BPgNzxPeQLlBdxR5vHugeM/XYuhy6CHlZrR/uueaO0X8RyhJcqeDjBy58IrwYS3Mpj7QCfBpQ9PqsqXEWV9BKwKiBXM2+3hkhawFDWa0GW2PDbORKtSLy/ORfGLx5Y9qxQYKEGvFQA3iqkTjrLj+KeUziKtuvEAcvsdBIJVIxeoHwdl+qqxEm8A7YuRBnWVxWc3jE6wBXboeFP92gVe/ueoXmY22riK9Ja0pli3TyNga8by9WM8Qp4D2ZqkVXHwg=
|
||||
on:
|
||||
tags: true
|
||||
condition: $TOXENV = py27-django111-wagtail113
|
@ -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
|
||||
|
58
CHANGES
@ -1,55 +1,3 @@
|
||||
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
|
@ -1,10 +0,0 @@
|
||||
Authors
|
||||
=======
|
||||
* Jasper Berghoef
|
||||
* Boris Besemer
|
||||
|
||||
Contributors
|
||||
============
|
||||
* Michael van Tellingen
|
||||
* Pim Vernooij
|
||||
* Tomasz Knapik
|
21
LICENSE
@ -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.
|
@ -1,6 +1,3 @@
|
||||
include README.rst
|
||||
|
||||
recursive-include src *
|
||||
|
||||
recursive-exclude src __pycache__
|
||||
recursive-exclude src *.py[co]
|
||||
recursive-include src
|
28
Makefile
@ -1,4 +1,4 @@
|
||||
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
|
||||
.PHONY: all clean requirements develop test lint flake8 isort dist
|
||||
|
||||
all: clean requirements dist
|
||||
|
||||
@ -10,9 +10,7 @@ clean:
|
||||
find . -name '*.egg-info' -delete
|
||||
|
||||
requirements:
|
||||
pip install --upgrade -e .[docs,test]
|
||||
|
||||
install: develop
|
||||
pip install --upgrade -e .
|
||||
|
||||
develop: clean requirements
|
||||
|
||||
@ -23,32 +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:
|
||||
./setup.py sdist bdist_wheel
|
||||
|
||||
sandbox:
|
||||
pip install -r sandbox/requirements.txt
|
||||
sandbox/manage.py migrate
|
||||
sandbox/manage.py loaddata sandbox/exampledata/users.json
|
||||
sandbox/manage.py loaddata sandbox/exampledata/personalisation.json
|
||||
sandbox/manage.py runserver
|
||||
|
||||
release:
|
||||
pip install twine wheel
|
||||
rm -rf dist/*
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload -s dist/*
|
||||
|
62
README.rst
@ -1,75 +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/LabD/wagtail-personalisation.svg?branch=master
|
||||
:target: https://travis-ci.org/LabD/wagtail-personalisation
|
||||
|
||||
.. image:: http://codecov.io/github/LabD/wagtail-personalisation/coverage.svg?branch=master
|
||||
:target: http://codecov.io/github/LabD/wagtail-personalisation?branch=master
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/wagtail-personalisation.svg
|
||||
:target: https://pypi.python.org/pypi/wagtail-personalisation/
|
||||
|
||||
.. end-no-pypi
|
||||
|
||||
Wagtail Personalisation
|
||||
=======================
|
||||
|
||||
Wagtail Personalisation is a fully-featured personalisation module for
|
||||
`Wagtail CMS`_. It enables editors to create customised pages
|
||||
- or parts of pages - based on segments whose rules are configured directly
|
||||
in the admin interface.
|
||||
|
||||
.. _Wagtail CMS: http://wagtail.io/
|
||||
|
||||
.. image:: logo.png
|
||||
:scale: 50 %
|
||||
:alt: Wagxperience
|
||||
:align: center
|
||||
|
||||
|
||||
.. image:: screenshot.png
|
||||
Wagtail personalisation
|
||||
=======================
|
||||
Wagtail personalisation enables simple content personalisation through segmenting for Wagtail.
|
||||
|
||||
|
||||
Instructions
|
||||
------------
|
||||
Wagtail Personalisation requires Wagtail 1.9 or 1.10 and Django 1.9, 1.10 or 1.11.
|
||||
|
||||
To install the package with pip::
|
||||
|
||||
pip install wagtail-personalisation
|
||||
|
||||
Next, include the ``wagtail_personalisation``, ``wagtail.contrib.modeladmin``
|
||||
and ``wagtailfontawesome`` apps in your project's ``INSTALLED_APPS``:
|
||||
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',
|
||||
# ...
|
||||
]
|
||||
|
||||
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.
|
||||
|
@ -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)
|
163
docs/conf.py
@ -1,163 +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
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'wagtail-personalisation'
|
||||
copyright = '2017, Lab Digital BV'
|
||||
author = 'Lab Digital BV'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.11.1'
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.11.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
html_theme_options = {
|
||||
'github_user': 'LabD',
|
||||
'github_banner': True,
|
||||
'github_repo': 'wagtail-personalisation',
|
||||
'travis_button': True,
|
||||
'codecov_button': True,
|
||||
'analytics_id': 'UA-100203499-2',
|
||||
}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
# html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'wagtail-personalisationdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'wagtail-personalisation.tex', 'wagtail-personalisation Documentation',
|
||||
'Lab Digital BV', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'wagtail-personalisation', 'wagtail-personalisation Documentation',
|
||||
author, 'wagtail-personalisation', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
@ -1,123 +0,0 @@
|
||||
Included rules
|
||||
==============
|
||||
|
||||
Time rule
|
||||
---------
|
||||
|
||||
The time rule allows you to segment visitors based on the time of their visit.
|
||||
Define a time frame in which visitors are matched to this segment.
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
================== ==========================================================
|
||||
Start time The start time of your time frame.
|
||||
End time The end time of your time frame.
|
||||
================== ==========================================================
|
||||
|
||||
``wagtail_personalisation.rules.TimeRule``
|
||||
|
||||
Day rule
|
||||
--------
|
||||
|
||||
The day rule allows you to segment visitors based on the day of their visit.
|
||||
Select one or multiple days on which you'd like your segment to be applied.
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
================== ==========================================================
|
||||
Monday Matches when the visitors visits on a monday.
|
||||
Tuesday Matches when the visitors visits on a tuesday.
|
||||
Wednesday Matches when the visitors visits on a wednesday.
|
||||
Thursday Matches when the visitors visits on a thursday.
|
||||
Friday Matches when the visitors visits on a friday.
|
||||
Saturday Matches when the visitors visits on a saturday.
|
||||
Sunday Matches when the visitors visits on a sunday.
|
||||
================== ==========================================================
|
||||
|
||||
``wagtail_personalisation.rules.DayRule``
|
||||
|
||||
Referral rule
|
||||
-------------
|
||||
|
||||
The referral rule allows you to match visitors based on the website they were
|
||||
referred from. For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
example\.com|secondexample\.com|.*subdomain\.com
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
================== ==========================================================
|
||||
Regex string The regex string to match the referral header to.
|
||||
================== ==========================================================
|
||||
|
||||
``wagtail_personalisation.rules.ReferralRule``
|
||||
|
||||
Visit count rule
|
||||
----------------
|
||||
|
||||
The visit count rule allows you to segment a visitor based on the amount of
|
||||
visits per page. Use the operator to to set a maximum, minimum or equal
|
||||
amount of visits.
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
================== ==========================================================
|
||||
Page The page on which visits will be counted.
|
||||
Count The amount of visits to match.
|
||||
Operator Whether to match for more than, less than or equal to the
|
||||
specified visit count.
|
||||
================== ==========================================================
|
||||
|
||||
``wagtail_personalisation.rules.VisitCountRule``
|
||||
|
||||
Query rule
|
||||
----------
|
||||
|
||||
The query rule allows you to match a visitor based on the query included in
|
||||
the url. It let's you define both the parameter and the value. It will look
|
||||
something like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
example.com/?campaign=ourbestoffer
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
================== ==========================================================
|
||||
Parameter The first part of the query ('campaign').
|
||||
Value The second part of the query ('ourbestoffer').
|
||||
================== ==========================================================
|
||||
|
||||
``wagtail_personalisation.rules.QueryRule``
|
||||
|
||||
Device rule
|
||||
-----------
|
||||
|
||||
The device rule allows you to match visitors by the type of device they are
|
||||
using. You can select any combination you want.
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
================== ==========================================================
|
||||
Mobile phone Matches when the visitor uses a mobile phone.
|
||||
Tablet Matches when the visitor uses a tablet.
|
||||
Desktop Matches when the visitor uses a desktop.
|
||||
================== ==========================================================
|
||||
|
||||
``wagtail_personalisation.rules.DeviceRule``
|
||||
|
||||
User is logged in rule
|
||||
----------------------
|
||||
|
||||
The user is logged in rule allows you to match visitors that are authenticated
|
||||
and logged in to your app.
|
||||
|
||||
================== ==========================================================
|
||||
Option Description
|
||||
================== ==========================================================
|
||||
Is logged in Whether the user is logged in or logged out.
|
||||
================== ==========================================================
|
||||
|
||||
``wagtail_personalisation.rules.UserIsLoggedInRule``
|
@ -1,32 +0,0 @@
|
||||
Getting started
|
||||
===============
|
||||
|
||||
|
||||
Installing Wagxperience
|
||||
-----------------------
|
||||
|
||||
Installing the module
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The Wagxperience app runs in the Wagtail CMS. You can find out more here_.
|
||||
|
||||
.. _here: http://docs.wagtail.io/en/latest/getting_started/tutorial.html
|
||||
|
||||
1. Install the module::
|
||||
|
||||
pip install wagtail-personalisation
|
||||
|
||||
2. Add the module and ``wagtail.contrib.modeladmin`` to your ``INSTALLED_APPS``::
|
||||
|
||||
INSTALLED_APPS = [
|
||||
# ...
|
||||
'wagtail.contrib.modeladmin',
|
||||
'wagtail_personalisation',
|
||||
# ...
|
||||
]
|
||||
|
||||
3. Update your database::
|
||||
|
||||
python manage.py migrate
|
||||
|
||||
Continue reading: :doc:`implementation`
|
@ -1,87 +0,0 @@
|
||||
Implementation
|
||||
===============
|
||||
|
||||
Extending a page to be personalisable
|
||||
-------------------------------------
|
||||
Wagxperience offers a ``PersonalisablePage`` base class to extend from.
|
||||
This is a standard ``Page`` class with personalisation options added.
|
||||
|
||||
Creating a new personalisable page
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
Import and extend the ``personalisation.models.PersonalisablePage`` class to create a personalisable page.
|
||||
|
||||
A very simple example for a personalisable homepage::
|
||||
|
||||
from wagtail_personalisation.models import PersonalisablePage
|
||||
|
||||
class HomePage(PersonalisablePage):
|
||||
subtitle = models.CharField(max_length=255)
|
||||
body = RichTextField(blank=True, default='')
|
||||
|
||||
content_panels = PersonalisablePage.content_panels + [
|
||||
FieldPanel('subtitle'),
|
||||
FieldPanel('body'),
|
||||
]
|
||||
|
||||
It's just as simple as extending a standard ``Page`` class.
|
||||
|
||||
Migrating an existing page to be personalisable
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
Creating custom rules
|
||||
---------------------
|
||||
|
||||
Rules consist of two important elements, the model's fields and the ``test_user`` function.
|
||||
|
||||
A very simple example of a rule would look something like this::
|
||||
|
||||
from django.db import models
|
||||
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
||||
from personalisation import AbstractBaseRule
|
||||
|
||||
class MyNewRule(AbstractBaseRule):
|
||||
field = models.BooleanField(default=False)
|
||||
|
||||
panels = [
|
||||
FieldPanel('field'),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MyNewRule, self).__init__(*args, **kwargs)
|
||||
|
||||
def test_user(self, request):
|
||||
return self.field
|
||||
|
||||
As you can see, the only real requirement is the ``test_user`` function that will either return
|
||||
``True`` or ``False`` based on the model's fields and optionally the request object.
|
||||
|
||||
Below is the "Time rule" model included with the module, which offers more complex functionality::
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TimeRule(AbstractBaseRule):
|
||||
"""Time rule to segment users based on a start and end time"""
|
||||
start_time = models.TimeField(_("Starting time"))
|
||||
end_time = models.TimeField(_("Ending time"))
|
||||
|
||||
panels = [
|
||||
FieldRowPanel([
|
||||
FieldPanel('start_time'),
|
||||
FieldPanel('end_time'),
|
||||
]),
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(TimeRule, self).__init__(*args, **kwargs)
|
||||
|
||||
def test_user(self, request=None):
|
||||
current_time = datetime.now().time()
|
||||
starting_time = self.start_time
|
||||
ending_time = self.end_time
|
||||
|
||||
return starting_time <= current_time <= ending_time
|
||||
|
||||
def __str__(self):
|
||||
return 'Time Rule'
|
||||
|
||||
Continue reading: :doc:`usage_guide`
|
@ -1,24 +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!
|
||||
==========================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
getting_started
|
||||
implementation
|
||||
usage_guide
|
||||
default_rules
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
@ -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
|
@ -1,95 +0,0 @@
|
||||
Usage guide
|
||||
===========
|
||||
|
||||
Creating a segment
|
||||
------------------
|
||||
|
||||
As soon as the installation is completed and configured, the module will be
|
||||
visible in the Wagtail administrative area.
|
||||
|
||||
To create a segment, go to the "Segments" page and click on "Add a new segment".
|
||||
|
||||
On this page you will be presented with a form. Follow these steps to create a
|
||||
new segment:
|
||||
|
||||
1. Enter a name for your segment.
|
||||
|
||||
2. (Optional) Select whether to match any or all defined rules.
|
||||
|
||||
``match any`` will result in a segment that is applied as soon as one of
|
||||
your rules matches the visitor. When ``match all`` is selected, all rules
|
||||
must match before the segment is applied.
|
||||
|
||||
3. (Optional) Set the segment persistence.
|
||||
|
||||
When persistence is enabled, your segment will stick to the visitor once
|
||||
applied, even if the rules no longer match on the next visit.
|
||||
|
||||
4. Define your segment rules.
|
||||
|
||||
Wagxperience comes with a basic set of :doc:`default_rules` that allow
|
||||
you to get started quickly. The rules you define will be evaluated once a
|
||||
visitor makes a request to your application.
|
||||
|
||||
5. Save your segment.
|
||||
|
||||
Click "save" to store your segment. It will be enabled by default,
|
||||
unless otherwise defined.
|
||||
|
||||
|
||||
Creating personalized content
|
||||
-----------------------------
|
||||
|
||||
Once you've created a segment you can start serving these visitors with
|
||||
personalised content. To do this, you can go one of two directions.
|
||||
|
||||
1. Create a copy of a page for your segment.
|
||||
|
||||
2. Create StreamField blocks only visible to your segment.
|
||||
|
||||
3. Create a template block only visible to your segment.
|
||||
|
||||
|
||||
Method 1: Create a copy
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To create a copy from a page for a specific Segment (which you can change to
|
||||
your liking after copying it) simply go to the Explorer section and find the
|
||||
page you'd wish to personalize.
|
||||
|
||||
You'll notice a new "Variants" dropdown button has appeared. Click the button
|
||||
and select the segment you'd like to create personalized content for.
|
||||
|
||||
Once you've selected the segment, a copy of the page will be created with a
|
||||
title that includes the segment. Don't worry, your visitors won't be able to
|
||||
see this title.
|
||||
|
||||
You can change everything on this page you'd like. Visitors that are included in
|
||||
your segment, will automatically see the new page you've created for them.
|
||||
|
||||
|
||||
Method 2: Create a StreamField block
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
Method 3: Create a template block
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can add a template block that only shows its contents to users of a
|
||||
specific segment. This is done using the "segment" block.
|
||||
|
||||
When editing templates make sure to load the ``wagtail_personalisation_tags``
|
||||
tags library in the template::
|
||||
|
||||
{% load wagtail_personalisation_tags %}
|
||||
|
||||
After that you can add a template block with the name of the segment you want
|
||||
the content to show up for::
|
||||
|
||||
{% segment name="My Segment" %}
|
||||
<p>Only users within "My Segment" see this!</p>
|
||||
{% endsegment %}
|
||||
|
||||
The template block currently only supports one segment at a time. If you want
|
||||
to target multiple segments you will have to make multiple blocks with the
|
||||
same content.
|
@ -1 +0,0 @@
|
||||
import '../scss/dashboard.scss';
|
@ -1 +0,0 @@
|
||||
import '../scss/form.scss';
|
@ -1 +0,0 @@
|
||||
import '../scss/index.scss';
|
BIN
logo.png
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
22
manage.py
Normal 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)
|
53
package.json
@ -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/LabD/wagtail-personalisation.git"
|
||||
},
|
||||
"author": "Lab Digital",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/LabD/wagtail-personalisation/issues"
|
||||
},
|
||||
"homepage": "https://github.com/LabD/wagtail-personalisation#readme"
|
||||
}
|
@ -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"
|
||||
}
|
||||
}]
|
@ -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": []
|
||||
}
|
||||
}
|
||||
]
|
@ -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)
|
@ -1,4 +0,0 @@
|
||||
Django>=1.11,<1.12
|
||||
wagtail>=1.10,<1.11
|
||||
django-debug-toolbar==1.8
|
||||
-e .[docs,test]
|
@ -1 +0,0 @@
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -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),
|
||||
]
|
@ -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.wagtailcore.fields
|
||||
import wagtail_personalisation
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0002_create_homepage'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='homepage',
|
||||
name='intro',
|
||||
field=wagtail.wagtailcore.fields.RichTextField(
|
||||
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='homepage',
|
||||
name='body',
|
||||
field=wagtail.wagtailcore.fields.StreamField((('personalisable_paragraph', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -1,23 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
|
||||
from wagtail.wagtailcore import blocks
|
||||
from wagtail.wagtailcore.fields import RichTextField, StreamField
|
||||
from wagtail.wagtailcore.models import Page
|
||||
|
||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||
from wagtail_personalisation.blocks import PersonalisedStructBlock
|
||||
|
||||
|
||||
class HomePage(PersonalisablePageMixin, Page):
|
||||
intro = RichTextField()
|
||||
body = StreamField([
|
||||
('personalisable_paragraph', PersonalisedStructBlock([
|
||||
('paragraph', blocks.RichTextBlock()),
|
||||
], icon='pilcrow'))
|
||||
])
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
RichTextFieldPanel('intro'),
|
||||
StreamFieldPanel('body'),
|
||||
]
|
@ -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 %}
|
@ -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 }}&page={{ search_results.previous_page_number }}">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
{% if search_results.has_next %}
|
||||
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.next_page_number }}">Next</a>
|
||||
{% endif %}
|
||||
{% elif search_query %}
|
||||
No results found
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -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.wagtailcore.models import Page
|
||||
from wagtail.wagtailsearch.models import Query
|
||||
|
||||
|
||||
def search(request):
|
||||
search_query = request.GET.get('query', None)
|
||||
page = request.GET.get('page', 1)
|
||||
|
||||
# Search
|
||||
if search_query:
|
||||
search_results = Page.objects.live().search(search_query)
|
||||
query = Query.get(search_query)
|
||||
|
||||
# Record hit
|
||||
query.add_hit()
|
||||
else:
|
||||
search_results = Page.objects.none()
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(search_results, 10)
|
||||
try:
|
||||
search_results = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
search_results = paginator.page(1)
|
||||
except EmptyPage:
|
||||
search_results = paginator.page(paginator.num_pages)
|
||||
|
||||
return render(request, 'search/search.html', {
|
||||
'search_query': search_query,
|
||||
'search_results': search_results,
|
||||
})
|
@ -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 = []
|
@ -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']
|
@ -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()),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,51 +0,0 @@
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser, PermissionsMixin, UserManager)
|
||||
from django.core.mail import send_mail
|
||||
from django.db import connections, models
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
"""Cusomtized version of the default `AbstractUser` from Django.
|
||||
|
||||
"""
|
||||
first_name = models.CharField(_('first name'), max_length=100, blank=True)
|
||||
last_name = models.CharField(_('last name'), max_length=100, blank=True)
|
||||
email = models.EmailField(_('email address'), blank=True, unique=True)
|
||||
is_staff = models.BooleanField(
|
||||
_('staff status'), default=False,
|
||||
help_text=_('Designates whether the user can log into this admin '
|
||||
'site.'))
|
||||
is_active = models.BooleanField(
|
||||
_('active'), default=True,
|
||||
help_text=_('Designates whether this user should be treated as '
|
||||
'active. Unselect this instead of deleting accounts.'))
|
||||
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('user')
|
||||
verbose_name_plural = _('users')
|
||||
|
||||
def get_full_name(self):
|
||||
"""
|
||||
Returns the first_name plus the last_name, with a space in between.
|
||||
"""
|
||||
full_name = '%s %s' % (self.first_name, self.last_name)
|
||||
return full_name.strip()
|
||||
|
||||
def get_short_name(self):
|
||||
"Returns the short name for the user."
|
||||
return self.first_name
|
||||
|
||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||
"""
|
||||
Sends an email to this User.
|
||||
"""
|
||||
send_mail(subject, message, from_email, [self.email], **kwargs)
|
@ -1,162 +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
|
||||
|
||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||
|
||||
DEBUG = True
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '^anfvx$i7%wts8j=7u1h5ua$w6c76*333(@h)rrjlak1c&x0r+'
|
||||
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'wagtail.wagtailforms',
|
||||
'wagtail.wagtailredirects',
|
||||
'wagtail.wagtailembeds',
|
||||
'wagtail.wagtailsites',
|
||||
'wagtail.wagtailusers',
|
||||
'wagtail.wagtailsnippets',
|
||||
'wagtail.wagtaildocs',
|
||||
'wagtail.wagtailimages',
|
||||
'wagtail.wagtailsearch',
|
||||
'wagtail.wagtailadmin',
|
||||
'wagtail.wagtailcore',
|
||||
'wagtail.contrib.modeladmin',
|
||||
|
||||
'wagtailfontawesome',
|
||||
'modelcluster',
|
||||
'taggit',
|
||||
'debug_toolbar',
|
||||
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'wagtail_personalisation',
|
||||
|
||||
'sandbox.apps.home',
|
||||
'sandbox.apps.search',
|
||||
'sandbox.apps.user',
|
||||
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
|
||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'sandbox.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [
|
||||
os.path.join(PROJECT_DIR, 'templates'),
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'sandbox.wsgi.application'
|
||||
|
||||
AUTH_USER_MODEL = 'user.User'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATICFILES_FINDERS = [
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
]
|
||||
|
||||
STATICFILES_DIRS = [
|
||||
os.path.join(PROJECT_DIR, 'static'),
|
||||
]
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
|
||||
# Wagtail settings
|
||||
|
||||
WAGTAIL_SITE_NAME = "sandbox"
|
||||
|
||||
# Base URL to use when referring to full URLs within the Wagtail admin backend -
|
||||
# e.g. in notification emails. Don't include '/admin' or a trailing slash
|
||||
BASE_URL = 'http://example.com'
|
||||
|
||||
|
||||
INTERNAL_IPS = ['127.0.0.1']
|
@ -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 %}
|
@ -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>
|
@ -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>
|
@ -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.wagtailadmin import urls as wagtailadmin_urls
|
||||
from wagtail.wagtailcore import urls as wagtail_urls
|
||||
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
||||
|
||||
from sandbox.apps.search import views as search_views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
|
||||
url(r'^cms/', include(wagtailadmin_urls)),
|
||||
url(r'^documents/', include(wagtaildocs_urls)),
|
||||
|
||||
url(r'^search/$', search_views.search, name='search'),
|
||||
|
||||
# For anything not caught by a more specific rule above, hand over to
|
||||
# Wagtail's page serving mechanism. This should be the last pattern in
|
||||
# the list:
|
||||
url(r'', include(wagtail_urls)),
|
||||
|
||||
# Alternatively, if you want Wagtail pages to be served from a subpath
|
||||
# of your site, rather than the site root:
|
||||
# url(r'^pages/', include(wagtail_urls)),
|
||||
]
|
||||
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
# Serve static and media files from development server
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^__debug__/', include(debug_toolbar.urls)),
|
||||
] + urlpatterns
|
@ -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()
|
BIN
screenshot.png
Before Width: | Height: | Size: 98 KiB |
33
setup.cfg
@ -1,31 +1,8 @@
|
||||
[bumpversion]
|
||||
current_version = 0.11.1
|
||||
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:run]
|
||||
omit =
|
||||
src/**/migrations/*.py
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
[bumpversion:file:docs/conf.py]
|
||||
|
||||
ignore=E731
|
||||
exclude=
|
||||
src/**/migrations/*.py
|
||||
|
53
setup.py
@ -1,69 +1,50 @@
|
||||
import re
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
install_requires = [
|
||||
'wagtail>=1.10,<1.14',
|
||||
'user-agents>=1.0.1',
|
||||
'wagtailfontawesome>=1.0.6',
|
||||
'django-polymorphic==1.0.2',
|
||||
'wagtail>=1.7',
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
'factory_boy==2.8.1',
|
||||
'flake8',
|
||||
'flake8-blind-except',
|
||||
'flake8-debugger',
|
||||
'flake8-imports',
|
||||
'freezegun==0.3.8',
|
||||
'pytest==3.0.4',
|
||||
'pytest-cov==2.4.0',
|
||||
'pytest-django==3.1.2',
|
||||
'pytest-django==3.0.0',
|
||||
'pytest-sugar==0.7.1',
|
||||
'pytest-mock==1.6.3',
|
||||
'pytest==3.1.0',
|
||||
'wagtail_factories==0.3.0',
|
||||
'freezegun==0.3.8',
|
||||
'factory_boy==2.7.0',
|
||||
]
|
||||
|
||||
docs_require = [
|
||||
'sphinx>=1.4.0',
|
||||
]
|
||||
|
||||
with open('README.rst') as fh:
|
||||
long_description = re.sub(
|
||||
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
|
||||
|
||||
setup(
|
||||
name='wagtail-personalisation-molo',
|
||||
version='0.11.1',
|
||||
description='A forked version of Wagtail add-on for showing personalized content',
|
||||
author='Praekelt.org',
|
||||
author_email='dev@praekeltfoundation.org',
|
||||
url='https://github.com/praekeltfoundation/wagtail-personalisation/',
|
||||
name='wagtail-personalisation',
|
||||
version='0.1.0',
|
||||
description='A Wagtail add-on for showing personalized content',
|
||||
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 :: 2 - Pre-Alpha',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
||||
],
|
||||
)
|
||||
|
40
src/personalisation/admin.py
Normal 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)
|
15
src/personalisation/admin_urls.py
Normal 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')
|
||||
]
|
@ -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'),
|
||||
),
|
||||
]
|
@ -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,
|
@ -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 = [
|
@ -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 = [
|
@ -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,
|
||||
},
|
||||
),
|
||||
]
|
379
src/personalisation/models.py
Normal 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
|
Before Width: | Height: | Size: 794 B After Width: | Height: | Size: 794 B |
@ -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;
|
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
@ -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 %}
|
19
src/personalisation/templatetags/personalisation_filters.py
Normal 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
|
5
src/personalisation/utils.py
Normal 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
|
46
src/personalisation/views.py
Normal 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)
|
228
src/personalisation/wagtail_hooks.py
Normal 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())
|
@ -1 +0,0 @@
|
||||
default_app_config = 'wagtail_personalisation.config.WagtailPersonalisationConfig'
|
@ -1,226 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import AbstractBaseRule
|
||||
from wagtail_personalisation.utils import create_segment_dictionary
|
||||
|
||||
|
||||
class BaseSegmentsAdapter(object):
|
||||
"""Base segments adapter."""
|
||||
|
||||
def __init__(self, request):
|
||||
"""Prepare the request session for segment storage.
|
||||
|
||||
:param request: The http request
|
||||
:type request: django.http.HttpRequest
|
||||
|
||||
"""
|
||||
self.request = request
|
||||
|
||||
def setup(self):
|
||||
"""Prepare the adapter for segment storage."""
|
||||
|
||||
def get_segments(self):
|
||||
"""Return the segments stored in the adapter storage."""
|
||||
|
||||
def get_segment_by_id(self):
|
||||
"""Return a single segment stored in the adapter storage."""
|
||||
|
||||
def add(self):
|
||||
"""Add a new segment to the adapter storage."""
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the segments stored in the adapter storage."""
|
||||
|
||||
def _test_rules(self, rules, request, match_any=False):
|
||||
"""Tests the provided rules to see if the request still belongs
|
||||
to a segment.
|
||||
:param rules: The rules to test for
|
||||
:type rules: list of wagtail_personalisation.rules
|
||||
:param request: The http request
|
||||
:type request: django.http.HttpRequest
|
||||
:param match_any: Whether all rules need to match, or any
|
||||
:type match_any: bool
|
||||
:returns: A boolean indicating the segment matches the request
|
||||
:rtype: bool
|
||||
"""
|
||||
if not rules:
|
||||
return False
|
||||
if match_any:
|
||||
return any(rule.test_user(request) for rule in rules)
|
||||
return all(rule.test_user(request) for rule in rules)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
"""Segment adapter that uses Django's session backend."""
|
||||
|
||||
def __init__(self, request):
|
||||
super(SessionSegmentsAdapter, self).__init__(request)
|
||||
self.request.session.setdefault('segments', [])
|
||||
self._segment_cache = None
|
||||
|
||||
def get_segments(self, key="segments"):
|
||||
"""Return the persistent segments stored in the request session.
|
||||
|
||||
:param key: The key under which the segments are stored
|
||||
:type key: String
|
||||
:returns: The segments in the request session
|
||||
:rtype: list of wagtail_personalisation.models.Segment or empty list
|
||||
|
||||
"""
|
||||
if key == "segments" and self._segment_cache is not None:
|
||||
return self._segment_cache
|
||||
|
||||
if key not in self.request.session:
|
||||
return []
|
||||
raw_segments = self.request.session[key]
|
||||
segment_ids = [segment['id'] for segment in raw_segments]
|
||||
|
||||
segments = (
|
||||
Segment.objects
|
||||
.enabled()
|
||||
.filter(persistent=True)
|
||||
.in_bulk(segment_ids))
|
||||
|
||||
retval = [segments[pk] for pk in segment_ids if pk in segments]
|
||||
if key == "segments":
|
||||
self._segment_cache = retval
|
||||
return retval
|
||||
|
||||
def set_segments(self, segments, key="segments"):
|
||||
"""Set the currently active segments
|
||||
|
||||
:param segments: The segments to set for the current request
|
||||
:type segments: list of wagtail_personalisation.models.Segment
|
||||
:param key: The key under which to store the segments. Optional
|
||||
:type key: String
|
||||
|
||||
"""
|
||||
cache_segments = []
|
||||
serialized_segments = []
|
||||
segment_ids = set()
|
||||
for segment in segments:
|
||||
serialized = create_segment_dictionary(segment)
|
||||
if serialized['id'] in segment_ids:
|
||||
continue
|
||||
|
||||
cache_segments.append(segment)
|
||||
serialized_segments.append(serialized)
|
||||
segment_ids.add(segment.pk)
|
||||
|
||||
self.request.session[key] = serialized_segments
|
||||
if key == "segments":
|
||||
self._segment_cache = cache_segments
|
||||
|
||||
def get_segment_by_id(self, segment_id):
|
||||
"""Find and return a single segment from the request session.
|
||||
|
||||
:param segment_id: The primary key of the segment
|
||||
:type segment_id: int
|
||||
:returns: The matching segment
|
||||
:rtype: wagtail_personalisation.models.Segment or None
|
||||
|
||||
"""
|
||||
for segment in self.get_segments():
|
||||
if segment.pk == segment_id:
|
||||
return segment
|
||||
|
||||
def add_page_visit(self, page):
|
||||
"""Mark the page as visited by the user"""
|
||||
visit_count = self.request.session.setdefault('visit_count', [])
|
||||
page_visits = [visit for visit in visit_count if visit['id'] == page.pk]
|
||||
|
||||
if page_visits:
|
||||
for page_visit in page_visits:
|
||||
page_visit['count'] += 1
|
||||
page_visit['path'] = page.url_path if page else self.request.path
|
||||
self.request.session.modified = True
|
||||
else:
|
||||
visit_count.append({
|
||||
'slug': page.slug,
|
||||
'id': page.pk,
|
||||
'path': page.url_path if page else self.request.path,
|
||||
'count': 1,
|
||||
})
|
||||
|
||||
def get_visit_count(self, page=None):
|
||||
"""Return the number of visits on the current request or given page"""
|
||||
path = page.url_path if page else self.request.path
|
||||
visit_count = self.request.session.setdefault('visit_count', [])
|
||||
for visit in visit_count:
|
||||
if visit['path'] == path:
|
||||
return visit['count']
|
||||
return 0
|
||||
|
||||
def update_visit_count(self):
|
||||
"""Update the visit count for all segments in the request session."""
|
||||
segments = self.request.session['segments']
|
||||
segment_pks = [s['id'] for s in segments]
|
||||
|
||||
# Update counts
|
||||
(Segment.objects
|
||||
.enabled()
|
||||
.filter(pk__in=segment_pks)
|
||||
.update(visit_count=F('visit_count') + 1))
|
||||
|
||||
def refresh(self):
|
||||
"""Retrieve the request session segments and verify whether or not they
|
||||
still apply to the requesting visitor.
|
||||
|
||||
"""
|
||||
enabled_segments = Segment.objects.enabled()
|
||||
rule_models = AbstractBaseRule.get_descendant_models()
|
||||
|
||||
current_segments = self.get_segments()
|
||||
excluded_segments = self.get_segments("excluded_segments")
|
||||
|
||||
# Run tests on all remaining enabled segments to verify applicability.
|
||||
additional_segments = []
|
||||
for segment in enabled_segments:
|
||||
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
|
||||
additional_segments.append(segment)
|
||||
elif (segment.excluded_users.filter(id=self.request.user.id).exists() or
|
||||
segment in excluded_segments):
|
||||
continue
|
||||
elif not segment.is_static or not segment.is_full:
|
||||
segment_rules = []
|
||||
for rule_model in rule_models:
|
||||
segment_rules.extend(rule_model.objects.filter(segment=segment))
|
||||
|
||||
result = self._test_rules(segment_rules, self.request,
|
||||
match_any=segment.match_any)
|
||||
|
||||
if result and segment.randomise_into_segment():
|
||||
if segment.is_static and not segment.is_full:
|
||||
if self.request.user.is_authenticated():
|
||||
segment.static_users.add(self.request.user)
|
||||
additional_segments.append(segment)
|
||||
elif result:
|
||||
if segment.is_static and self.request.user.is_authenticated():
|
||||
segment.excluded_users.add(self.request.user)
|
||||
else:
|
||||
excluded_segments += [segment]
|
||||
|
||||
self.set_segments(current_segments + additional_segments)
|
||||
self.set_segments(excluded_segments, "excluded_segments")
|
||||
self.update_visit_count()
|
||||
|
||||
|
||||
SEGMENT_ADAPTER_CLASS = import_string(getattr(
|
||||
settings,
|
||||
'PERSONALISATION_SEGMENTS_ADAPTER',
|
||||
'wagtail_personalisation.adapters.SessionSegmentsAdapter'))
|
||||
|
||||
|
||||
def get_segment_adapter(request):
|
||||
"""Return the Segment Adapter for the given request"""
|
||||
if not hasattr(request, 'segment_adapter'):
|
||||
request.segment_adapter = SEGMENT_ADAPTER_CLASS(request)
|
||||
return request.segment_adapter
|
@ -1,46 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from wagtail_personalisation import models, rules
|
||||
|
||||
|
||||
class UserIsLoggedInRuleAdminInline(admin.TabularInline):
|
||||
"""Inline the UserIsLoggedIn Rule into the
|
||||
administration interface for segments.
|
||||
|
||||
"""
|
||||
model = rules.UserIsLoggedInRule
|
||||
|
||||
|
||||
class TimeRuleAdminInline(admin.TabularInline):
|
||||
"""Inline the Time Rule into the
|
||||
administration interface for segments.
|
||||
|
||||
"""
|
||||
model = rules.TimeRule
|
||||
|
||||
|
||||
class ReferralRuleAdminInline(admin.TabularInline):
|
||||
"""Inline the Referral Rule into the
|
||||
administration interface for segments.
|
||||
|
||||
"""
|
||||
model = rules.ReferralRule
|
||||
|
||||
|
||||
class VisitCountRuleAdminInline(admin.TabularInline):
|
||||
"""Inline the Visit Count Rule into the
|
||||
administration interface for segments.
|
||||
|
||||
"""
|
||||
model = rules.VisitCountRule
|
||||
|
||||
|
||||
class SegmentAdmin(admin.ModelAdmin):
|
||||
"""Add the inline models to the Segment admin interface."""
|
||||
inlines = (UserIsLoggedInRuleAdminInline, TimeRuleAdminInline,
|
||||
ReferralRuleAdminInline, VisitCountRuleAdminInline)
|
||||
|
||||
|
||||
admin.site.register(models.Segment, SegmentAdmin)
|
@ -1,18 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from wagtail_personalisation import views
|
||||
|
||||
app_name = 'segment'
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^segment/(?P<segment_id>[0-9]+)/toggle/$',
|
||||
views.toggle, name='toggle'),
|
||||
url(r'^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$',
|
||||
views.copy_page_view, name='copy_page'),
|
||||
url(r'^segment/toggle_segment_view/$',
|
||||
views.toggle_segment_view, name='toggle_segment_view'),
|
||||
url(r'^segment/users/(?P<segment_id>[0-9]+)$',
|
||||
views.segment_user_data, name='segment_user_data'),
|
||||
]
|
@ -1,44 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailcore import blocks
|
||||
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import Segment
|
||||
|
||||
|
||||
def list_segment_choices():
|
||||
for pk, name in Segment.objects.values_list('pk', 'name'):
|
||||
yield pk, name
|
||||
|
||||
|
||||
class PersonalisedStructBlock(blocks.StructBlock):
|
||||
"""Struct block that allows personalisation per block."""
|
||||
|
||||
segment = blocks.ChoiceBlock(
|
||||
choices=list_segment_choices,
|
||||
required=False, label=_("Personalisation segment"),
|
||||
help_text=_("Only show this content block for users in this segment"))
|
||||
|
||||
def render(self, value, context=None):
|
||||
"""Only render this content block for users in this segment.
|
||||
|
||||
:param value: The value from the block
|
||||
:type value: dict
|
||||
:param context: The context containing the request
|
||||
:type context: dict
|
||||
:returns: The provided block if matched, otherwise an empty string
|
||||
:rtype: blocks.StructBlock or empty str
|
||||
|
||||
"""
|
||||
request = context['request']
|
||||
adapter = get_segment_adapter(request)
|
||||
user_segments = adapter.get_segments()
|
||||
|
||||
if value['segment']:
|
||||
for segment in user_segments:
|
||||
if segment.id == int(value['segment']):
|
||||
return super(PersonalisedStructBlock, self).render(
|
||||
value, context)
|
||||
|
||||
return ""
|
@ -1,13 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class WagtailPersonalisationConfig(AppConfig):
|
||||
label = 'wagtail_personalisation'
|
||||
name = 'wagtail_personalisation'
|
||||
verbose_name = _('Wagtail Personalisation')
|
||||
|
||||
def ready(self):
|
||||
from wagtail_personalisation import receivers
|
||||
|
||||
receivers.register()
|
@ -1,138 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils.lru_cache import lru_cache
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailadmin.forms import WagtailAdminModelForm
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def user_from_data(user_id):
|
||||
User = get_user_model()
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
class SegmentAdminForm(WagtailAdminModelForm):
|
||||
|
||||
def count_matching_users(self, rules, match_any):
|
||||
""" Calculates how many users match the given static rules
|
||||
"""
|
||||
count = 0
|
||||
|
||||
static_rules = [rule for rule in rules if rule.static]
|
||||
|
||||
if not static_rules:
|
||||
return count
|
||||
|
||||
User = get_user_model()
|
||||
users = User.objects.filter(is_active=True, is_staff=False)
|
||||
|
||||
for user in users.iterator():
|
||||
if match_any:
|
||||
if any(rule.test_user(None, user) for rule in static_rules):
|
||||
count += 1
|
||||
elif all(rule.test_user(None, user) for rule in static_rules):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(SegmentAdminForm, self).clean()
|
||||
Segment = self._meta.model
|
||||
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
for form in formset
|
||||
if form not in formset.deleted_forms
|
||||
]
|
||||
consistent = rules and Segment.all_static(rules)
|
||||
|
||||
if cleaned_data.get('type') == Segment.TYPE_STATIC and not cleaned_data.get('count') and not consistent:
|
||||
self.add_error('count', _('Static segments with non-static compatible rules must include a count.'))
|
||||
|
||||
if self.instance.id and self.instance.is_static:
|
||||
if self.has_changed():
|
||||
self.add_error_to_fields(self, excluded=['name', 'enabled'])
|
||||
|
||||
for formset in self.formsets.values():
|
||||
if formset.has_changed():
|
||||
for form in formset:
|
||||
if form not in formset.deleted_forms:
|
||||
self.add_error_to_fields(form)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def add_error_to_fields(self, form, excluded=list()):
|
||||
for field in form.changed_data:
|
||||
if field not in excluded:
|
||||
form.add_error(field, _('Cannot update a static segment'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
|
||||
if not self.instance.is_static:
|
||||
self.instance.count = 0
|
||||
|
||||
if is_new and self.instance.is_static and not self.instance.all_rules_static:
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
for form in formset
|
||||
if form not in formset.deleted_forms
|
||||
]
|
||||
self.instance.matched_users_count = self.count_matching_users(
|
||||
rules, self.instance.match_any)
|
||||
self.instance.matched_count_updated_at = datetime.now()
|
||||
|
||||
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
|
||||
|
||||
if is_new and instance.is_static and instance.all_rules_static:
|
||||
from .adapters import get_segment_adapter
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.session = SessionStore()
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
users_to_add = []
|
||||
users_to_exclude = []
|
||||
|
||||
User = get_user_model()
|
||||
users = User.objects.filter(is_active=True, is_staff=False)
|
||||
|
||||
matched_count = 0
|
||||
for user in users.iterator():
|
||||
request.user = user
|
||||
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
|
||||
if passes:
|
||||
matched_count += 1
|
||||
if instance.count == 0 or len(users_to_add) <= instance.count:
|
||||
if instance.randomise_into_segment():
|
||||
users_to_add.append(user)
|
||||
else:
|
||||
users_to_exclude.append(user)
|
||||
|
||||
instance.matched_users_count = matched_count
|
||||
instance.matched_count_updated_at = datetime.now()
|
||||
instance.static_users.add(*users_to_add)
|
||||
instance.excluded_users.add(*users_to_exclude)
|
||||
|
||||
return instance
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
media = super(SegmentAdminForm, self).media
|
||||
media.add_js(
|
||||
[static('js/segment_form_control.js')]
|
||||
)
|
||||
return media
|
@ -1,352 +0,0 @@
|
||||
# Wagtail Personalisation english translation strings.
|
||||
# Copyright (C) 2017 Lab Digital B.V.
|
||||
# This file is distributed under the same license as the wagtail_personalisation package.
|
||||
# Boris Besemer <b.besemer@labdigital.nl>, 2017.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: wagtail_personalisation 0.1.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2017-05-31 09:30-0500\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: blocks.py:20
|
||||
msgid "Personalisation segment"
|
||||
msgstr ""
|
||||
|
||||
#: blocks.py:21
|
||||
msgid "Only show this content block for users in this segment"
|
||||
msgstr ""
|
||||
|
||||
#: config.py:8
|
||||
msgid "Wagtail Personalisation"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:26
|
||||
msgid "Enabled"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:27
|
||||
msgid "Disabled"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:39
|
||||
msgid "Should the segment persist between visits?"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:42
|
||||
msgid "Should the segment match all the rules or just one of them?"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:60
|
||||
msgid "Rules"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:167
|
||||
msgid "Content"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:169
|
||||
msgid "Variations"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:171
|
||||
msgid "Promote"
|
||||
msgstr ""
|
||||
|
||||
#: models.py:173
|
||||
msgid "Settings"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:29 rules.py:48
|
||||
msgid "Abstract segmentation rule"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:63
|
||||
msgid "Starting time"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:64
|
||||
msgid "Ending time"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:74
|
||||
msgid "Time Rule"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:85
|
||||
msgid "These users visit between"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:86
|
||||
msgid "{} and {}"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:103
|
||||
msgid "Monday"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:104
|
||||
msgid "Tuesday"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:105
|
||||
msgid "Wednesday"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:106
|
||||
msgid "Thursday"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:107
|
||||
msgid "Friday"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:108
|
||||
msgid "Saturday"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:109
|
||||
msgid "Sunday"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:122
|
||||
msgid "Day Rule"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:146
|
||||
msgid "These users visit on"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:162
|
||||
msgid "Regular expression to match the referrer"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:169
|
||||
msgid "Referral Rule"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:182
|
||||
msgid "These visits originate from"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:183 rules.py:366
|
||||
msgid "{}"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:202
|
||||
msgid "More than"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:203
|
||||
msgid "Less than"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:204
|
||||
msgid "Equal to"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:247
|
||||
msgid "Visit count Rule"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:251
|
||||
msgid "These users visited {}"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:254
|
||||
msgid "{} {} times"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:271
|
||||
msgid "The query parameter to search for"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:273
|
||||
msgid "The value of the parameter to match"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:282
|
||||
msgid "Query Rule"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:293
|
||||
msgid "These users used a URL with the query"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:294
|
||||
msgid "?{}={}"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:312
|
||||
msgid "Mobile phone"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:313
|
||||
msgid "Tablet"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:314
|
||||
msgid "Desktop"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:323
|
||||
msgid "Device Rule"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:354
|
||||
msgid "Logged in Rule"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:360
|
||||
msgid "Logged in"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:362
|
||||
msgid "Not logged in"
|
||||
msgstr ""
|
||||
|
||||
#: rules.py:365
|
||||
msgid "These visitors are"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/base.html:28
|
||||
msgid "Switch view"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:14
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/index.html:14
|
||||
msgid "Filter"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:30
|
||||
msgid "This segment has been visited"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:31
|
||||
msgid "time"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:34
|
||||
msgid "This segment has been active for"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:35
|
||||
msgid "day"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:43
|
||||
msgid "The visitor must match"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:45
|
||||
msgid "Any rule"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:47
|
||||
msgid "All rules"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:52
|
||||
msgid "The persistence of this segment is"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:54
|
||||
msgid "This segment persists in between visits"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:54
|
||||
msgid "Persistent"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:56
|
||||
msgid "This segment is re-evaluated on every visit"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:56
|
||||
msgid "Fleeting"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:76
|
||||
msgid "Enable this segment"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:78
|
||||
msgid "Disable this segment"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:80
|
||||
msgid "Configure this segment"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:88
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" <a class=\"block suggestion\" href="
|
||||
"\"%(url)s\">\n"
|
||||
" <span class=\"suggestive_text\">Add "
|
||||
"a new %(name)s</span>\n"
|
||||
" </a>\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/dashboard.html:101
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/index.html:45
|
||||
#, python-format
|
||||
msgid "Page %(current_page)s of %(num_pages)s."
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/index.html:24
|
||||
#, python-format
|
||||
msgid ""
|
||||
"No %(name)s have been created yet. One of the following must be created "
|
||||
"before you can add any %(name)s:"
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/index.html:29
|
||||
#, python-format
|
||||
msgid "No %(name)s have been created yet."
|
||||
msgstr ""
|
||||
|
||||
#: templates/modeladmin/wagtail_personalisation/segment/index.html:31
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" Why not <a href=\"%(url)s\">add "
|
||||
"one</a>?\n"
|
||||
" "
|
||||
msgstr ""
|
||||
|
||||
#: views.py:60
|
||||
#, python-brace-format
|
||||
msgid "{visits} visits"
|
||||
msgstr ""
|
||||
|
||||
#: views.py:63
|
||||
#, python-brace-format
|
||||
msgid "{days} days"
|
||||
msgstr ""
|
||||
|
||||
#: wagtail_hooks.py:121
|
||||
msgid "Variants"
|
||||
msgstr ""
|
||||
|
||||
#: wagtail_hooks.py:126
|
||||
msgid "Create a new variant"
|
||||
msgstr ""
|
||||
|
||||
#: wagtail_hooks.py:146
|
||||
msgid "Create this variant"
|
||||
msgstr ""
|
||||
|
||||
#: wagtail_hooks.py:159
|
||||
msgid "Segments"
|
||||
msgstr ""
|
@ -1,28 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.1 on 2016-12-11 12:15
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0004_segment_persistent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserIsLoggedInRule',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_logged_in', models.BooleanField(default=False)),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_userisloggedinrule_related', related_query_name='wagtail_personalisation_userisloggedinrules', to='wagtail_personalisation.Segment')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -1,20 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.1 on 2016-12-22 13:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0005_userisloggedinrule'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='match_any',
|
||||
field=models.BooleanField(default=False, help_text='Should the segment match all the rules or just one of them?'),
|
||||
),
|
||||
]
|
@ -1,34 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.5 on 2017-01-10 14:35
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0006_segment_match_any'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DayRule',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mon', models.BooleanField(default=False, verbose_name='Monday')),
|
||||
('tue', models.BooleanField(default=False, verbose_name='Tuesday')),
|
||||
('wed', models.BooleanField(default=False, verbose_name='Wednesday')),
|
||||
('thu', models.BooleanField(default=False, verbose_name='Thursday')),
|
||||
('fri', models.BooleanField(default=False, verbose_name='Friday')),
|
||||
('sat', models.BooleanField(default=False, verbose_name='Saturday')),
|
||||
('sun', models.BooleanField(default=False, verbose_name='Sunday')),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_dayrule_related', related_query_name='wagtail_personalisation_dayrules', to='wagtail_personalisation.Segment')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -1,30 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.7 on 2017-04-20 15:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
import modelcluster.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0007_dayrule'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DeviceRule',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mobile', models.BooleanField(default=False, verbose_name='Mobile phone')),
|
||||
('tablet', models.BooleanField(default=False, verbose_name='Tablet')),
|
||||
('desktop', models.BooleanField(default=False, verbose_name='Desktop')),
|
||||
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_devicerule_related', related_query_name='wagtail_personalisation_devicerules', to='wagtail_personalisation.Segment')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -1,30 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-31 04:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0008_devicerule'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='personalisablepage',
|
||||
name='canonical_page',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='personalisablepage',
|
||||
name='page_ptr',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='personalisablepage',
|
||||
name='segment',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='PersonalisablePage',
|
||||
),
|
||||
]
|
@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-31 11:01
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0009_auto_20170531_0428'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='dayrule',
|
||||
options={'verbose_name': 'Day Rule'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='devicerule',
|
||||
options={'verbose_name': 'Device Rule'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='queryrule',
|
||||
options={'verbose_name': 'Query Rule'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='referralrule',
|
||||
options={'verbose_name': 'Referral Rule'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='timerule',
|
||||
options={'verbose_name': 'Time Rule'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='userisloggedinrule',
|
||||
options={'verbose_name': 'Logged in Rule'},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='visitcountrule',
|
||||
options={'verbose_name': 'Visit count Rule'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='referralrule',
|
||||
name='regex_string',
|
||||
field=models.TextField(verbose_name='Regular expression to match the referrer'),
|
||||
),
|
||||
]
|
@ -1,30 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-05-31 14:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0001_initial'),
|
||||
('wagtail_personalisation', '0010_auto_20170531_1101'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PersonalisablePageMetadata',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('is_segmented', models.BooleanField(default=False)),
|
||||
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='personalisable_canonical_metadata', to='wagtailcore.Page')),
|
||||
('segment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='page_metadata', to='wagtail_personalisation.Segment')),
|
||||
('variant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-06-01 11:48
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0011_personalisablepagemetadata'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='personalisablepagemetadata',
|
||||
name='is_segmented',
|
||||
),
|
||||
]
|
@ -1,31 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-10-17 11:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sessions', '0001_initial'),
|
||||
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='count',
|
||||
field=models.PositiveSmallIntegerField(default=0, help_text='If this number is set for a static segment users will be added to the set until the number is reached. After this no more users will be added.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
field=models.ManyToManyField(to='sessions.Session'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('dynamic', 'Dynamic'), ('static', 'Static')], default='dynamic', help_text='\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ', max_length=20),
|
||||
),
|
||||
]
|
@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-11-01 15:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0013_add_dynamic_static_to_segment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='static_users',
|
||||
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-01-25 09:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0015_static_users'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='matched_count_updated_at',
|
||||
field=models.DateTimeField(editable=False, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='matched_users_count',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
),
|
||||
]
|
@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.8 on 2018-01-31 16:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0016_auto_20180125_0918'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='randomisation_percent',
|
||||
field=models.PositiveSmallIntegerField(blank=True, default=None, help_text='If this number is set each user matching the rules will have this percentage chance of being placed in the segment.', null=True, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
@ -1,22 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-09 08:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0017_segment_randomisation_percent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='excluded_users',
|
||||
field=models.ManyToManyField(help_text='Users that matched the rules but were excluded from the segment for some reason e.g. randomisation', related_name='excluded_segments', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|