Compare commits
193 Commits
0.9.0
...
feature/dj
Author | SHA1 | Date | |
---|---|---|---|
0bdb80f25a | |||
2a48eb3498 | |||
4ad097b4fa | |||
12f110d913 | |||
c8fe62d2b1 | |||
84ac76f33e | |||
f6598ca1f7 | |||
726c0cd70f | |||
4f3f9a4d40 | |||
3a378830e0 | |||
8a151e3bab | |||
bb34bddaf4 | |||
9710d3b479 | |||
5536adc3ec | |||
5b8d578493 | |||
bdba6b65cf | |||
cbcd80d248 | |||
9b1c5a6ab6 | |||
62d258fd9e | |||
32e73329c3 | |||
fde53ea0ef | |||
22a7367211 | |||
0d89d47735 | |||
92189a3be8 | |||
6c9d8b2730 | |||
e141e5396e | |||
c0e2b969e8 | |||
7b5e3d4c9d | |||
6b7a1ed591 | |||
9b25cd2a94 | |||
3a86c189dc | |||
82c26f9772 | |||
03eb812e45 | |||
e3522d0acb | |||
7f5e958ee3 | |||
241bfb5240 | |||
d5df6e0e58 | |||
865efd0792 | |||
454c936e0f | |||
74d3123084 | |||
9bfd816430 | |||
02e06bd9f3 | |||
c7ad3251cf | |||
cb8b7da496 | |||
0efd3ae937 | |||
d335e4fd7b | |||
db2f82967e | |||
37243365a7 | |||
43a2b590b4 | |||
cc1dd337bb | |||
a677846ff7 | |||
7d7861b862 | |||
8e854d0abe | |||
0051061d96 | |||
f898dfe017 | |||
8ced5bd81c | |||
9a86b0c8cc | |||
9408f90789 | |||
ba6056e3f8 | |||
fdc0a7f2e1 | |||
12b0cd9231 | |||
330557be8d | |||
aa917dee9c | |||
364cb1a7e6 | |||
8f789b3e17 | |||
bedbe06c65 | |||
362f15e5ff | |||
8a0dba2efb | |||
59f4877e04 | |||
2ff29cc375 | |||
8527e6ff23 | |||
d7c07cb238 | |||
6e83366df6 | |||
55364f8906 | |||
4fd0b30c66 | |||
c909852b08 | |||
ea1ecc2a98 | |||
0f0aecf673 | |||
c11960f921 | |||
37d49dcdfb | |||
869237360d | |||
33277a0b20 | |||
2cd643fb2d | |||
0f18024ebc | |||
521222f748 | |||
56a8e106d8 | |||
3162191a16 | |||
8c7e99313b | |||
824e42174f | |||
d114bb2570 | |||
7bba1e57cc | |||
3017f32b6b | |||
6b1a7cf1f2 | |||
1525b7946c | |||
7bf1bc3f19 | |||
4c60bcbe6b | |||
ad4f75d471 | |||
086168954d | |||
881090f2f9 | |||
d073c7d268 | |||
7200b5b4c4 | |||
6f97c76958 | |||
ecb4f928fb | |||
29aa91477e | |||
5c3acc6661 | |||
602919d2d4 | |||
ae97118c3f | |||
51774b939e | |||
908f85e295 | |||
99f9700ed0 | |||
7fa8ee1a46 | |||
5ad70d68f6 | |||
06bfe77901 | |||
d5e89d374b | |||
5b39e82f80 | |||
fbcebb43a4 | |||
ef271587ec | |||
786a8801b1 | |||
caf73aa43c | |||
4021d2c915 | |||
33f96af4a3 | |||
6299feb497 | |||
7ced6db126 | |||
c6ce67c9c9 | |||
3df3fc0b16 | |||
a00929846e | |||
49fba11049 | |||
e3488e87ad | |||
808aa6d202 | |||
efb060cc6e | |||
414afa5269 | |||
b3f0ac2d58 | |||
4f9c18d2cf | |||
a4a283e4f3 | |||
30318549e2 | |||
f19de241b0 | |||
95ecd8d200 | |||
6436b85b1d | |||
06471248d3 | |||
e3df03f559 | |||
0a42ce3eeb | |||
e5068894c3 | |||
fdc2b97194 | |||
a8d3aeab68 | |||
c76d6d1617 | |||
a8c4b66d6e | |||
f3fbee99a2 | |||
4918c99b5f | |||
330c3bd377 | |||
9c9a9d3acd | |||
51e9aa9724 | |||
a5705fd53c | |||
9d1f3074c0 | |||
3bfd5b8e8f | |||
232609fb4e | |||
35fd4836b0 | |||
b786b0a4d2 | |||
23b1456438 | |||
1f4a4536ab | |||
b8bf27fb99 | |||
d07e06b4f0 | |||
71d7faba1f | |||
743d3f668e | |||
bc0b69fde5 | |||
7cf22d05f6 | |||
9e0fc8e6fd | |||
a116b14d57 | |||
44cc95617e | |||
c6ff2801c5 | |||
0d2834a55f | |||
ff236a095d | |||
ef20580334 | |||
cf41be4b76 | |||
f339879907 | |||
aa2a239aec | |||
8c96fffd4e | |||
675d219f1f | |||
0d9e4aab0c | |||
ac9f32c570 | |||
bc91d64770 | |||
821ee5863e | |||
1a2777835c | |||
d160bb5217 | |||
b021164309 | |||
6f6d6e3a06 | |||
fb7ed4936d | |||
80e33a467e | |||
b553295fc2 | |||
0da7f111e3 | |||
fe6a26e1fd | |||
94c947a435 | |||
ef08403ba3 | |||
8905f471ee |
2
.gitignore
vendored
2
.gitignore
vendored
@ -23,3 +23,5 @@ tests/sandbox/assets
|
||||
node_modules
|
||||
|
||||
.DS_Store
|
||||
|
||||
.pytest_cache/
|
||||
|
14
.travis.yml
14
.travis.yml
@ -4,16 +4,12 @@ language: python
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django111-wagtail110
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django111-wagtail110
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django111-wagtail110
|
||||
|
||||
allow_failures:
|
||||
- python: 3.5
|
||||
env: TOXENV=lint
|
||||
env: lint
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django20-wagtail20
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django20-wagtail21
|
||||
|
||||
install:
|
||||
- pip install tox codecov
|
||||
|
9
.tx/config
Normal file
9
.tx/config
Normal file
@ -0,0 +1,9 @@
|
||||
[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
|
||||
|
55
CHANGES
55
CHANGES
@ -1,3 +1,58 @@
|
||||
0.11.3
|
||||
==================
|
||||
- Bugfix: Handle errors when testing an invalid visit count rule
|
||||
|
||||
0.11.2
|
||||
==================
|
||||
- Bugfix: Stop populating static segments when the count is reached
|
||||
|
||||
0.11.1
|
||||
==================
|
||||
- Populate entirely static segments from registered Users not active Sessions
|
||||
|
||||
0.11.0
|
||||
==================
|
||||
- Bug Fix: Query rule should not be static
|
||||
- Enable retrieval of user data for static rules through csv download
|
||||
|
||||
0.10.9
|
||||
==================
|
||||
- Bug Fix: Display the number of users in a static segment on dashboard
|
||||
|
||||
0.10.8
|
||||
==================
|
||||
- Don't add users to exclude list for dynamic segments
|
||||
- Store segments a user is excluded from in the session
|
||||
|
||||
0.10.7
|
||||
==================
|
||||
- Bug Fix: Ensure static segment members are show the survey immediately
|
||||
- Records users excluded by randomisation on the segment
|
||||
- Don't re-check excluded users
|
||||
|
||||
0.10.6
|
||||
==================
|
||||
- Accepts and stores randomisation percentage for segment
|
||||
- Adds users to segment based on random number relative to percentage
|
||||
|
||||
0.10.5
|
||||
==================
|
||||
- Count how many users match a segments rules before saving the segment
|
||||
- Stores count on the segment and displays in the dashboard
|
||||
- Enables testing users against rules if there isn't an active request
|
||||
|
||||
0.10.0
|
||||
==================
|
||||
- Adds static and dynamic segments
|
||||
|
||||
0.9.1 (tbd)
|
||||
==================
|
||||
|
||||
- Fixes import for reverse resolver for older Django versions (<1.10)
|
||||
- Bases migrations off of older wagtail dependencies
|
||||
- Adds more dashboard panels and fixes exclude variants function
|
||||
|
||||
|
||||
0.9.0 (2017-06-02)
|
||||
==================
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
include README.rst
|
||||
|
||||
recursive-include src
|
||||
recursive-include src *
|
||||
|
||||
recursive-exclude src __pycache__
|
||||
recursive-exclude src *.py[co]
|
||||
|
9
Makefile
9
Makefile
@ -1,13 +1,13 @@
|
||||
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
|
||||
|
||||
all: clean requirements dist
|
||||
|
||||
default: develop
|
||||
|
||||
all: clean requirements dist
|
||||
|
||||
clean:
|
||||
find src -name '*.pyc' -delete
|
||||
find tests -name '*.pyc' -delete
|
||||
find . -name '*.egg-info' -delete
|
||||
find . -name '*.egg-info' |xargs rm -rf
|
||||
|
||||
requirements:
|
||||
pip install --upgrade -e .[docs,test]
|
||||
@ -38,7 +38,8 @@ isort:
|
||||
isort --recursive src tests
|
||||
|
||||
dist:
|
||||
./setup.py sdist bdist_wheel
|
||||
pip install wheel
|
||||
python ./setup.py sdist bdist_wheel
|
||||
|
||||
sandbox:
|
||||
pip install -r sandbox/requirements.txt
|
||||
|
@ -35,7 +35,7 @@ in the admin interface.
|
||||
|
||||
Instructions
|
||||
------------
|
||||
Wagtail Personalisation requires Wagtail 1.10 and Django 1.11.
|
||||
Wagtail Personalisation requires Wagtail 1.9 or 1.10 and Django 1.9, 1.10 or 1.11.
|
||||
|
||||
To install the package with pip::
|
||||
|
||||
@ -70,6 +70,6 @@ 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
|
||||
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``.
|
||||
|
@ -55,10 +55,10 @@ author = 'Lab Digital BV'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.9.0'
|
||||
version = '0.11.3'
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.9.0'
|
||||
release = '0.11.3'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
0
frontend/img/.gitkeep
Normal file
0
frontend/img/.gitkeep
Normal file
@ -86,6 +86,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 {
|
||||
@ -93,11 +98,6 @@
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.block_container .block .inspect_container .inspect li {
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.block_container .block .inspect_container .inspect li span {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
|
@ -1,4 +1,4 @@
|
||||
Django>=1.11,<1.12
|
||||
wagtail>=1.10,<1.11
|
||||
django-debug-toolbar==1.8
|
||||
Django>=2.0,<2.1
|
||||
wagtail>=2.0,<2.2
|
||||
django-debug-toolbar==1.9.1
|
||||
-e .[docs,test]
|
||||
|
@ -3,7 +3,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
import wagtail.wagtailcore.fields
|
||||
import wagtail.core.fields
|
||||
import wagtail_personalisation
|
||||
|
||||
|
||||
@ -17,14 +17,14 @@ class Migration(migrations.Migration):
|
||||
migrations.AddField(
|
||||
model_name='homepage',
|
||||
name='intro',
|
||||
field=wagtail.wagtailcore.fields.RichTextField(
|
||||
field=wagtail.core.fields.RichTextField(
|
||||
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
|
||||
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=''),
|
||||
field=wagtail.core.fields.StreamField((('personalisable_paragraph', wagtail.core.blocks.StructBlock((('segment', wagtail.core.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.core.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
@ -1,9 +1,9 @@
|
||||
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.admin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
|
||||
from wagtail.core import blocks
|
||||
from wagtail.core.fields import RichTextField, StreamField
|
||||
from wagtail.core.models import Page
|
||||
|
||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||
from wagtail_personalisation.blocks import PersonalisedStructBlock
|
||||
|
@ -3,8 +3,8 @@ from __future__ import absolute_import, unicode_literals
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||
from django.shortcuts import render
|
||||
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.wagtailsearch.models import Query
|
||||
from wagtail.core.models import Page
|
||||
from wagtail.search.models import Query
|
||||
|
||||
|
||||
def search(request):
|
||||
|
@ -29,21 +29,30 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'wagtail.wagtailforms',
|
||||
'wagtail.wagtailredirects',
|
||||
'wagtail.wagtailembeds',
|
||||
'wagtail.wagtailsites',
|
||||
'wagtail.wagtailusers',
|
||||
'wagtail.wagtailsnippets',
|
||||
'wagtail.wagtaildocs',
|
||||
'wagtail.wagtailimages',
|
||||
'wagtail.wagtailsearch',
|
||||
'wagtail.wagtailadmin',
|
||||
'wagtail.wagtailcore',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'wagtail.contrib.forms',
|
||||
'wagtail.contrib.redirects',
|
||||
'wagtail.embeds',
|
||||
'wagtail.sites',
|
||||
'wagtail.users',
|
||||
'wagtail.snippets',
|
||||
'wagtail.documents',
|
||||
'wagtail.images',
|
||||
'wagtail.search',
|
||||
'wagtail.admin',
|
||||
'wagtail.core',
|
||||
'wagtail.contrib.modeladmin',
|
||||
|
||||
'wagtailfontawesome',
|
||||
@ -51,13 +60,6 @@ INSTALLED_APPS = [
|
||||
'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',
|
||||
@ -68,17 +70,17 @@ INSTALLED_APPS = [
|
||||
|
||||
MIDDLEWARE = [
|
||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.http.ConditionalGetMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
|
||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
||||
'wagtail.core.middleware.SiteMiddleware',
|
||||
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'sandbox.urls'
|
||||
|
@ -4,14 +4,14 @@ 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 wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.core import urls as wagtail_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
from sandbox.apps.search import views as search_views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^admin/', admin.site.urls),
|
||||
|
||||
url(r'^cms/', include(wagtailadmin_urls)),
|
||||
url(r'^documents/', include(wagtaildocs_urls)),
|
||||
|
26
setup.cfg
26
setup.cfg
@ -1,3 +1,9 @@
|
||||
[bumpversion]
|
||||
current_version = 0.11.3
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = {new_version}
|
||||
|
||||
[tool:pytest]
|
||||
DJANGO_SETTINGS_MODULE = tests.settings
|
||||
minversion = 3.0
|
||||
@ -7,25 +13,19 @@ testpaths = tests
|
||||
python_paths = .
|
||||
|
||||
[flake8]
|
||||
ignore=E731
|
||||
ignore = E731
|
||||
max-line-length = 120
|
||||
exclude=
|
||||
src/**/migrations/*.py
|
||||
exclude =
|
||||
src/**/migrations/*.py
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[coverage:run]
|
||||
omit =
|
||||
src/**/migrations/*.py
|
||||
|
||||
|
||||
[bumpversion]
|
||||
current_version = 0.9.0
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = {new_version}
|
||||
[coverage]
|
||||
include = src/**/
|
||||
omit = src/**/migrations/*.py
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
[bumpversion:file:docs/conf.py]
|
||||
|
||||
|
34
setup.py
34
setup.py
@ -1,25 +1,26 @@
|
||||
import re
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
install_requires = [
|
||||
'wagtail>=1.10,<1.11',
|
||||
'user-agents>=1.0.1',
|
||||
'wagtailfontawesome>=1.0.6',
|
||||
'wagtail>=2.0,<2.2',
|
||||
'user-agents>=1.1.0',
|
||||
'wagtailfontawesome>=1.1.3',
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
'factory_boy==2.8.1',
|
||||
'flake8',
|
||||
'flake8-blind-except',
|
||||
'flake8-debugger',
|
||||
'flake8-imports',
|
||||
'flake8',
|
||||
'freezegun==0.3.8',
|
||||
'pytest-cov==2.4.0',
|
||||
'pytest-cov==2.5.1',
|
||||
'pytest-django==3.1.2',
|
||||
'pytest-sugar==0.7.1',
|
||||
'pytest==3.1.0',
|
||||
'wagtail_factories==0.3.0',
|
||||
'pytest-pythonpath==0.7.2',
|
||||
'pytest-sugar==0.9.1',
|
||||
'pytest==3.4.2',
|
||||
'wagtail_factories==1.0.0',
|
||||
'pytest-mock==1.6.3',
|
||||
]
|
||||
|
||||
docs_require = [
|
||||
@ -32,11 +33,11 @@ with open('README.rst') as fh:
|
||||
|
||||
setup(
|
||||
name='wagtail-personalisation',
|
||||
version='0.9.0',
|
||||
version='0.12.0',
|
||||
description='A Wagtail add-on for showing personalized content',
|
||||
author='Lab Digital BV',
|
||||
author='Lab Digital BV and others',
|
||||
author_email='opensource@labdigital.nl',
|
||||
url='http://labdigital.nl',
|
||||
url='https://labdigital.nl/',
|
||||
install_requires=install_requires,
|
||||
tests_require=tests_require,
|
||||
extras_require={
|
||||
@ -54,17 +55,10 @@ setup(
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
'Framework :: Django :: 2',
|
||||
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
||||
],
|
||||
)
|
||||
|
@ -9,7 +9,7 @@ from wagtail_personalisation.rules import AbstractBaseRule
|
||||
from wagtail_personalisation.utils import create_segment_dictionary
|
||||
|
||||
|
||||
class BaseSegmentsAdapter(object):
|
||||
class BaseSegmentsAdapter:
|
||||
"""Base segments adapter."""
|
||||
|
||||
def __init__(self, request):
|
||||
@ -66,34 +66,48 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
self.request.session.setdefault('segments', [])
|
||||
self._segment_cache = None
|
||||
|
||||
def get_segments(self):
|
||||
"""Return the persistent segments stored in the request session.
|
||||
|
||||
:returns: The segments in the request session
|
||||
:rtype: list of wagtail_personalisation.models.Segment or empty list
|
||||
|
||||
"""
|
||||
if self._segment_cache is not None:
|
||||
return self._segment_cache
|
||||
|
||||
raw_segments = self.request.session['segments']
|
||||
segment_ids = [segment['id'] for segment in raw_segments]
|
||||
|
||||
def _segments(self, ids=None):
|
||||
if not ids:
|
||||
ids = []
|
||||
segments = (
|
||||
Segment.objects
|
||||
.enabled()
|
||||
.filter(persistent=True)
|
||||
.in_bulk(segment_ids))
|
||||
.filter(pk__in=ids)
|
||||
)
|
||||
return segments
|
||||
|
||||
retval = [segments[pk] for pk in segment_ids if pk in segments]
|
||||
self._segment_cache = retval
|
||||
return retval
|
||||
def get_segments(self, key="segments"):
|
||||
"""Return the persistent segments stored in the request session.
|
||||
|
||||
def set_segments(self, segments):
|
||||
:param key: The key under which the segments are stored
|
||||
:type key: String
|
||||
:returns: The segments in the request session
|
||||
:rtype: list of wagtail_personalisation.models.Segment or empty list
|
||||
|
||||
"""
|
||||
if key == "segments" and self._segment_cache is not None:
|
||||
return self._segment_cache
|
||||
|
||||
if key not in self.request.session:
|
||||
return []
|
||||
raw_segments = self.request.session[key]
|
||||
segment_ids = [segment['id'] for segment in raw_segments]
|
||||
|
||||
segments = self._segments(ids=segment_ids)
|
||||
|
||||
result = list(segments)
|
||||
if key == "segments":
|
||||
self._segment_cache = result
|
||||
return result
|
||||
|
||||
def set_segments(self, segments, key="segments"):
|
||||
"""Set the currently active segments
|
||||
|
||||
: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 = []
|
||||
@ -108,8 +122,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
serialized_segments.append(serialized)
|
||||
segment_ids.add(segment.pk)
|
||||
|
||||
self.request.session['segments'] = serialized_segments
|
||||
self._segment_cache = cache_segments
|
||||
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.
|
||||
@ -120,9 +135,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
:rtype: wagtail_personalisation.models.Segment or None
|
||||
|
||||
"""
|
||||
for segment in self.get_segments():
|
||||
if segment.pk == segment_id:
|
||||
return segment
|
||||
segments = self._segments(ids=[segment_id])
|
||||
if segments.exists():
|
||||
return segments.get()
|
||||
|
||||
def add_page_visit(self, page):
|
||||
"""Mark the page as visited by the user"""
|
||||
@ -132,18 +147,19 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
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': self.request.path,
|
||||
'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.path if page else self.request.path
|
||||
path = page.url_path if page else self.request.path
|
||||
visit_count = self.request.session.setdefault('visit_count', [])
|
||||
for visit in visit_count:
|
||||
if visit['path'] == path:
|
||||
@ -170,21 +186,40 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
rule_models = AbstractBaseRule.get_descendant_models()
|
||||
|
||||
current_segments = self.get_segments()
|
||||
excluded_segments = self.get_segments("excluded_segments")
|
||||
current_segments = list(
|
||||
set(current_segments) - set(excluded_segments)
|
||||
)
|
||||
|
||||
# Run tests on all remaining enabled segments to verify applicability.
|
||||
additional_segments = []
|
||||
for segment in enabled_segments:
|
||||
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:
|
||||
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()
|
||||
|
||||
|
||||
|
@ -13,4 +13,6 @@ urlpatterns = [
|
||||
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,7 +1,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailcore import blocks
|
||||
from wagtail.core import blocks
|
||||
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import Segment
|
||||
|
138
src/wagtail_personalisation/forms.py
Normal file
138
src/wagtail_personalisation/forms.py
Normal file
@ -0,0 +1,138 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils.lru_cache import lru_cache
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.admin.forms import WagtailAdminModelForm
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def user_from_data(user_id):
|
||||
User = get_user_model()
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
class SegmentAdminForm(WagtailAdminModelForm):
|
||||
|
||||
def count_matching_users(self, rules, match_any):
|
||||
""" Calculates how many users match the given static rules
|
||||
"""
|
||||
count = 0
|
||||
|
||||
static_rules = [rule for rule in rules if rule.static]
|
||||
|
||||
if not static_rules:
|
||||
return count
|
||||
|
||||
User = get_user_model()
|
||||
users = User.objects.filter(is_active=True, is_staff=False)
|
||||
|
||||
for user in users.iterator():
|
||||
if match_any:
|
||||
if any(rule.test_user(None, user) for rule in static_rules):
|
||||
count += 1
|
||||
elif all(rule.test_user(None, user) for rule in static_rules):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(SegmentAdminForm, self).clean()
|
||||
Segment = self._meta.model
|
||||
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
for form in formset
|
||||
if form not in formset.deleted_forms
|
||||
]
|
||||
consistent = rules and Segment.all_static(rules)
|
||||
|
||||
if cleaned_data.get('type') == Segment.TYPE_STATIC and not cleaned_data.get('count') and not consistent:
|
||||
self.add_error('count', _('Static segments with non-static compatible rules must include a count.'))
|
||||
|
||||
if self.instance.id and self.instance.is_static:
|
||||
if self.has_changed():
|
||||
self.add_error_to_fields(self, excluded=['name', 'enabled'])
|
||||
|
||||
for formset in self.formsets.values():
|
||||
if formset.has_changed():
|
||||
for form in formset:
|
||||
if form not in formset.deleted_forms:
|
||||
self.add_error_to_fields(form)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def add_error_to_fields(self, form, excluded=list()):
|
||||
for field in form.changed_data:
|
||||
if field not in excluded:
|
||||
form.add_error(field, _('Cannot update a static segment'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
|
||||
if not self.instance.is_static:
|
||||
self.instance.count = 0
|
||||
|
||||
if is_new and self.instance.is_static and not self.instance.all_rules_static:
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
for form in formset
|
||||
if form not in formset.deleted_forms
|
||||
]
|
||||
self.instance.matched_users_count = self.count_matching_users(
|
||||
rules, self.instance.match_any)
|
||||
self.instance.matched_count_updated_at = datetime.now()
|
||||
|
||||
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
|
||||
|
||||
if is_new and instance.is_static and instance.all_rules_static:
|
||||
from .adapters import get_segment_adapter
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.session = SessionStore()
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
users_to_add = []
|
||||
users_to_exclude = []
|
||||
|
||||
User = get_user_model()
|
||||
users = User.objects.filter(is_active=True, is_staff=False)
|
||||
|
||||
matched_count = 0
|
||||
for user in users.iterator():
|
||||
request.user = user
|
||||
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
|
||||
if passes:
|
||||
matched_count += 1
|
||||
if instance.count == 0 or len(users_to_add) < instance.count:
|
||||
if instance.randomise_into_segment():
|
||||
users_to_add.append(user)
|
||||
else:
|
||||
users_to_exclude.append(user)
|
||||
|
||||
instance.matched_users_count = matched_count
|
||||
instance.matched_count_updated_at = datetime.now()
|
||||
instance.static_users.add(*users_to_add)
|
||||
instance.excluded_users.add(*users_to_exclude)
|
||||
|
||||
return instance
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
media = super(SegmentAdminForm, self).media
|
||||
media.add_js(
|
||||
[static('js/segment_form_control.js')]
|
||||
)
|
||||
return media
|
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0030_index_on_pagerevision_created_at'),
|
||||
('wagtailcore', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -2,14 +2,14 @@
|
||||
# Generated by Django 1.11.1 on 2017-05-31 14:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||
('wagtailcore', '0001_initial'),
|
||||
('wagtail_personalisation', '0010_auto_20170531_1101'),
|
||||
]
|
||||
|
||||
|
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-10-17 11:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sessions', '0001_initial'),
|
||||
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='count',
|
||||
field=models.PositiveSmallIntegerField(default=0, help_text='If this number is set for a static segment users will be added to the set until the number is reached. After this no more users will be added.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
field=models.ManyToManyField(to='sessions.Session'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('dynamic', 'Dynamic'), ('static', 'Static')], default='dynamic', help_text='\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ', max_length=20),
|
||||
),
|
||||
]
|
26
src/wagtail_personalisation/migrations/0015_static_users.py
Normal file
26
src/wagtail_personalisation/migrations/0015_static_users.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-11-01 15:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0013_add_dynamic_static_to_segment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='static_users',
|
||||
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-01-25 09:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0015_static_users'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='matched_count_updated_at',
|
||||
field=models.DateTimeField(editable=False, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='matched_users_count',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
),
|
||||
]
|
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.8 on 2018-01-31 16:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0016_auto_20180125_0918'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='randomisation_percent',
|
||||
field=models.PositiveSmallIntegerField(blank=True, default=None, help_text='If this number is set each user matching the rules will have this percentage chance of being placed in the segment.', null=True, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-09 08:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0017_segment_randomisation_percent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='excluded_users',
|
||||
field=models.ManyToManyField(help_text='Users that matched the rules but were excluded from the segment for some reason e.g. randomisation', related_name='excluded_segments', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
@ -1,19 +1,24 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import itertools
|
||||
import random
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from modelcluster.models import ClusterableModel
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
from wagtail.admin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.core.models import Page
|
||||
|
||||
from wagtail_personalisation.rules import AbstractBaseRule
|
||||
from wagtail_personalisation.utils import count_active_days
|
||||
|
||||
from .forms import SegmentAdminForm
|
||||
|
||||
|
||||
class SegmentQuerySet(models.QuerySet):
|
||||
def enabled(self):
|
||||
@ -31,6 +36,14 @@ class Segment(ClusterableModel):
|
||||
(STATUS_DISABLED, _('Disabled')),
|
||||
)
|
||||
|
||||
TYPE_DYNAMIC = 'dynamic'
|
||||
TYPE_STATIC = 'static'
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_DYNAMIC, _('Dynamic')),
|
||||
(TYPE_STATIC, _('Static')),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
edit_date = models.DateTimeField(auto_now=True)
|
||||
@ -45,9 +58,54 @@ class Segment(ClusterableModel):
|
||||
default=False,
|
||||
help_text=_("Should the segment match all the rules or just one of them?")
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=20,
|
||||
choices=TYPE_CHOICES,
|
||||
default=TYPE_DYNAMIC,
|
||||
help_text=mark_safe(_("""
|
||||
</br></br><strong>Dynamic:</strong> Users in this segment will change
|
||||
as more or less meet the rules specified in the segment.
|
||||
</br><strong>Static:</strong> If the segment contains only static
|
||||
compatible rules the segment will contain the members that pass
|
||||
those rules when the segment is created. Mixed static segments or
|
||||
those containing entirely non static compatible rules will be
|
||||
populated using the count variable.
|
||||
"""))
|
||||
)
|
||||
count = models.PositiveSmallIntegerField(
|
||||
default=0,
|
||||
help_text=_(
|
||||
"If this number is set for a static segment users will be added to the "
|
||||
"set until the number is reached. After this no more users will be added."
|
||||
)
|
||||
)
|
||||
static_users = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
)
|
||||
excluded_users = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
help_text=_("Users that matched the rules but were excluded from the "
|
||||
"segment for some reason e.g. randomisation"),
|
||||
related_name="excluded_segments"
|
||||
)
|
||||
|
||||
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
|
||||
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
|
||||
|
||||
randomisation_percent = models.PositiveSmallIntegerField(
|
||||
null=True, blank=True, default=None,
|
||||
help_text=_(
|
||||
"If this number is set each user matching the rules will "
|
||||
"have this percentage chance of being placed in the segment."
|
||||
), validators=[
|
||||
MaxValueValidator(100),
|
||||
MinValueValidator(0)
|
||||
])
|
||||
|
||||
objects = SegmentQuerySet.as_manager()
|
||||
|
||||
base_form_class = SegmentAdminForm
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Segment.panels = [
|
||||
MultiFieldPanel([
|
||||
@ -57,11 +115,17 @@ class Segment(ClusterableModel):
|
||||
FieldPanel('persistent'),
|
||||
]),
|
||||
FieldPanel('match_any'),
|
||||
FieldPanel('type', widget=forms.RadioSelect),
|
||||
FieldPanel('count', classname='count_field'),
|
||||
FieldPanel('randomisation_percent', classname='percent_field'),
|
||||
], heading="Segment"),
|
||||
MultiFieldPanel([
|
||||
InlinePanel(
|
||||
"{}_related".format(rule_model._meta.db_table),
|
||||
label=rule_model._meta.verbose_name,
|
||||
label='{}{}'.format(
|
||||
rule_model._meta.verbose_name,
|
||||
' ({})'.format(_('Static compatible')) if rule_model.static else ''
|
||||
),
|
||||
) for rule_model in AbstractBaseRule.__subclasses__()
|
||||
], heading=_("Rules")),
|
||||
]
|
||||
@ -71,6 +135,23 @@ class Segment(ClusterableModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_static(self):
|
||||
return self.type == self.TYPE_STATIC
|
||||
|
||||
@classmethod
|
||||
def all_static(cls, rules):
|
||||
return all(rule.static for rule in rules)
|
||||
|
||||
@property
|
||||
def all_rules_static(self):
|
||||
rules = self.get_rules()
|
||||
return rules and self.all_static(rules)
|
||||
|
||||
@property
|
||||
def is_full(self):
|
||||
return self.static_users.count() >= self.count
|
||||
|
||||
def encoded_name(self):
|
||||
"""Return a string with a slug for the segment."""
|
||||
return slugify(self.name.lower())
|
||||
@ -81,15 +162,11 @@ class Segment(ClusterableModel):
|
||||
|
||||
def get_used_pages(self):
|
||||
"""Return the pages that have variants using this segment."""
|
||||
pages = list(PersonalisablePageMetadata.objects.filter(segment=self))
|
||||
|
||||
return pages
|
||||
return PersonalisablePageMetadata.objects.filter(segment=self)
|
||||
|
||||
def get_created_variants(self):
|
||||
"""Return the variants using this segment."""
|
||||
pages = Page.objects.filter(_personalisable_page_metadata__segment=self)
|
||||
|
||||
return pages
|
||||
return Page.objects.filter(_personalisable_page_metadata__segment=self)
|
||||
|
||||
def get_rules(self):
|
||||
"""Retrieve all rules in the segment."""
|
||||
@ -107,6 +184,19 @@ class Segment(ClusterableModel):
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
def randomise_into_segment(self):
|
||||
""" Returns True if randomisation_percent is not set or it generates
|
||||
a random number less than the randomisation_percent
|
||||
This is so there is some randomisation in which users are added to the
|
||||
segment
|
||||
"""
|
||||
if self.randomisation_percent is None:
|
||||
return True
|
||||
|
||||
if random.randint(1, 100) <= self.randomisation_percent:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PersonalisablePageMetadata(ClusterableModel):
|
||||
"""The personalisable page model. Allows creation of variants with linked
|
||||
@ -120,10 +210,13 @@ class PersonalisablePageMetadata(ClusterableModel):
|
||||
)
|
||||
|
||||
variant = models.OneToOneField(
|
||||
Page, related_name='_personalisable_page_metadata')
|
||||
Page, related_name='_personalisable_page_metadata',
|
||||
on_delete=models.CASCADE)
|
||||
|
||||
segment = models.ForeignKey(
|
||||
Segment, related_name='page_metadata', null=True, blank=True)
|
||||
Segment, related_name='page_metadata',
|
||||
on_delete=models.SET_NULL,
|
||||
null=True, blank=True)
|
||||
|
||||
@cached_property
|
||||
def has_variants(self):
|
||||
@ -194,7 +287,7 @@ class PersonalisablePageMetadata(ClusterableModel):
|
||||
return Segment.objects.none()
|
||||
|
||||
|
||||
class PersonalisablePageMixin(object):
|
||||
class PersonalisablePageMixin:
|
||||
"""The personalisable page model. Allows creation of variants with linked
|
||||
segments.
|
||||
|
||||
|
@ -2,22 +2,30 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from modelcluster.fields import ParentalKey
|
||||
from user_agents import parse
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
from wagtail.admin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel, PageChooserPanel)
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class AbstractBaseRule(models.Model):
|
||||
"""Base for creating rules to segment users with."""
|
||||
icon = 'fa-circle-o'
|
||||
static = False
|
||||
|
||||
segment = ParentalKey(
|
||||
'wagtail_personalisation.Segment',
|
||||
@ -190,6 +198,7 @@ class VisitCountRule(AbstractBaseRule):
|
||||
|
||||
"""
|
||||
icon = 'fa-calculator'
|
||||
static = True
|
||||
|
||||
OPERATOR_CHOICES = (
|
||||
('more_than', _("More than")),
|
||||
@ -218,16 +227,46 @@ class VisitCountRule(AbstractBaseRule):
|
||||
class Meta:
|
||||
verbose_name = _('Visit count Rule')
|
||||
|
||||
def test_user(self, request):
|
||||
def _get_user_session(self, user):
|
||||
sessions = Session.objects.iterator()
|
||||
for session in sessions:
|
||||
session_data = session.get_decoded()
|
||||
if session_data.get('_auth_user_id') == str(user.id):
|
||||
return SessionStore(session_key=session.session_key)
|
||||
return SessionStore()
|
||||
|
||||
def test_user(self, request, user=None):
|
||||
# Local import for cyclic import
|
||||
from wagtail_personalisation.adapters import (
|
||||
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
|
||||
|
||||
# Django formsets don't honour 'required' fields so check rule is valid
|
||||
try:
|
||||
self.counted_page
|
||||
except ObjectDoesNotExist:
|
||||
return False
|
||||
|
||||
if user:
|
||||
# Create a fake request so we can use the adapter
|
||||
request = RequestFactory().get('/')
|
||||
request.user = user
|
||||
|
||||
# If we're using the session adapter check for an active session
|
||||
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
|
||||
request.session = self._get_user_session(user)
|
||||
else:
|
||||
request.session = SessionStore()
|
||||
|
||||
elif not request:
|
||||
# Return false if we don't have a user or a request
|
||||
return False
|
||||
|
||||
operator = self.operator
|
||||
segment_count = self.count
|
||||
|
||||
# Local import for cyclic import
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
visit_count = adapter.get_visit_count()
|
||||
visit_count = adapter.get_visit_count(self.counted_page)
|
||||
if visit_count and operator == "more_than":
|
||||
if visit_count > segment_count:
|
||||
return True
|
||||
@ -250,6 +289,28 @@ class VisitCountRule(AbstractBaseRule):
|
||||
),
|
||||
}
|
||||
|
||||
def get_column_header(self):
|
||||
return "Visit count - %s" % self.counted_page
|
||||
|
||||
def get_user_info_string(self, user):
|
||||
# Local import for cyclic import
|
||||
from wagtail_personalisation.adapters import (
|
||||
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
|
||||
|
||||
# Create a fake request so we can use the adapter
|
||||
request = RequestFactory().get('/')
|
||||
request.user = user
|
||||
|
||||
# If we're using the session adapter check for an active session
|
||||
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
|
||||
request.session = self._get_user_session(user)
|
||||
else:
|
||||
request.session = SessionStore()
|
||||
|
||||
adapter = get_segment_adapter(request)
|
||||
visit_count = adapter.get_visit_count(self.counted_page)
|
||||
return str(visit_count)
|
||||
|
||||
|
||||
class QueryRule(AbstractBaseRule):
|
||||
"""Query rule to segment users based on matching queries.
|
||||
@ -258,7 +319,7 @@ class QueryRule(AbstractBaseRule):
|
||||
present in the request query.
|
||||
|
||||
"""
|
||||
icon = 'fa-link-o'
|
||||
icon = 'fa-link'
|
||||
|
||||
parameter = models.SlugField(_("The query parameter to search for"),
|
||||
max_length=20)
|
||||
|
@ -1,2 +1,2 @@
|
||||
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li{display:inline-block;margin-bottom:5px}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
|
||||
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block .inspect_container .inspect .stat_card{display:inline-block;margin-bottom:5px;margin-right:10px}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
|
||||
/*# sourceMappingURL=dashboard.css.map*/
|
@ -1 +1 @@
|
||||
{"version":3,"sources":["webpack:///./scss/dashboard.scss"],"names":[],"mappings":"AAAA,cACI,kBACA,kBAAmB,CAGvB,iBACI,cACA,eAAgB,CAGpB,wBACI,cACA,WACA,8BAAsB,sBACtB,kBACA,uBACA,iBACA,kBACA,mBACA,yBACA,kBACA,sBACA,+DAAkE,uDAClE,2GAA8F,2UAC9F,cAAe,CAGnB,0FAEI,UAAY,CAGZ,2BACI,qBACA,UAAW,CAGnB,uCACI,iBAAkB,CAGlB,uCACI,gBACA,SACA,SAAU,CAGV,0CACI,WACA,iBAAkB,CAGtB,qDACI,cAAe,CAG3B,mCACI,yBAA0B,CAG9B,8BACI,sBACA,uEAAkE,+DAGtE,yBACI,wBACI,WACA,cAAe,CAClB,CAGL,2CACI,oBAAa,iCACb,8BAAmB,uEACnB,qBAAiB,iBACjB,yBAA8B,oDAC9B,0BAAoB,2CACpB,kBAAmB,CAGvB,oDACI,cACA,WACA,uBACA,UACA,SACA,eAAgB,CAGhB,yCACI,kBACA,qBAAsB,CAG1B,uDACI,qBACA,iBAAkB,CAGtB,4DACI,cACA,eACA,gBACA,aACA,wBAAyB,CAG7B,2DACI,kBACA,8BAAsB,sBACtB,WACA,sBACA,sBACA,sBACA,gBACA,qBACA,qBACA,iBAAkB,CAG1B,oDACI,cACA,kBACA,wBACA,kBACA,QACA,mCAA2B,2BAC3B,cACA,eACA,eAAgB","file":"../css/dashboard.css","sourcesContent":[".nice-padding {\n padding-left: 50px;\n padding-right: 50px;\n}\n\n.block_container {\n display: block;\n margin-top: 30px;\n}\n\n.block_container .block {\n display: block;\n float: left;\n box-sizing: border-box;\n position: relative;\n width: calc(50% - 10px);\n min-height: 216px;\n padding: 10px 20px;\n margin-bottom: 20px;\n border: 1px solid #d9d9d9;\n border-radius: 3px;\n background-color: #fff;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);\n cursor: pointer;\n}\n\n.block_container .block--disabled h2,\n.block_container .block--disabled .inspect_container {\n opacity: 0.5;\n}\n\n .block_container .block h2 {\n display: inline-block;\n width: auto;\n }\n\n.block_container .block:nth-child(odd) {\n margin-right: 20px;\n}\n\n .block_container .block .block_actions {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n .block_container .block .block_actions li {\n float: left;\n margin-right: 10px;\n }\n\n .block_container .block .block_actions li:last-child {\n margin-right: 0;\n }\n\n.block_container .block.suggestion {\n border: 1px dashed #d9d9d9;\n}\n\n.block_container .block:hover {\n border: 1px solid #fff;\n box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);\n}\n\n@media (max-width: 699px) {\n .block_container .block {\n width: 100%;\n margin-right: 0;\n }\n}\n\n.block_container .block .inspect_container {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: stretch;\n margin-bottom: 10px;\n}\n\n.block_container .block .inspect_container .inspect {\n display: block;\n float: left;\n width: calc(50% - 10px);\n padding: 0;\n margin: 0;\n list-style: none;\n}\n\n .block_container .block span.icon::before {\n margin-right: 0.3em;\n vertical-align: bottom;\n }\n\n .block_container .block .inspect_container .inspect li {\n display: inline-block;\n margin-bottom: 5px;\n }\n\n .block_container .block .inspect_container .inspect li span {\n display: block;\n font-size: 20px;\n font-weight: bold;\n margin: 5px 0;\n overflow-wrap: break-word;\n }\n\n .block_container .block .inspect_container .inspect li pre {\n position: relative;\n box-sizing: border-box;\n width: auto;\n background-color: #eee;\n border: 1px solid #ccc;\n margin: 5px 0 5px 21px;\n padding: 2px 5px;\n word-wrap: break-word;\n word-break: break-all;\n border-radius: 3px;\n }\n\n.block_container .block.suggestion .suggestive_text {\n display: block;\n position: absolute;\n width: calc(100% - 40px);\n text-align: center;\n top: 50%;\n transform: translateY(-50%);\n color: #d9d9d9;\n font-size: 20px;\n font-weight: 100;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./scss/dashboard.scss"],"sourceRoot":""}
|
||||
{"version":3,"sources":["webpack:///./scss/dashboard.scss"],"names":[],"mappings":"AAAA,cACI,kBACA,kBAAmB,CAGvB,iBACI,cACA,eAAgB,CAGpB,wBACI,cACA,WACA,8BAAsB,sBACtB,kBACA,uBACA,iBACA,kBACA,mBACA,yBACA,kBACA,sBACA,+DAAkE,uDAClE,2GAA8F,2UAC9F,cAAe,CAGnB,0FAEI,UAAY,CAGZ,2BACI,qBACA,UAAW,CAGnB,uCACI,iBAAkB,CAGlB,uCACI,gBACA,SACA,SAAU,CAGV,0CACI,WACA,iBAAkB,CAGtB,qDACI,cAAe,CAG3B,mCACI,yBAA0B,CAG9B,8BACI,sBACA,uEAAkE,+DAGtE,yBACI,wBACI,WACA,cAAe,CAClB,CAGL,2CACI,oBAAa,iCACb,8BAAmB,uEACnB,qBAAiB,iBACjB,yBAA8B,oDAC9B,0BAAoB,2CACpB,kBAAmB,CAGvB,oDACI,cACA,WACA,uBACA,UACA,SACA,eAAgB,CAMnB,+DAJO,qBACA,kBACA,iBAAkB,CAItB,yCACI,kBACA,qBAAsB,CAG1B,4DACI,cACA,eACA,gBACA,aACA,wBAAyB,CAG7B,2DACI,kBACA,8BAAsB,sBACtB,WACA,sBACA,sBACA,sBACA,gBACA,qBACA,qBACA,iBAAkB,CAG1B,oDACI,cACA,kBACA,wBACA,kBACA,QACA,mCAA2B,2BAC3B,cACA,eACA,eAAgB","file":"../css/dashboard.css","sourcesContent":[".nice-padding {\n padding-left: 50px;\n padding-right: 50px;\n}\n\n.block_container {\n display: block;\n margin-top: 30px;\n}\n\n.block_container .block {\n display: block;\n float: left;\n box-sizing: border-box;\n position: relative;\n width: calc(50% - 10px);\n min-height: 216px;\n padding: 10px 20px;\n margin-bottom: 20px;\n border: 1px solid #d9d9d9;\n border-radius: 3px;\n background-color: #fff;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);\n cursor: pointer;\n}\n\n.block_container .block--disabled h2,\n.block_container .block--disabled .inspect_container {\n opacity: 0.5;\n}\n\n .block_container .block h2 {\n display: inline-block;\n width: auto;\n }\n\n.block_container .block:nth-child(odd) {\n margin-right: 20px;\n}\n\n .block_container .block .block_actions {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n .block_container .block .block_actions li {\n float: left;\n margin-right: 10px;\n }\n\n .block_container .block .block_actions li:last-child {\n margin-right: 0;\n }\n\n.block_container .block.suggestion {\n border: 1px dashed #d9d9d9;\n}\n\n.block_container .block:hover {\n border: 1px solid #fff;\n box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);\n}\n\n@media (max-width: 699px) {\n .block_container .block {\n width: 100%;\n margin-right: 0;\n }\n}\n\n.block_container .block .inspect_container {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: stretch;\n margin-bottom: 10px;\n}\n\n.block_container .block .inspect_container .inspect {\n display: block;\n float: left;\n width: calc(50% - 10px);\n padding: 0;\n margin: 0;\n list-style: none;\n .stat_card {\n display: inline-block;\n margin-bottom: 5px;\n margin-right: 10px;\n }\n}\n\n .block_container .block span.icon::before {\n margin-right: 0.3em;\n vertical-align: bottom;\n }\n\n .block_container .block .inspect_container .inspect li span {\n display: block;\n font-size: 20px;\n font-weight: bold;\n margin: 5px 0;\n overflow-wrap: break-word;\n }\n\n .block_container .block .inspect_container .inspect li pre {\n position: relative;\n box-sizing: border-box;\n width: auto;\n background-color: #eee;\n border: 1px solid #ccc;\n margin: 5px 0 5px 21px;\n padding: 2px 5px;\n word-wrap: break-word;\n word-break: break-all;\n border-radius: 3px;\n }\n\n.block_container .block.suggestion .suggestive_text {\n display: block;\n position: absolute;\n width: calc(100% - 40px);\n text-align: center;\n top: 50%;\n transform: translateY(-50%);\n color: #d9d9d9;\n font-size: 20px;\n font-weight: 100;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./scss/dashboard.scss"],"sourceRoot":""}
|
0
src/wagtail_personalisation/static/img/.gitkeep
Normal file
0
src/wagtail_personalisation/static/img/.gitkeep
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1,20 @@
|
||||
(function($) {
|
||||
$(document).ready( () => {
|
||||
var count = $('.count_field');
|
||||
var typeRadio = $('input:radio[name="type"]');
|
||||
|
||||
var updateCountDispay = function(value) {
|
||||
if (value == 'dynamic') {
|
||||
count.slideUp(250);
|
||||
} else {
|
||||
count.slideDown(250);
|
||||
}
|
||||
};
|
||||
|
||||
updateCountDispay(typeRadio.filter(':checked').val());
|
||||
|
||||
typeRadio.change( event => {
|
||||
updateCountDispay(event.target.value);
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
@ -22,24 +22,38 @@
|
||||
<div class="nice-padding block_container">
|
||||
{% if all_count %}
|
||||
{% for segment in object_list %}
|
||||
<div class="block block--{{ segment.status }}" onclick="location.href = 'edit/{{ segment.pk }}'">
|
||||
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
|
||||
<h2>{{ segment }}</h2>
|
||||
|
||||
<div class="inspect_container">
|
||||
<ul class="inspect segment_stats">
|
||||
<li class="visit_stat">
|
||||
<li class="stat_card">
|
||||
{% trans "This segment has been visited" %}
|
||||
<span class="icon icon-fa-rocket">{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
|
||||
</li>
|
||||
<li class="days_stat">
|
||||
<li class="stat_card">
|
||||
{% trans "This segment has been active for" %}
|
||||
<span class="icon icon-fa-calendar">{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
|
||||
</li>
|
||||
{% if segment.is_static %}
|
||||
<li class="stat_card">
|
||||
{% trans "This segment is Static" %}
|
||||
<span class="icon icon-fa-user">
|
||||
{{ segment.static_users.count|localize }}
|
||||
{% if segment.static_users.count < segment.count %}
|
||||
/ {{ segment.count }} {% trans "member" %}{{ segment.count|pluralize }}
|
||||
{% else %}
|
||||
{% trans "member" %}{{ segment.count|pluralize }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<hr />
|
||||
|
||||
<ul class="inspect segment_rules">
|
||||
<li class="match_state {{ segment.match_any|yesno:"any,all" }}">
|
||||
<li class="stat_card {{ segment.match_any|yesno:"any,all" }}">
|
||||
{% trans "The visitor must match" %}
|
||||
{% if segment.match_any %}
|
||||
<span class="icon icon-fa-cube">{% trans "Any rule" %}</span>
|
||||
@ -48,7 +62,7 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
<li class="persistent_state {{ segment.persistent|yesno:"persistent,fleeting" }}">
|
||||
<li class="stat_card {{ segment.persistent|yesno:"persistent,fleeting" }}">
|
||||
{% trans "The persistence of this segment is" %}
|
||||
{% if segment.persistent %}
|
||||
<span class="icon icon-fa-bookmark" title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
|
||||
@ -57,8 +71,15 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
{% if segment.randomisation_percent is not None %}
|
||||
<li class="stat_card">
|
||||
<span>{{ segment.randomisation_percent }} %</span>
|
||||
{% trans "Chance that visitors matching the rules are added to the segment" %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for rule in segment.get_rules %}
|
||||
<li class="{{ rule.encoded_name }}">
|
||||
<li class="stat_card {{ rule.encoded_name }}">
|
||||
{{ rule.description.title }}
|
||||
{% if rule.description.code %}
|
||||
<pre>{{ rule.description.value }}</pre>
|
||||
@ -67,6 +88,11 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if segment.matched_users_count > 0 %}
|
||||
<li class="stat_card">
|
||||
<span class="icon icon-fa-user"> {{ segment.matched_users_count }} {% trans "user" %}{{ segment.matched_users_count|pluralize }}</span> {% trans "were possible matches for this segment at creation" %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -77,7 +103,10 @@
|
||||
{% elif segment.status == segment.STATUS_ENABLED %}
|
||||
<li><a href="{% url 'segment:toggle' 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>
|
||||
<li><a href="{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
||||
{% if segment.is_static %}
|
||||
<li><a href="{% url 'segment:segment_user_data' segment.pk %}" title="{% trans "Download user info" %}">download users csv</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import time
|
||||
|
||||
from django.db.models import F
|
||||
from django.template.base import FilterExpression, kwarg_re
|
||||
from django.utils import timezone
|
||||
|
||||
@ -93,3 +94,16 @@ def parse_tag(token, parser):
|
||||
args.append(FilterExpression(bit, parser))
|
||||
|
||||
return (tag_name, args, kwargs)
|
||||
|
||||
|
||||
def exclude_variants(pages):
|
||||
"""Checks if page is not a variant
|
||||
|
||||
:param pages: Set of pages to check
|
||||
:type pages: QuerySet
|
||||
:return: Queryset of pages that aren't variants
|
||||
:rtype: QuerySet
|
||||
"""
|
||||
return pages.filter(
|
||||
personalisable_canonical_metadata__canonical_page_id=F(
|
||||
'personalisable_canonical_metadata__variant__id'))
|
||||
|
@ -1,12 +1,16 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import csv
|
||||
|
||||
from django import forms
|
||||
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, reverse
|
||||
from django.http import (
|
||||
HttpResponse, HttpResponseForbidden, HttpResponseRedirect)
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
||||
from wagtail.contrib.modeladmin.views import IndexView
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.core.models import Page
|
||||
|
||||
from wagtail_personalisation.models import Segment
|
||||
|
||||
@ -138,3 +142,32 @@ def copy_page_view(request, page_id, segment_id):
|
||||
return HttpResponseRedirect(edit_url)
|
||||
|
||||
return HttpResponseForbidden()
|
||||
|
||||
|
||||
# CSV download views
|
||||
def segment_user_data(request, segment_id):
|
||||
if request.user.has_perm('wagtailadmin.access_admin'):
|
||||
segment = get_object_or_404(Segment, pk=segment_id)
|
||||
|
||||
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||
response['Content-Disposition'] = \
|
||||
'attachment;filename=segment-%s-users.csv' % str(segment_id)
|
||||
|
||||
headers = ['Username']
|
||||
for rule in segment.get_rules():
|
||||
if rule.static:
|
||||
headers.append(rule.get_column_header())
|
||||
|
||||
writer = csv.writer(response)
|
||||
writer.writerow(headers)
|
||||
|
||||
for user in segment.static_users.all():
|
||||
row = [user.username]
|
||||
for rule in segment.get_rules():
|
||||
if rule.static:
|
||||
row.append(rule.get_user_info_string(user))
|
||||
writer.writerow(row)
|
||||
|
||||
return response
|
||||
|
||||
return HttpResponseForbidden()
|
||||
|
@ -3,16 +3,17 @@ from __future__ import absolute_import, unicode_literals
|
||||
import logging
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.defaultfilters import pluralize
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailadmin.site_summary import SummaryItem
|
||||
from wagtail.wagtailadmin.widgets import Button, ButtonWithDropdownFromHook
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem
|
||||
from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook
|
||||
from wagtail.core import hooks
|
||||
from wagtail.core.models import Page
|
||||
|
||||
from wagtail_personalisation import admin_urls, models
|
||||
from wagtail_personalisation import admin_urls, models, utils
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import PersonalisablePageMixin, Segment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -22,9 +23,7 @@ def register_admin_urls():
|
||||
"""Adds the administration urls for the personalisation apps."""
|
||||
return [
|
||||
url(r'^personalisation/', include(
|
||||
admin_urls,
|
||||
app_name='wagtail_personalisation',
|
||||
namespace='wagtail_personalisation')),
|
||||
admin_urls, namespace='wagtail_personalisation')),
|
||||
]
|
||||
|
||||
|
||||
@ -34,7 +33,7 @@ def set_visit_count(page, request, serve_args, serve_kwargs):
|
||||
to a segment.
|
||||
|
||||
:param page: The page being served
|
||||
:type page: wagtail.wagtailcore.models.Page
|
||||
:type page: wagtail.core.models.Page
|
||||
:param request: The http request
|
||||
:type request: django.http.HttpRequest
|
||||
|
||||
@ -48,7 +47,7 @@ def segment_user(page, request, serve_args, serve_kwargs):
|
||||
"""Apply a segment to a visitor before serving the page.
|
||||
|
||||
:param page: The page being served
|
||||
:type page: wagtail.wagtailcore.models.Page
|
||||
:type page: wagtail.core.models.Page
|
||||
:param request: The http request
|
||||
:type request: django.http.HttpRequest
|
||||
|
||||
@ -62,16 +61,16 @@ def serve_variant(page, request, serve_args, serve_kwargs):
|
||||
"""Apply a segment to a visitor before serving the page.
|
||||
|
||||
:param page: The page being served
|
||||
:type page: wagtail.wagtailcore.models.Page
|
||||
:type page: wagtail.core.models.Page
|
||||
:param request: The http request
|
||||
:type request: django.http.HttpRequest
|
||||
:returns: A variant if one is available for the visitor's segment,
|
||||
otherwise the original page
|
||||
:rtype: wagtail.wagtailcore.models.Page
|
||||
:rtype: wagtail.core.models.Page
|
||||
|
||||
"""
|
||||
user_segments = []
|
||||
if not isinstance(page, PersonalisablePageMixin):
|
||||
if not isinstance(page, models.PersonalisablePageMixin):
|
||||
return
|
||||
|
||||
adapter = get_segment_adapter(request)
|
||||
@ -89,9 +88,7 @@ def serve_variant(page, request, serve_args, serve_kwargs):
|
||||
|
||||
@hooks.register('construct_explorer_page_queryset')
|
||||
def dont_show_variant(parent_page, pages, request):
|
||||
return [page for page in pages
|
||||
if (page.personalisation_metadata is None)
|
||||
or (page.personalisation_metadata.is_canonical)]
|
||||
return utils.exclude_variants(pages)
|
||||
|
||||
|
||||
@hooks.register('register_page_listing_buttons')
|
||||
@ -147,15 +144,33 @@ def page_listing_more_buttons(page, page_perms, is_parent=False):
|
||||
priority=200)
|
||||
|
||||
|
||||
class CorrectedPagesSummaryPanel(PagesSummaryItem):
|
||||
def get_context(self):
|
||||
context = super(CorrectedPagesSummaryPanel, self).get_context()
|
||||
|
||||
pages = utils.exclude_variants(Page.objects.all().specific())
|
||||
|
||||
context['total_pages'] = len(pages) - 1
|
||||
return context
|
||||
|
||||
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
def add_corrected_pages_summary_panel(request, items):
|
||||
"""Replaces the Pages summary panel to hide variants."""
|
||||
for index, item in enumerate(items):
|
||||
if item.__class__ is PagesSummaryItem:
|
||||
items[index] = CorrectedPagesSummaryPanel(request)
|
||||
|
||||
|
||||
class SegmentSummaryPanel(SummaryItem):
|
||||
"""The segment summary panel showing the total amount of segments on the
|
||||
site and allowing quick access to the Segment dashboard.
|
||||
|
||||
"""
|
||||
order = 500
|
||||
order = 2000
|
||||
|
||||
def render(self):
|
||||
segment_count = Segment.objects.count()
|
||||
segment_count = models.Segment.objects.count()
|
||||
target_url = reverse('wagtail_personalisation_segment_modeladmin_index')
|
||||
title = _("Segments")
|
||||
return mark_safe("""
|
||||
@ -164,11 +179,39 @@ class SegmentSummaryPanel(SummaryItem):
|
||||
</li>""".format(target_url, segment_count, title))
|
||||
|
||||
|
||||
class PersonalisedPagesSummaryPanel(PagesSummaryItem):
|
||||
order = 2100
|
||||
|
||||
def render(self):
|
||||
page_count = models.PersonalisablePageMetadata.objects.filter(
|
||||
segment__isnull=True).count()
|
||||
title = _("Personalised Page")
|
||||
return mark_safe("""
|
||||
<li class="icon icon-fa-file-o">
|
||||
<span>{}</span>{}{}
|
||||
</li>""".format(page_count, title, pluralize(page_count)))
|
||||
|
||||
|
||||
class VariantPagesSummaryPanel(PagesSummaryItem):
|
||||
order = 2200
|
||||
|
||||
def render(self):
|
||||
page_count = models.PersonalisablePageMetadata.objects.filter(
|
||||
segment__isnull=False).count()
|
||||
title = _("Variant")
|
||||
return mark_safe("""
|
||||
<li class="icon icon-fa-files-o">
|
||||
<span>{}</span>{}{}
|
||||
</li>""".format(page_count, title, pluralize(page_count)))
|
||||
|
||||
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
def add_segment_summary_panel(request, items):
|
||||
def add_personalisation_summary_panels(request, items):
|
||||
"""Adds a summary panel to the Wagtail dashboard showing the total amount
|
||||
of segments on the site and allowing quick access to the Segment
|
||||
dashboard.
|
||||
|
||||
"""
|
||||
items.append(SegmentSummaryPanel(request))
|
||||
items.append(PersonalisedPagesSummaryPanel(request))
|
||||
items.append(VariantPagesSummaryPanel(request))
|
||||
|
@ -9,7 +9,7 @@ pytest_plugins = [
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def django_db_setup(django_db_setup, django_db_blocker):
|
||||
from wagtail.wagtailcore.models import Page, Site
|
||||
from wagtail.core.models import Page, Site
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
# Remove some initial data that is brought by the tests.site module
|
||||
|
@ -8,6 +8,7 @@ from tests.site.pages import models
|
||||
|
||||
|
||||
class ContentPageFactory(PageFactory):
|
||||
parent = None
|
||||
title = 'Test page'
|
||||
slug = factory.LazyAttribute(lambda obj: slugify(obj.title))
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import factory
|
||||
from wagtail.wagtailcore.models import Site
|
||||
from wagtail.core.models import Site
|
||||
|
||||
from tests.factories.page import ContentPageFactory
|
||||
|
||||
|
@ -44,3 +44,8 @@ class RequestFactory(BaseRequestFactory):
|
||||
request.session = SessionStore()
|
||||
request._messages = FallbackStorage(request)
|
||||
return request
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user(django_user_model):
|
||||
return django_user_model.objects.create(username='user')
|
||||
|
@ -57,24 +57,23 @@ MIDDLEWARE = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
|
||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||
'wagtail.core.middleware.SiteMiddleware',
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'wagtail_personalisation',
|
||||
|
||||
'wagtail.contrib.modeladmin',
|
||||
'wagtail.wagtailsearch',
|
||||
'wagtail.wagtailsites',
|
||||
'wagtail.wagtailusers',
|
||||
'wagtail.wagtailimages',
|
||||
'wagtail.wagtaildocs',
|
||||
'wagtail.wagtailadmin',
|
||||
'wagtail.wagtailcore',
|
||||
'wagtail.search',
|
||||
'wagtail.sites',
|
||||
'wagtail.users',
|
||||
'wagtail.images',
|
||||
'wagtail.documents',
|
||||
'wagtail.admin',
|
||||
'wagtail.core',
|
||||
|
||||
'taggit',
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
import wagtail.wagtailcore.fields
|
||||
import wagtail.core.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
import wagtail_personalisation.models
|
||||
@ -14,16 +14,16 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||
('wagtailcore', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContentPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
||||
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
|
@ -1,16 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.1 on 2017-06-02 04:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import wagtail.wagtailcore.fields
|
||||
import wagtail.core.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||
('wagtailcore', '0001_initial'),
|
||||
('pages', '0001_initial'),
|
||||
]
|
||||
|
||||
@ -18,9 +17,9 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='RegularPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
||||
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
|
@ -1,9 +1,7 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.db import models
|
||||
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
||||
from wagtail.wagtailcore.fields import RichTextField
|
||||
from wagtail.wagtailcore.models import Page
|
||||
from wagtail.admin.edit_handlers import FieldPanel
|
||||
from wagtail.core.fields import RichTextField
|
||||
from wagtail.core.models import Page
|
||||
|
||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||
|
||||
|
@ -2,12 +2,12 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.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 wagtail.admin import urls as wagtailadmin_urls
|
||||
from wagtail.core import urls as wagtail_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^django-admin/', include(admin.site.urls)),
|
||||
url(r'^django-admin/', admin.site.urls),
|
||||
|
||||
url(r'^admin/', include(wagtailadmin_urls)),
|
||||
url(r'^documents/', include(wagtaildocs_urls)),
|
||||
|
@ -20,6 +20,23 @@ def test_get_segments(rf):
|
||||
assert segments == [segment_1, segment_2]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_segments_session(rf):
|
||||
request = rf.get('/')
|
||||
|
||||
adapter = adapters.SessionSegmentsAdapter(request)
|
||||
|
||||
segment_1 = SegmentFactory(name='segment-1', persistent=True)
|
||||
segment_2 = SegmentFactory(name='segment-2', persistent=True)
|
||||
|
||||
adapter.set_segments([segment_1, segment_2])
|
||||
assert len(request.session['segments']) == 2
|
||||
|
||||
adapter._segment_cache = None
|
||||
segments = adapter.get_segments()
|
||||
assert segments == [segment_1, segment_2]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_segment_by_id(rf):
|
||||
request = rf.get('/')
|
||||
|
@ -4,16 +4,14 @@ import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.factories.page import ContentPageFactory
|
||||
from tests.factories.rule import (
|
||||
DayRuleFactory, DeviceRuleFactory, ReferralRuleFactory, TimeRuleFactory)
|
||||
from tests.factories.rule import QueryRuleFactory, ReferralRuleFactory
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from tests.factories.site import SiteFactory
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import TimeRule
|
||||
|
||||
# Factory tests
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_segment_create():
|
||||
factoried_segment = SegmentFactory()
|
||||
@ -27,8 +25,6 @@ def test_segment_create():
|
||||
assert factoried_segment.status == segment.status
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_referral_rule_create():
|
||||
segment = SegmentFactory(name='Referral')
|
||||
@ -37,3 +33,15 @@ def test_referral_rule_create():
|
||||
segment=segment)
|
||||
|
||||
assert referral_rule.regex_string == 'test.test'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_query_rule_create():
|
||||
segment = SegmentFactory(name='Query')
|
||||
query_rule = QueryRuleFactory(
|
||||
parameter="query",
|
||||
value="value",
|
||||
segment=segment)
|
||||
|
||||
assert query_rule.parameter == 'query'
|
||||
assert query_rule.value == 'value'
|
||||
|
@ -4,7 +4,9 @@ import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.factories.page import ContentPageFactory
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from tests.site.pages import models
|
||||
from wagtail_personalisation.rules import TimeRule
|
||||
|
||||
|
||||
@ -25,3 +27,10 @@ def test_metadata_page_has_variants(segmented_page):
|
||||
canonical = segmented_page.personalisation_metadata.canonical_page
|
||||
assert canonical.personalisation_metadata.is_canonical
|
||||
assert canonical.personalisation_metadata.has_variants
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_content_page_model():
|
||||
page = ContentPageFactory()
|
||||
qs = models.ContentPage.objects.all()
|
||||
assert page in qs
|
||||
|
@ -16,6 +16,8 @@ def test_time_rule_create():
|
||||
segment=segment)
|
||||
|
||||
assert time_rule.start_time == datetime.time(8, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@freeze_time("10:00:00")
|
||||
def test_requesttime_segment(client, site):
|
||||
|
@ -1,5 +1,9 @@
|
||||
import pytest
|
||||
|
||||
from tests.factories.rule import VisitCountRuleFactory
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from wagtail_personalisation.rules import VisitCountRule
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_visit_count(site, client):
|
||||
@ -20,3 +24,56 @@ def test_visit_count(site, client):
|
||||
visit_count = client.session['visit_count']
|
||||
assert visit_count[0]['count'] == 2
|
||||
assert visit_count[1]['count'] == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_call_test_user_on_invalid_rule_fails(site, user, mocker):
|
||||
rule = VisitCountRule()
|
||||
assert not (rule.test_user(None, user))
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_visit_count_call_test_user_with_user(site, client, user):
|
||||
segment = SegmentFactory(name='VisitCount')
|
||||
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||
|
||||
session = client.session
|
||||
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
|
||||
assert rule.test_user(None, user)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_visit_count_call_test_user_with_user_or_request_fails(site, client, user):
|
||||
segment = SegmentFactory(name='VisitCount')
|
||||
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||
|
||||
session = client.session
|
||||
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
|
||||
assert not rule.test_user(None)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_column_header(site):
|
||||
segment = SegmentFactory(name='VisitCount')
|
||||
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||
|
||||
assert rule.get_column_header() == 'Visit count - Test page'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_user_info_string_returns_count(site, client, user):
|
||||
segment = SegmentFactory(name='VisitCount')
|
||||
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||
|
||||
session = client.session
|
||||
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
|
||||
assert rule.get_user_info_string(user) == '2'
|
||||
|
574
tests/unit/test_static_dynamic_segments.py
Normal file
574
tests/unit/test_static_dynamic_segments.py
Normal file
@ -0,0 +1,574 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from wagtail_personalisation.forms import SegmentAdminForm
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import TimeRule, VisitCountRule
|
||||
|
||||
|
||||
def form_with_data(segment, *rules):
|
||||
model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent']
|
||||
|
||||
class TestSegmentAdminForm(SegmentAdminForm):
|
||||
class Meta:
|
||||
model = Segment
|
||||
fields = model_fields
|
||||
|
||||
data = model_to_dict(segment, model_fields)
|
||||
for formset in TestSegmentAdminForm().formsets.values():
|
||||
rule_data = {}
|
||||
count = 0
|
||||
for rule in rules:
|
||||
if isinstance(rule, formset.model):
|
||||
rule_data = model_to_dict(rule)
|
||||
for key, value in rule_data.items():
|
||||
data['{}-{}-{}'.format(formset.prefix, count, key)] = value
|
||||
count += 1
|
||||
data['{}-INITIAL_FORMS'.format(formset.prefix)] = 0
|
||||
data['{}-TOTAL_FORMS'.format(formset.prefix)] = count
|
||||
return TestSegmentAdminForm(data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_added_to_static_segment_at_creation(site, user, mocker):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
instance = form.save()
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_not_added_to_full_static_segment_at_creation(site, django_user_model, mocker):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user',
|
||||
side_effect=[True, True])
|
||||
instance = form.save()
|
||||
|
||||
assert len(instance.static_users.all()) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_anonymous_user_not_added_to_static_segment_at_creation(site, client, mocker):
|
||||
session = client.session
|
||||
session.save()
|
||||
client.get(site.root_page.url)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
instance = form.save()
|
||||
|
||||
assert not instance.static_users.all()
|
||||
assert mock_test_rule.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_match_any_correct_populates(site, django_user_model, mocker):
|
||||
user = django_user_model.objects.create(username='first')
|
||||
other_user = django_user_model.objects.create(username='second')
|
||||
other_page = site.root_page.get_last_child()
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, match_any=True)
|
||||
rule_1 = VisitCountRule(counted_page=site.root_page)
|
||||
rule_2 = VisitCountRule(counted_page=other_page)
|
||||
form = form_with_data(segment, rule_1, rule_2)
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', side_effect=[True, False, True, False])
|
||||
instance = form.save()
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
assert other_user in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, mocker):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
static_rule = VisitCountRule(counted_page=site.root_page)
|
||||
non_static_rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
)
|
||||
form = form_with_data(segment, static_rule, non_static_rule)
|
||||
|
||||
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
instance = form.save()
|
||||
|
||||
assert not instance.static_users.all()
|
||||
assert mock_test_rule.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_session_not_added_to_static_segment_after_creation(site, client, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_session_added_to_static_segment_after_creation(site, client, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_anonymou_user_not_added_to_static_segment_after_creation(site, client):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_session_not_added_to_static_segment_after_full(site, client, django_user_model):
|
||||
user = django_user_model.objects.create(username='first')
|
||||
other_user = django_user_model.objects.create(username='second')
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
session = client.session
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert instance.static_users.count() == 1
|
||||
|
||||
client.cookies.clear()
|
||||
second_session = client.session
|
||||
client.force_login(other_user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert session.session_key != second_session.session_key
|
||||
assert instance.static_users.count() == 1
|
||||
assert user in instance.static_users.all()
|
||||
assert other_user not in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sessions_not_added_to_static_segment_if_rule_not_static(mocker):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
segment=segment,
|
||||
)
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
instance = form.save()
|
||||
|
||||
assert not instance.static_users.all()
|
||||
assert mock_test_rule.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_does_not_calculate_the_segment_again(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=2)
|
||||
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||
form = form_with_data(segment, rule)
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
instance = form.save()
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
|
||||
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
assert mock_test_rule.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_non_static_rules_have_a_count():
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||
rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
segment=segment,
|
||||
)
|
||||
form = form_with_data(segment, rule)
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_static_segment_with_static_rules_needs_no_count(site):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||
form = form_with_data(segment, rule)
|
||||
assert form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dynamic_segment_with_non_static_rules_have_a_count():
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC, count=0)
|
||||
rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
)
|
||||
form = form_with_data(segment, rule)
|
||||
assert form.is_valid(), form.errors
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_randomisation_percentage_added_to_segment_at_creation(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = 80
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert instance.randomisation_percent == 80
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_randomisation_percentage_min_zero(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = -1
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_randomisation_percentage_max_100(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = 101
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_in_static_segment_if_random_is_below_percentage(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=39)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert instance.id == client.session['segments'][0]['id']
|
||||
assert user in instance.static_users.all()
|
||||
assert user not in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_in_static_segment_if_random_is_above_percentage(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=41)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert len(client.session['segments']) == 0
|
||||
assert user not in instance.static_users.all()
|
||||
assert user in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_offered_dynamic_segment_if_random_is_below_percentage(site, client, mocker):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=39)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert len(client.session['excluded_segments']) == 0
|
||||
assert instance.id == client.session['segments'][0]['id']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_offered_dynamic_segment_if_random_is_above_percentage(site, client, mocker):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=41)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert len(client.session['segments']) == 0
|
||||
assert instance.id == client.session['excluded_segments'][0]['id']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_in_segment_if_percentage_is_0(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=0)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert len(client.session['segments']) == 0
|
||||
assert user not in instance.static_users.all()
|
||||
assert user in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_always_in_segment_if_percentage_is_100(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=100)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert instance.id == client.session['segments'][0]['id']
|
||||
assert user in instance.static_users.all()
|
||||
assert user not in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_added_to_static_segment_at_creation_if_random_above_percent(site, mocker, user):
|
||||
mocker.patch('random.randint', return_value=41)
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
instance = form.save()
|
||||
|
||||
assert user not in instance.static_users.all()
|
||||
assert user in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_added_to_static_segment_at_creation_if_random_below_percent(site, mocker, user):
|
||||
mocker.patch('random.randint', return_value=39)
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
instance = form.save()
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
assert user not in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rules_check_skipped_if_user_in_excluded(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=100)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
instance.excluded_users.add(user)
|
||||
instance.save
|
||||
|
||||
mock_test_rule = mocker.patch(
|
||||
'wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert mock_test_rule.call_count == 0
|
||||
assert len(client.session['segments']) == 0
|
||||
assert user not in instance.static_users.all()
|
||||
assert user in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rules_check_skipped_if_dynamic_segment_in_excluded(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||
randomisation_percent=100)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
instance.persistent = True
|
||||
instance.save()
|
||||
|
||||
session = client.session
|
||||
session['excluded_segments'] = [{'id': instance.pk}]
|
||||
session.save()
|
||||
|
||||
mock_test_rule = mocker.patch(
|
||||
'wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert mock_test_rule.call_count == 0
|
||||
assert len(client.session['segments']) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_matched_user_count_added_to_segment_at_creation(site, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
form.instance.type = Segment.TYPE_STATIC
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
instance = form.save()
|
||||
|
||||
assert mock_test_user.call_count == 2
|
||||
instance.matched_users_count = 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_users_matching_static_rules(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_excludes_staff(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second', is_staff=True)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 1
|
||||
assert mock_test_user.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_excludes_inactive(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second', is_active=False)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 1
|
||||
assert mock_test_user.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_only_counts_static_rules(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
segment=segment,
|
||||
)
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.TimeRule.test_user')
|
||||
|
||||
assert form.count_matching_users([rule], True) is 0
|
||||
assert mock_test_user.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_handles_match_any(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
first_rule = VisitCountRule(counted_page=site.root_page)
|
||||
other_page = site.root_page.get_last_child()
|
||||
second_rule = VisitCountRule(counted_page=other_page)
|
||||
form = form_with_data(segment, first_rule, second_rule)
|
||||
|
||||
mock_test_user = mocker.patch(
|
||||
'wagtail_personalisation.rules.VisitCountRule.test_user',
|
||||
side_effect=[True, False, True, False])
|
||||
|
||||
assert form.count_matching_users([first_rule, second_rule], True) is 2
|
||||
mock_test_user.call_count == 4
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_handles_match_all(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
first_rule = VisitCountRule(counted_page=site.root_page)
|
||||
other_page = site.root_page.get_last_child()
|
||||
second_rule = VisitCountRule(counted_page=other_page)
|
||||
form = form_with_data(segment, first_rule, second_rule)
|
||||
|
||||
mock_test_user = mocker.patch(
|
||||
'wagtail_personalisation.rules.VisitCountRule.test_user',
|
||||
side_effect=[True, True, False, True])
|
||||
|
||||
assert form.count_matching_users([first_rule, second_rule], False) is 1
|
||||
mock_test_user.call_count == 4
|
@ -1,21 +1,26 @@
|
||||
import pytest
|
||||
|
||||
from tests.factories.page import ContentPageFactory
|
||||
from wagtail_personalisation.utils import impersonate_other_page
|
||||
|
||||
|
||||
class Page(object):
|
||||
def __init__(self, path, depth, url_path, title):
|
||||
self.path = path
|
||||
self.depth = depth
|
||||
self.url_path = url_path
|
||||
self.title = title
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__dict__ == other.__dict__
|
||||
@pytest.fixture
|
||||
def rootpage():
|
||||
return ContentPageFactory(parent=None, path='/', depth=0, title='root')
|
||||
|
||||
|
||||
def test_impersonate_other_page():
|
||||
page = Page(path="/", depth=0, url_path="/", title="Hoi")
|
||||
other_page = Page(path="/other", depth=1, url_path="/other", title="Doei")
|
||||
@pytest.fixture
|
||||
def page(rootpage):
|
||||
return ContentPageFactory(parent=rootpage, path='/hi', title='Hi')
|
||||
|
||||
impersonate_other_page(page, other_page)
|
||||
|
||||
assert page == other_page
|
||||
@pytest.fixture
|
||||
def otherpage(rootpage):
|
||||
return ContentPageFactory(parent=rootpage, path='/bye', title='Bye')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_impersonate_other_page(page, otherpage):
|
||||
impersonate_other_page(page, otherpage)
|
||||
assert page.title == otherpage.title == 'Bye'
|
||||
assert page.path == otherpage.path
|
||||
|
53
tests/unit/test_views.py
Normal file
53
tests/unit/test_views.py
Normal file
@ -0,0 +1,53 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import VisitCountRule
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_segment_user_data_view_requires_admin_access(site, client, django_user_model):
|
||||
user = django_user_model.objects.create(username='first')
|
||||
|
||||
segment = Segment(type=Segment.TYPE_STATIC, count=1)
|
||||
segment.save()
|
||||
|
||||
client.force_login(user)
|
||||
url = reverse('segment:segment_user_data', args=(segment.id,))
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == '/admin/login/?next=%s' % url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_segment_user_data_view(site, client, mocker, django_user_model):
|
||||
user1 = django_user_model.objects.create(username='first')
|
||||
user2 = django_user_model.objects.create(username='second')
|
||||
admin_user = django_user_model.objects.create(
|
||||
username='admin', is_superuser=True)
|
||||
|
||||
segment = Segment(type=Segment.TYPE_STATIC, count=1)
|
||||
segment.save()
|
||||
segment.static_users.add(user1)
|
||||
segment.static_users.add(user2)
|
||||
|
||||
rule1 = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||
rule2 = VisitCountRule(counted_page=site.root_page.get_last_child(),
|
||||
segment=segment)
|
||||
rule1.save()
|
||||
rule2.save()
|
||||
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.get_user_info_string',
|
||||
side_effect=[3, 9, 0, 1])
|
||||
|
||||
client.force_login(admin_user)
|
||||
response = client.get(
|
||||
reverse('segment:segment_user_data', args=(segment.id,)))
|
||||
|
||||
assert response.status_code == 200
|
||||
data_lines = response.content.decode().split("\n")
|
||||
|
||||
assert data_lines[0] == 'Username,Visit count - Test page,Visit count - Regular page\r'
|
||||
assert data_lines[1] == 'first,3,9\r'
|
||||
assert data_lines[2] == 'second,0,1\r'
|
20
tox.ini
20
tox.ini
@ -1,26 +1,26 @@
|
||||
[tox]
|
||||
envlist = py{27,35,36}-django{111}-wagtail{110},lint
|
||||
envlist = py{36}-django{20}-wagtail{20,21},lint
|
||||
|
||||
[testenv]
|
||||
basepython = python3.6
|
||||
commands = coverage run --parallel -m pytest {posargs}
|
||||
extras = test
|
||||
deps =
|
||||
django111: django>=1.11,<1.12
|
||||
wagtail110: wagtail>=1.10,<1.11
|
||||
deps =
|
||||
django20: django>=2.0,<2.1
|
||||
wagtail20: wagtail>=2.0,<2.1
|
||||
wagtail21: wagtail>=2.1,<2.2
|
||||
|
||||
[testenv:coverage-report]
|
||||
basepython = python3.5
|
||||
basepython = python3.6
|
||||
deps = coverage
|
||||
pip_pre = true
|
||||
skip_install = true
|
||||
commands =
|
||||
coverage combine
|
||||
coverage report
|
||||
|
||||
coverage report --include="src/**/" --omit="src/**/migrations/*.py"
|
||||
|
||||
[testenv:lint]
|
||||
basepython = python3.5
|
||||
basepython = python3.6
|
||||
deps = flake8
|
||||
commands =
|
||||
commands =
|
||||
flake8 src tests setup.py
|
||||
isort -q --recursive --diff src/ tests/
|
||||
|
Reference in New Issue
Block a user