Compare commits
122 Commits
0.10.4
...
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 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -23,3 +23,5 @@ tests/sandbox/assets
|
|||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
.pytest_cache/
|
||||||
|
19
.travis.yml
19
.travis.yml
@ -4,11 +4,12 @@ language: python
|
|||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- python: 2.7
|
- python: 3.6
|
||||||
env: lint
|
env: lint
|
||||||
|
- python: 3.6
|
||||||
- python: 2.7
|
env: TOXENV=py36-django20-wagtail20
|
||||||
env: TOXENV=py27-django111-wagtail113
|
- python: 3.6
|
||||||
|
env: TOXENV=py36-django20-wagtail21
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install tox codecov
|
- pip install tox codecov
|
||||||
@ -19,13 +20,3 @@ script:
|
|||||||
after_success:
|
after_success:
|
||||||
- tox -e coverage-report
|
- tox -e coverage-report
|
||||||
- codecov
|
- codecov
|
||||||
|
|
||||||
deploy:
|
|
||||||
provider: pypi
|
|
||||||
distributions: sdist bdist_wheel
|
|
||||||
user: praekelt.org
|
|
||||||
password:
|
|
||||||
secure: IxPnc95OFNQsl7kFfLlLc38KfHh/W79VXnhEkdb2x1GZznOsG167QlhpAnyXIJysCQxgMMwLMuPOOdk1WIxOpGNM1/M80hNzPAfxMYWPuwposDdwoIc8SyREPJt16BXWAY+rAH8SHxld9p1YdRbOEPSSglODe4dCmQWsCbKjV3aKv+gZxBvf6pMxUglp2fBIlCwrE77MyMYh9iW0AGzD6atC2xN9NgAm4f+2WFlKCUL/MztLhNWdvWEiibQav6Tts7p8tWrsBVzywDW2IOy3O0ihPgRdISZ7QrxhiJTjlHYPAy4eRGOnYSQXvp6Dy8ONE5a6Uv5g3Xw2UmQo85sSMUs2VxR0G7d+PQgL0A7ENBQ5i7eSAFHYs8EswiGilW2A7mM4qRXwg9URLelYSdkM+aNXvR+25dCsXakiO4NjCz/BPgNzxPeQLlBdxR5vHugeM/XYuhy6CHlZrR/uueaO0X8RyhJcqeDjBy58IrwYS3Mpj7QCfBpQ9PqsqXEWV9BKwKiBXM2+3hkhawFDWa0GW2PDbORKtSLy/ORfGLx5Y9qxQYKEGvFQA3iqkTjrLj+KeUziKtuvEAcvsdBIJVIxeoHwdl+qqxEm8A7YuRBnWVxWc3jE6wBXboeFP92gVe/ueoXmY22riK9Ja0pli3TyNga8by9WM8Qp4D2ZqkVXHwg=
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
all_branches: true
|
|
||||||
|
43
CHANGES
43
CHANGES
@ -1,3 +1,46 @@
|
|||||||
|
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
|
0.10.0
|
||||||
==================
|
==================
|
||||||
- Adds static and dynamic segments
|
- Adds static and dynamic segments
|
||||||
|
9
Makefile
9
Makefile
@ -1,13 +1,13 @@
|
|||||||
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
|
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
|
||||||
|
|
||||||
all: clean requirements dist
|
|
||||||
|
|
||||||
default: develop
|
default: develop
|
||||||
|
|
||||||
|
all: clean requirements dist
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
find src -name '*.pyc' -delete
|
find src -name '*.pyc' -delete
|
||||||
find tests -name '*.pyc' -delete
|
find tests -name '*.pyc' -delete
|
||||||
find . -name '*.egg-info' -delete
|
find . -name '*.egg-info' |xargs rm -rf
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
pip install --upgrade -e .[docs,test]
|
pip install --upgrade -e .[docs,test]
|
||||||
@ -38,7 +38,8 @@ isort:
|
|||||||
isort --recursive src tests
|
isort --recursive src tests
|
||||||
|
|
||||||
dist:
|
dist:
|
||||||
./setup.py sdist bdist_wheel
|
pip install wheel
|
||||||
|
python ./setup.py sdist bdist_wheel
|
||||||
|
|
||||||
sandbox:
|
sandbox:
|
||||||
pip install -r sandbox/requirements.txt
|
pip install -r sandbox/requirements.txt
|
||||||
|
@ -55,10 +55,10 @@ author = 'Lab Digital BV'
|
|||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '0.10.4'
|
version = '0.11.3'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = '0.10.4'
|
release = '0.11.3'
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
# for a list of supported languages.
|
# for a list of supported languages.
|
||||||
|
0
frontend/img/.gitkeep
Normal file
0
frontend/img/.gitkeep
Normal file
@ -1,4 +1,4 @@
|
|||||||
Django>=1.11,<1.12
|
Django>=2.0,<2.1
|
||||||
wagtail>=1.10,<1.11
|
wagtail>=2.0,<2.2
|
||||||
django-debug-toolbar==1.8
|
django-debug-toolbar==1.9.1
|
||||||
-e .[docs,test]
|
-e .[docs,test]
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations
|
from django.db import migrations
|
||||||
import wagtail.wagtailcore.fields
|
import wagtail.core.fields
|
||||||
import wagtail_personalisation
|
import wagtail_personalisation
|
||||||
|
|
||||||
|
|
||||||
@ -17,14 +17,14 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='homepage',
|
model_name='homepage',
|
||||||
name='intro',
|
name='intro',
|
||||||
field=wagtail.wagtailcore.fields.RichTextField(
|
field=wagtail.core.fields.RichTextField(
|
||||||
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
|
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='homepage',
|
model_name='homepage',
|
||||||
name='body',
|
name='body',
|
||||||
field=wagtail.wagtailcore.fields.StreamField((('personalisable_paragraph', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
|
field=wagtail.core.fields.StreamField((('personalisable_paragraph', wagtail.core.blocks.StructBlock((('segment', wagtail.core.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.core.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
|
from wagtail.admin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
|
||||||
from wagtail.wagtailcore import blocks
|
from wagtail.core import blocks
|
||||||
from wagtail.wagtailcore.fields import RichTextField, StreamField
|
from wagtail.core.fields import RichTextField, StreamField
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||||
from wagtail_personalisation.blocks import PersonalisedStructBlock
|
from wagtail_personalisation.blocks import PersonalisedStructBlock
|
||||||
|
@ -3,8 +3,8 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
from wagtail.wagtailsearch.models import Query
|
from wagtail.search.models import Query
|
||||||
|
|
||||||
|
|
||||||
def search(request):
|
def search(request):
|
||||||
|
@ -29,21 +29,30 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
|
||||||
|
|
||||||
|
SITE_ID = 1
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'wagtail.wagtailforms',
|
'django.contrib.admin',
|
||||||
'wagtail.wagtailredirects',
|
'django.contrib.auth',
|
||||||
'wagtail.wagtailembeds',
|
'django.contrib.contenttypes',
|
||||||
'wagtail.wagtailsites',
|
'django.contrib.messages',
|
||||||
'wagtail.wagtailusers',
|
'django.contrib.sessions',
|
||||||
'wagtail.wagtailsnippets',
|
'django.contrib.sites',
|
||||||
'wagtail.wagtaildocs',
|
'django.contrib.staticfiles',
|
||||||
'wagtail.wagtailimages',
|
|
||||||
'wagtail.wagtailsearch',
|
'wagtail.contrib.forms',
|
||||||
'wagtail.wagtailadmin',
|
'wagtail.contrib.redirects',
|
||||||
'wagtail.wagtailcore',
|
'wagtail.embeds',
|
||||||
|
'wagtail.sites',
|
||||||
|
'wagtail.users',
|
||||||
|
'wagtail.snippets',
|
||||||
|
'wagtail.documents',
|
||||||
|
'wagtail.images',
|
||||||
|
'wagtail.search',
|
||||||
|
'wagtail.admin',
|
||||||
|
'wagtail.core',
|
||||||
'wagtail.contrib.modeladmin',
|
'wagtail.contrib.modeladmin',
|
||||||
|
|
||||||
'wagtailfontawesome',
|
'wagtailfontawesome',
|
||||||
@ -51,13 +60,6 @@ INSTALLED_APPS = [
|
|||||||
'taggit',
|
'taggit',
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
|
|
||||||
'django.contrib.admin',
|
|
||||||
'django.contrib.auth',
|
|
||||||
'django.contrib.contenttypes',
|
|
||||||
'django.contrib.sessions',
|
|
||||||
'django.contrib.messages',
|
|
||||||
'django.contrib.staticfiles',
|
|
||||||
|
|
||||||
'wagtail_personalisation',
|
'wagtail_personalisation',
|
||||||
|
|
||||||
'sandbox.apps.home',
|
'sandbox.apps.home',
|
||||||
@ -68,17 +70,17 @@ INSTALLED_APPS = [
|
|||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.http.ConditionalGetMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
|
||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
|
||||||
|
|
||||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
'wagtail.core.middleware.SiteMiddleware',
|
||||||
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
|
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'sandbox.urls'
|
ROOT_URLCONF = 'sandbox.urls'
|
||||||
|
@ -4,14 +4,14 @@ import debug_toolbar
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
from wagtail.wagtailcore import urls as wagtail_urls
|
from wagtail.core import urls as wagtail_urls
|
||||||
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
from wagtail.documents import urls as wagtaildocs_urls
|
||||||
|
|
||||||
from sandbox.apps.search import views as search_views
|
from sandbox.apps.search import views as search_views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', admin.site.urls),
|
||||||
|
|
||||||
url(r'^cms/', include(wagtailadmin_urls)),
|
url(r'^cms/', include(wagtailadmin_urls)),
|
||||||
url(r'^documents/', include(wagtaildocs_urls)),
|
url(r'^documents/', include(wagtaildocs_urls)),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 0.10.4
|
current_version = 0.11.3
|
||||||
commit = true
|
commit = true
|
||||||
tag = true
|
tag = true
|
||||||
tag_name = {new_version}
|
tag_name = {new_version}
|
||||||
@ -21,9 +21,9 @@ exclude =
|
|||||||
[wheel]
|
[wheel]
|
||||||
universal = 1
|
universal = 1
|
||||||
|
|
||||||
[coverage:run]
|
[coverage]
|
||||||
omit =
|
include = src/**/
|
||||||
src/**/migrations/*.py
|
omit = src/**/migrations/*.py
|
||||||
|
|
||||||
[bumpversion:file:setup.py]
|
[bumpversion:file:setup.py]
|
||||||
|
|
||||||
|
37
setup.py
37
setup.py
@ -2,24 +2,25 @@ import re
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'wagtail>=1.10,<1.14',
|
'wagtail>=2.0,<2.2',
|
||||||
'user-agents>=1.0.1',
|
'user-agents>=1.1.0',
|
||||||
'wagtailfontawesome>=1.0.6',
|
'wagtailfontawesome>=1.1.3',
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
'factory_boy==2.8.1',
|
'factory_boy==2.8.1',
|
||||||
'flake8',
|
|
||||||
'flake8-blind-except',
|
'flake8-blind-except',
|
||||||
'flake8-debugger',
|
'flake8-debugger',
|
||||||
'flake8-imports',
|
'flake8-imports',
|
||||||
|
'flake8',
|
||||||
'freezegun==0.3.8',
|
'freezegun==0.3.8',
|
||||||
'pytest-cov==2.4.0',
|
'pytest-cov==2.5.1',
|
||||||
'pytest-django==3.1.2',
|
'pytest-django==3.1.2',
|
||||||
'pytest-sugar==0.7.1',
|
'pytest-pythonpath==0.7.2',
|
||||||
|
'pytest-sugar==0.9.1',
|
||||||
|
'pytest==3.4.2',
|
||||||
|
'wagtail_factories==1.0.0',
|
||||||
'pytest-mock==1.6.3',
|
'pytest-mock==1.6.3',
|
||||||
'pytest==3.1.0',
|
|
||||||
'wagtail_factories==0.3.0',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
docs_require = [
|
docs_require = [
|
||||||
@ -31,12 +32,12 @@ with open('README.rst') as fh:
|
|||||||
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
|
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='wagtail-personalisation-molo',
|
name='wagtail-personalisation',
|
||||||
version='0.10.4',
|
version='0.12.0',
|
||||||
description='A forked version of Wagtail add-on for showing personalized content',
|
description='A Wagtail add-on for showing personalized content',
|
||||||
author='Praekelt.org',
|
author='Lab Digital BV and others',
|
||||||
author_email='dev@praekeltfoundation.org',
|
author_email='opensource@labdigital.nl',
|
||||||
url='https://github.com/praekeltfoundation/wagtail-personalisation/',
|
url='https://labdigital.nl/',
|
||||||
install_requires=install_requires,
|
install_requires=install_requires,
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
extras_require={
|
extras_require={
|
||||||
@ -54,16 +55,10 @@ setup(
|
|||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: BSD License',
|
'License :: OSI Approved :: BSD License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python :: 2',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.4',
|
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Framework :: Django',
|
'Framework :: Django',
|
||||||
'Framework :: Django :: 1.9',
|
'Framework :: Django :: 2',
|
||||||
'Framework :: Django :: 1.10',
|
|
||||||
'Framework :: Django :: 1.11',
|
|
||||||
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from wagtail_personalisation.rules import AbstractBaseRule
|
|||||||
from wagtail_personalisation.utils import create_segment_dictionary
|
from wagtail_personalisation.utils import create_segment_dictionary
|
||||||
|
|
||||||
|
|
||||||
class BaseSegmentsAdapter(object):
|
class BaseSegmentsAdapter:
|
||||||
"""Base segments adapter."""
|
"""Base segments adapter."""
|
||||||
|
|
||||||
def __init__(self, request):
|
def __init__(self, request):
|
||||||
@ -66,34 +66,48 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
self.request.session.setdefault('segments', [])
|
self.request.session.setdefault('segments', [])
|
||||||
self._segment_cache = None
|
self._segment_cache = None
|
||||||
|
|
||||||
def get_segments(self):
|
def _segments(self, ids=None):
|
||||||
"""Return the persistent segments stored in the request session.
|
if not ids:
|
||||||
|
ids = []
|
||||||
:returns: The segments in the request session
|
|
||||||
:rtype: list of wagtail_personalisation.models.Segment or empty list
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self._segment_cache is not None:
|
|
||||||
return self._segment_cache
|
|
||||||
|
|
||||||
raw_segments = self.request.session['segments']
|
|
||||||
segment_ids = [segment['id'] for segment in raw_segments]
|
|
||||||
|
|
||||||
segments = (
|
segments = (
|
||||||
Segment.objects
|
Segment.objects
|
||||||
.enabled()
|
.enabled()
|
||||||
.filter(persistent=True)
|
.filter(persistent=True)
|
||||||
.in_bulk(segment_ids))
|
.filter(pk__in=ids)
|
||||||
|
)
|
||||||
|
return segments
|
||||||
|
|
||||||
retval = [segments[pk] for pk in segment_ids if pk in segments]
|
def get_segments(self, key="segments"):
|
||||||
self._segment_cache = retval
|
"""Return the persistent segments stored in the request session.
|
||||||
return retval
|
|
||||||
|
|
||||||
def set_segments(self, segments):
|
:param key: The key under which the segments are stored
|
||||||
|
:type key: String
|
||||||
|
:returns: The segments in the request session
|
||||||
|
:rtype: list of wagtail_personalisation.models.Segment or empty list
|
||||||
|
|
||||||
|
"""
|
||||||
|
if key == "segments" and self._segment_cache is not None:
|
||||||
|
return self._segment_cache
|
||||||
|
|
||||||
|
if key not in self.request.session:
|
||||||
|
return []
|
||||||
|
raw_segments = self.request.session[key]
|
||||||
|
segment_ids = [segment['id'] for segment in raw_segments]
|
||||||
|
|
||||||
|
segments = self._segments(ids=segment_ids)
|
||||||
|
|
||||||
|
result = list(segments)
|
||||||
|
if key == "segments":
|
||||||
|
self._segment_cache = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_segments(self, segments, key="segments"):
|
||||||
"""Set the currently active segments
|
"""Set the currently active segments
|
||||||
|
|
||||||
:param segments: The segments to set for the current request
|
:param segments: The segments to set for the current request
|
||||||
:type segments: list of wagtail_personalisation.models.Segment
|
:type segments: list of wagtail_personalisation.models.Segment
|
||||||
|
:param key: The key under which to store the segments. Optional
|
||||||
|
:type key: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
cache_segments = []
|
cache_segments = []
|
||||||
@ -108,8 +122,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
serialized_segments.append(serialized)
|
serialized_segments.append(serialized)
|
||||||
segment_ids.add(segment.pk)
|
segment_ids.add(segment.pk)
|
||||||
|
|
||||||
self.request.session['segments'] = serialized_segments
|
self.request.session[key] = serialized_segments
|
||||||
self._segment_cache = cache_segments
|
if key == "segments":
|
||||||
|
self._segment_cache = cache_segments
|
||||||
|
|
||||||
def get_segment_by_id(self, segment_id):
|
def get_segment_by_id(self, segment_id):
|
||||||
"""Find and return a single segment from the request session.
|
"""Find and return a single segment from the request session.
|
||||||
@ -120,9 +135,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
:rtype: wagtail_personalisation.models.Segment or None
|
:rtype: wagtail_personalisation.models.Segment or None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for segment in self.get_segments():
|
segments = self._segments(ids=[segment_id])
|
||||||
if segment.pk == segment_id:
|
if segments.exists():
|
||||||
return segment
|
return segments.get()
|
||||||
|
|
||||||
def add_page_visit(self, page):
|
def add_page_visit(self, page):
|
||||||
"""Mark the page as visited by the user"""
|
"""Mark the page as visited by the user"""
|
||||||
@ -171,12 +186,19 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
rule_models = AbstractBaseRule.get_descendant_models()
|
rule_models = AbstractBaseRule.get_descendant_models()
|
||||||
|
|
||||||
current_segments = self.get_segments()
|
current_segments = self.get_segments()
|
||||||
|
excluded_segments = self.get_segments("excluded_segments")
|
||||||
|
current_segments = list(
|
||||||
|
set(current_segments) - set(excluded_segments)
|
||||||
|
)
|
||||||
|
|
||||||
# Run tests on all remaining enabled segments to verify applicability.
|
# Run tests on all remaining enabled segments to verify applicability.
|
||||||
additional_segments = []
|
additional_segments = []
|
||||||
for segment in enabled_segments:
|
for segment in enabled_segments:
|
||||||
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
|
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
|
||||||
additional_segments.append(segment)
|
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:
|
elif not segment.is_static or not segment.is_full:
|
||||||
segment_rules = []
|
segment_rules = []
|
||||||
for rule_model in rule_models:
|
for rule_model in rule_models:
|
||||||
@ -185,14 +207,19 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
|||||||
result = self._test_rules(segment_rules, self.request,
|
result = self._test_rules(segment_rules, self.request,
|
||||||
match_any=segment.match_any)
|
match_any=segment.match_any)
|
||||||
|
|
||||||
if result and segment.is_static and not segment.is_full:
|
if result and segment.randomise_into_segment():
|
||||||
if self.request.user.is_authenticated():
|
if segment.is_static and not segment.is_full:
|
||||||
segment.static_users.add(self.request.user)
|
if self.request.user.is_authenticated:
|
||||||
|
segment.static_users.add(self.request.user)
|
||||||
if result:
|
|
||||||
additional_segments.append(segment)
|
additional_segments.append(segment)
|
||||||
|
elif result:
|
||||||
|
if segment.is_static and self.request.user.is_authenticated:
|
||||||
|
segment.excluded_users.add(self.request.user)
|
||||||
|
else:
|
||||||
|
excluded_segments += [segment]
|
||||||
|
|
||||||
self.set_segments(current_segments + additional_segments)
|
self.set_segments(current_segments + additional_segments)
|
||||||
|
self.set_segments(excluded_segments, "excluded_segments")
|
||||||
self.update_visit_count()
|
self.update_visit_count()
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,4 +13,6 @@ urlpatterns = [
|
|||||||
views.copy_page_view, name='copy_page'),
|
views.copy_page_view, name='copy_page'),
|
||||||
url(r'^segment/toggle_segment_view/$',
|
url(r'^segment/toggle_segment_view/$',
|
||||||
views.toggle_segment_view, name='toggle_segment_view'),
|
views.toggle_segment_view, name='toggle_segment_view'),
|
||||||
|
url(r'^segment/users/(?P<segment_id>[0-9]+)$',
|
||||||
|
views.segment_user_data, name='segment_user_data'),
|
||||||
]
|
]
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from wagtail.wagtailcore import blocks
|
from wagtail.core import blocks
|
||||||
|
|
||||||
from wagtail_personalisation.adapters import get_segment_adapter
|
from wagtail_personalisation.adapters import get_segment_adapter
|
||||||
from wagtail_personalisation.models import Segment
|
from wagtail_personalisation.models import Segment
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from itertools import takewhile
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.contrib.sessions.models import Session
|
|
||||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
from django.utils.lru_cache import lru_cache
|
from django.utils.lru_cache import lru_cache
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from wagtail.wagtailadmin.forms import WagtailAdminModelForm
|
from wagtail.admin.forms import WagtailAdminModelForm
|
||||||
|
|
||||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
@ -26,6 +25,29 @@ def user_from_data(user_id):
|
|||||||
|
|
||||||
|
|
||||||
class SegmentAdminForm(WagtailAdminModelForm):
|
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):
|
def clean(self):
|
||||||
cleaned_data = super(SegmentAdminForm, self).clean()
|
cleaned_data = super(SegmentAdminForm, self).clean()
|
||||||
Segment = self._meta.model
|
Segment = self._meta.model
|
||||||
@ -63,6 +85,16 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
|||||||
if not self.instance.is_static:
|
if not self.instance.is_static:
|
||||||
self.instance.count = 0
|
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)
|
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
|
||||||
|
|
||||||
if is_new and instance.is_static and instance.all_rules_static:
|
if is_new and instance.is_static and instance.all_rules_static:
|
||||||
@ -73,22 +105,27 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
|||||||
adapter = get_segment_adapter(request)
|
adapter = get_segment_adapter(request)
|
||||||
|
|
||||||
users_to_add = []
|
users_to_add = []
|
||||||
sessions = Session.objects.iterator()
|
users_to_exclude = []
|
||||||
take_session = takewhile(
|
|
||||||
lambda x: instance.count == 0 or len(users_to_add) <= instance.count,
|
|
||||||
sessions
|
|
||||||
)
|
|
||||||
for session in take_session:
|
|
||||||
session_data = session.get_decoded()
|
|
||||||
user = user_from_data(session_data.get('_auth_user_id'))
|
|
||||||
if user.is_authenticated():
|
|
||||||
request.user = user
|
|
||||||
request.session = SessionStore(session_key=session.session_key)
|
|
||||||
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
|
|
||||||
if passes:
|
|
||||||
users_to_add.append(user)
|
|
||||||
|
|
||||||
|
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.static_users.add(*users_to_add)
|
||||||
|
instance.excluded_users.add(*users_to_exclude)
|
||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
@ -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,7 +1,8 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
import random
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
@ -9,9 +10,9 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from modelcluster.models import ClusterableModel
|
from modelcluster.models import ClusterableModel
|
||||||
from wagtail.wagtailadmin.edit_handlers import (
|
from wagtail.admin.edit_handlers import (
|
||||||
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
|
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.rules import AbstractBaseRule
|
from wagtail_personalisation.rules import AbstractBaseRule
|
||||||
from wagtail_personalisation.utils import count_active_days
|
from wagtail_personalisation.utils import count_active_days
|
||||||
@ -81,6 +82,25 @@ class Segment(ClusterableModel):
|
|||||||
static_users = models.ManyToManyField(
|
static_users = models.ManyToManyField(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
)
|
)
|
||||||
|
excluded_users = models.ManyToManyField(
|
||||||
|
settings.AUTH_USER_MODEL,
|
||||||
|
help_text=_("Users that matched the rules but were excluded from the "
|
||||||
|
"segment for some reason e.g. randomisation"),
|
||||||
|
related_name="excluded_segments"
|
||||||
|
)
|
||||||
|
|
||||||
|
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
|
||||||
|
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
|
||||||
|
|
||||||
|
randomisation_percent = models.PositiveSmallIntegerField(
|
||||||
|
null=True, blank=True, default=None,
|
||||||
|
help_text=_(
|
||||||
|
"If this number is set each user matching the rules will "
|
||||||
|
"have this percentage chance of being placed in the segment."
|
||||||
|
), validators=[
|
||||||
|
MaxValueValidator(100),
|
||||||
|
MinValueValidator(0)
|
||||||
|
])
|
||||||
|
|
||||||
objects = SegmentQuerySet.as_manager()
|
objects = SegmentQuerySet.as_manager()
|
||||||
|
|
||||||
@ -97,6 +117,7 @@ class Segment(ClusterableModel):
|
|||||||
FieldPanel('match_any'),
|
FieldPanel('match_any'),
|
||||||
FieldPanel('type', widget=forms.RadioSelect),
|
FieldPanel('type', widget=forms.RadioSelect),
|
||||||
FieldPanel('count', classname='count_field'),
|
FieldPanel('count', classname='count_field'),
|
||||||
|
FieldPanel('randomisation_percent', classname='percent_field'),
|
||||||
], heading="Segment"),
|
], heading="Segment"),
|
||||||
MultiFieldPanel([
|
MultiFieldPanel([
|
||||||
InlinePanel(
|
InlinePanel(
|
||||||
@ -141,15 +162,11 @@ class Segment(ClusterableModel):
|
|||||||
|
|
||||||
def get_used_pages(self):
|
def get_used_pages(self):
|
||||||
"""Return the pages that have variants using this segment."""
|
"""Return the pages that have variants using this segment."""
|
||||||
pages = list(PersonalisablePageMetadata.objects.filter(segment=self))
|
return PersonalisablePageMetadata.objects.filter(segment=self)
|
||||||
|
|
||||||
return pages
|
|
||||||
|
|
||||||
def get_created_variants(self):
|
def get_created_variants(self):
|
||||||
"""Return the variants using this segment."""
|
"""Return the variants using this segment."""
|
||||||
pages = Page.objects.filter(_personalisable_page_metadata__segment=self)
|
return Page.objects.filter(_personalisable_page_metadata__segment=self)
|
||||||
|
|
||||||
return pages
|
|
||||||
|
|
||||||
def get_rules(self):
|
def get_rules(self):
|
||||||
"""Retrieve all rules in the segment."""
|
"""Retrieve all rules in the segment."""
|
||||||
@ -167,6 +184,19 @@ class Segment(ClusterableModel):
|
|||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
def randomise_into_segment(self):
|
||||||
|
""" Returns True if randomisation_percent is not set or it generates
|
||||||
|
a random number less than the randomisation_percent
|
||||||
|
This is so there is some randomisation in which users are added to the
|
||||||
|
segment
|
||||||
|
"""
|
||||||
|
if self.randomisation_percent is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if random.randint(1, 100) <= self.randomisation_percent:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
class PersonalisablePageMetadata(ClusterableModel):
|
class PersonalisablePageMetadata(ClusterableModel):
|
||||||
"""The personalisable page model. Allows creation of variants with linked
|
"""The personalisable page model. Allows creation of variants with linked
|
||||||
@ -180,10 +210,13 @@ class PersonalisablePageMetadata(ClusterableModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
variant = models.OneToOneField(
|
variant = models.OneToOneField(
|
||||||
Page, related_name='_personalisable_page_metadata')
|
Page, related_name='_personalisable_page_metadata',
|
||||||
|
on_delete=models.CASCADE)
|
||||||
|
|
||||||
segment = models.ForeignKey(
|
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
|
@cached_property
|
||||||
def has_variants(self):
|
def has_variants(self):
|
||||||
@ -254,7 +287,7 @@ class PersonalisablePageMetadata(ClusterableModel):
|
|||||||
return Segment.objects.none()
|
return Segment.objects.none()
|
||||||
|
|
||||||
|
|
||||||
class PersonalisablePageMixin(object):
|
class PersonalisablePageMixin:
|
||||||
"""The personalisable page model. Allows creation of variants with linked
|
"""The personalisable page model. Allows creation of variants with linked
|
||||||
segments.
|
segments.
|
||||||
|
|
||||||
|
@ -2,17 +2,24 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from importlib import import_module
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.sessions.models import Session
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.template.defaultfilters import slugify
|
from django.template.defaultfilters import slugify
|
||||||
|
from django.test.client import RequestFactory
|
||||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from modelcluster.fields import ParentalKey
|
from modelcluster.fields import ParentalKey
|
||||||
from user_agents import parse
|
from user_agents import parse
|
||||||
from wagtail.wagtailadmin.edit_handlers import (
|
from wagtail.admin.edit_handlers import (
|
||||||
FieldPanel, FieldRowPanel, PageChooserPanel)
|
FieldPanel, FieldRowPanel, PageChooserPanel)
|
||||||
|
|
||||||
|
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class AbstractBaseRule(models.Model):
|
class AbstractBaseRule(models.Model):
|
||||||
@ -220,13 +227,43 @@ class VisitCountRule(AbstractBaseRule):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Visit count Rule')
|
verbose_name = _('Visit count Rule')
|
||||||
|
|
||||||
def test_user(self, request):
|
def _get_user_session(self, user):
|
||||||
|
sessions = Session.objects.iterator()
|
||||||
|
for session in sessions:
|
||||||
|
session_data = session.get_decoded()
|
||||||
|
if session_data.get('_auth_user_id') == str(user.id):
|
||||||
|
return SessionStore(session_key=session.session_key)
|
||||||
|
return SessionStore()
|
||||||
|
|
||||||
|
def test_user(self, request, user=None):
|
||||||
|
# Local import for cyclic import
|
||||||
|
from wagtail_personalisation.adapters import (
|
||||||
|
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
|
||||||
|
|
||||||
|
# Django formsets don't honour 'required' fields so check rule is valid
|
||||||
|
try:
|
||||||
|
self.counted_page
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if user:
|
||||||
|
# Create a fake request so we can use the adapter
|
||||||
|
request = RequestFactory().get('/')
|
||||||
|
request.user = user
|
||||||
|
|
||||||
|
# If we're using the session adapter check for an active session
|
||||||
|
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
|
||||||
|
request.session = self._get_user_session(user)
|
||||||
|
else:
|
||||||
|
request.session = SessionStore()
|
||||||
|
|
||||||
|
elif not request:
|
||||||
|
# Return false if we don't have a user or a request
|
||||||
|
return False
|
||||||
|
|
||||||
operator = self.operator
|
operator = self.operator
|
||||||
segment_count = self.count
|
segment_count = self.count
|
||||||
|
|
||||||
# Local import for cyclic import
|
|
||||||
from wagtail_personalisation.adapters import get_segment_adapter
|
|
||||||
|
|
||||||
adapter = get_segment_adapter(request)
|
adapter = get_segment_adapter(request)
|
||||||
|
|
||||||
visit_count = adapter.get_visit_count(self.counted_page)
|
visit_count = adapter.get_visit_count(self.counted_page)
|
||||||
@ -252,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):
|
class QueryRule(AbstractBaseRule):
|
||||||
"""Query rule to segment users based on matching queries.
|
"""Query rule to segment users based on matching queries.
|
||||||
@ -261,7 +320,6 @@ class QueryRule(AbstractBaseRule):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
icon = 'fa-link'
|
icon = 'fa-link'
|
||||||
static = True
|
|
||||||
|
|
||||||
parameter = models.SlugField(_("The query parameter to search for"),
|
parameter = models.SlugField(_("The query parameter to search for"),
|
||||||
max_length=20)
|
max_length=20)
|
||||||
|
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
@ -24,6 +24,7 @@
|
|||||||
{% for segment in object_list %}
|
{% for segment in object_list %}
|
||||||
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
|
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
|
||||||
<h2>{{ segment }}</h2>
|
<h2>{{ segment }}</h2>
|
||||||
|
|
||||||
<div class="inspect_container">
|
<div class="inspect_container">
|
||||||
<ul class="inspect segment_stats">
|
<ul class="inspect segment_stats">
|
||||||
<li class="stat_card">
|
<li class="stat_card">
|
||||||
@ -38,11 +39,11 @@
|
|||||||
<li class="stat_card">
|
<li class="stat_card">
|
||||||
{% trans "This segment is Static" %}
|
{% trans "This segment is Static" %}
|
||||||
<span class="icon icon-fa-user">
|
<span class="icon icon-fa-user">
|
||||||
{{ segment.sessions.count|localize }}
|
{{ segment.static_users.count|localize }}
|
||||||
{% if segment.sessions.count < segment.count %}
|
{% if segment.static_users.count < segment.count %}
|
||||||
/ {{ segment.count }} {% trans "member" %}{{ segment.count|pluralize }}
|
/ {{ segment.count }} {% trans "member" %}{{ segment.count|pluralize }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "member" %}{{ segment.sessions.count|pluralize }}
|
{% trans "member" %}{{ segment.count|pluralize }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
@ -70,6 +71,13 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% if segment.randomisation_percent is not None %}
|
||||||
|
<li class="stat_card">
|
||||||
|
<span>{{ segment.randomisation_percent }} %</span>
|
||||||
|
{% trans "Chance that visitors matching the rules are added to the segment" %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% for rule in segment.get_rules %}
|
{% for rule in segment.get_rules %}
|
||||||
<li class="stat_card {{ rule.encoded_name }}">
|
<li class="stat_card {{ rule.encoded_name }}">
|
||||||
{{ rule.description.title }}
|
{{ rule.description.title }}
|
||||||
@ -80,6 +88,11 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if segment.matched_users_count > 0 %}
|
||||||
|
<li class="stat_card">
|
||||||
|
<span class="icon icon-fa-user"> {{ segment.matched_users_count }} {% trans "user" %}{{ segment.matched_users_count|pluralize }}</span> {% trans "were possible matches for this segment at creation" %}
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -91,6 +104,9 @@
|
|||||||
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'wagtail_personalisation_segment_modeladmin_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>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
|
from django.db.models import F
|
||||||
from django.template.base import FilterExpression, kwarg_re
|
from django.template.base import FilterExpression, kwarg_re
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@ -98,22 +99,11 @@ def parse_tag(token, parser):
|
|||||||
def exclude_variants(pages):
|
def exclude_variants(pages):
|
||||||
"""Checks if page is not a variant
|
"""Checks if page is not a variant
|
||||||
|
|
||||||
:param pages: List of pages to check
|
:param pages: Set of pages to check
|
||||||
:type pages: list
|
:type pages: QuerySet
|
||||||
:return: List of pages that aren't variants
|
:return: Queryset of pages that aren't variants
|
||||||
:rtype: list
|
:rtype: QuerySet
|
||||||
"""
|
"""
|
||||||
return [
|
return pages.filter(
|
||||||
page for page in pages
|
personalisable_canonical_metadata__canonical_page_id=F(
|
||||||
if (
|
'personalisable_canonical_metadata__variant__id'))
|
||||||
(
|
|
||||||
hasattr(page, 'personalisation_metadata') is False
|
|
||||||
) or
|
|
||||||
(
|
|
||||||
hasattr(page, 'personalisation_metadata') and page.personalisation_metadata is None
|
|
||||||
) or
|
|
||||||
(
|
|
||||||
hasattr(page, 'personalisation_metadata') and page.personalisation_metadata.is_canonical
|
|
||||||
)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
import csv
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.urlresolvers import reverse
|
from django.http import (
|
||||||
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
HttpResponse, HttpResponseForbidden, HttpResponseRedirect)
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
||||||
from wagtail.contrib.modeladmin.views import IndexView
|
from wagtail.contrib.modeladmin.views import IndexView
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.models import Segment
|
from wagtail_personalisation.models import Segment
|
||||||
|
|
||||||
@ -139,3 +142,32 @@ def copy_page_view(request, page_id, segment_id):
|
|||||||
return HttpResponseRedirect(edit_url)
|
return HttpResponseRedirect(edit_url)
|
||||||
|
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
|
||||||
|
# CSV download views
|
||||||
|
def segment_user_data(request, segment_id):
|
||||||
|
if request.user.has_perm('wagtailadmin.access_admin'):
|
||||||
|
segment = get_object_or_404(Segment, pk=segment_id)
|
||||||
|
|
||||||
|
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||||
|
response['Content-Disposition'] = \
|
||||||
|
'attachment;filename=segment-%s-users.csv' % str(segment_id)
|
||||||
|
|
||||||
|
headers = ['Username']
|
||||||
|
for rule in segment.get_rules():
|
||||||
|
if rule.static:
|
||||||
|
headers.append(rule.get_column_header())
|
||||||
|
|
||||||
|
writer = csv.writer(response)
|
||||||
|
writer.writerow(headers)
|
||||||
|
|
||||||
|
for user in segment.static_users.all():
|
||||||
|
row = [user.username]
|
||||||
|
for rule in segment.get_rules():
|
||||||
|
if rule.static:
|
||||||
|
row.append(rule.get_user_info_string(user))
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
@ -3,14 +3,14 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.template.defaultfilters import pluralize
|
from django.template.defaultfilters import pluralize
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from wagtail.wagtailadmin.site_summary import PagesSummaryItem, SummaryItem
|
from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem
|
||||||
from wagtail.wagtailadmin.widgets import Button, ButtonWithDropdownFromHook
|
from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook
|
||||||
from wagtail.wagtailcore import hooks
|
from wagtail.core import hooks
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation import admin_urls, models, utils
|
from wagtail_personalisation import admin_urls, models, utils
|
||||||
from wagtail_personalisation.adapters import get_segment_adapter
|
from wagtail_personalisation.adapters import get_segment_adapter
|
||||||
@ -23,9 +23,7 @@ def register_admin_urls():
|
|||||||
"""Adds the administration urls for the personalisation apps."""
|
"""Adds the administration urls for the personalisation apps."""
|
||||||
return [
|
return [
|
||||||
url(r'^personalisation/', include(
|
url(r'^personalisation/', include(
|
||||||
admin_urls,
|
admin_urls, namespace='wagtail_personalisation')),
|
||||||
app_name='wagtail_personalisation',
|
|
||||||
namespace='wagtail_personalisation')),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -35,7 +33,7 @@ def set_visit_count(page, request, serve_args, serve_kwargs):
|
|||||||
to a segment.
|
to a segment.
|
||||||
|
|
||||||
:param page: The page being served
|
:param page: The page being served
|
||||||
:type page: wagtail.wagtailcore.models.Page
|
:type page: wagtail.core.models.Page
|
||||||
:param request: The http request
|
:param request: The http request
|
||||||
:type request: django.http.HttpRequest
|
:type request: django.http.HttpRequest
|
||||||
|
|
||||||
@ -49,7 +47,7 @@ def segment_user(page, request, serve_args, serve_kwargs):
|
|||||||
"""Apply a segment to a visitor before serving the page.
|
"""Apply a segment to a visitor before serving the page.
|
||||||
|
|
||||||
:param page: The page being served
|
:param page: The page being served
|
||||||
:type page: wagtail.wagtailcore.models.Page
|
:type page: wagtail.core.models.Page
|
||||||
:param request: The http request
|
:param request: The http request
|
||||||
:type request: django.http.HttpRequest
|
:type request: django.http.HttpRequest
|
||||||
|
|
||||||
@ -63,12 +61,12 @@ def serve_variant(page, request, serve_args, serve_kwargs):
|
|||||||
"""Apply a segment to a visitor before serving the page.
|
"""Apply a segment to a visitor before serving the page.
|
||||||
|
|
||||||
:param page: The page being served
|
:param page: The page being served
|
||||||
:type page: wagtail.wagtailcore.models.Page
|
:type page: wagtail.core.models.Page
|
||||||
:param request: The http request
|
:param request: The http request
|
||||||
:type request: django.http.HttpRequest
|
:type request: django.http.HttpRequest
|
||||||
:returns: A variant if one is available for the visitor's segment,
|
:returns: A variant if one is available for the visitor's segment,
|
||||||
otherwise the original page
|
otherwise the original page
|
||||||
:rtype: wagtail.wagtailcore.models.Page
|
:rtype: wagtail.core.models.Page
|
||||||
|
|
||||||
"""
|
"""
|
||||||
user_segments = []
|
user_segments = []
|
||||||
|
@ -9,7 +9,7 @@ pytest_plugins = [
|
|||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def django_db_setup(django_db_setup, django_db_blocker):
|
def django_db_setup(django_db_setup, django_db_blocker):
|
||||||
from wagtail.wagtailcore.models import Page, Site
|
from wagtail.core.models import Page, Site
|
||||||
|
|
||||||
with django_db_blocker.unblock():
|
with django_db_blocker.unblock():
|
||||||
# Remove some initial data that is brought by the tests.site module
|
# Remove some initial data that is brought by the tests.site module
|
||||||
|
@ -8,6 +8,7 @@ from tests.site.pages import models
|
|||||||
|
|
||||||
|
|
||||||
class ContentPageFactory(PageFactory):
|
class ContentPageFactory(PageFactory):
|
||||||
|
parent = None
|
||||||
title = 'Test page'
|
title = 'Test page'
|
||||||
slug = factory.LazyAttribute(lambda obj: slugify(obj.title))
|
slug = factory.LazyAttribute(lambda obj: slugify(obj.title))
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import factory
|
import factory
|
||||||
from wagtail.wagtailcore.models import Site
|
from wagtail.core.models import Site
|
||||||
|
|
||||||
from tests.factories.page import ContentPageFactory
|
from tests.factories.page import ContentPageFactory
|
||||||
|
|
||||||
|
@ -2,9 +2,6 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import django
|
|
||||||
from pkg_resources import parse_version as V
|
|
||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.sqlite3'),
|
'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.sqlite3'),
|
||||||
@ -55,38 +52,28 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = (
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
|
||||||
def get_middleware_settings():
|
'wagtail.core.middleware.SiteMiddleware',
|
||||||
return (
|
)
|
||||||
'django.middleware.common.CommonMiddleware',
|
|
||||||
'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',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Django 1.10 started to use "MIDDLEWARE" instead of "MIDDLEWARE_CLASSES".
|
|
||||||
if V(django.get_version()) < V('1.10'):
|
|
||||||
MIDDLEWARE_CLASSES = get_middleware_settings()
|
|
||||||
else:
|
|
||||||
MIDDLEWARE = get_middleware_settings()
|
|
||||||
|
|
||||||
INSTALLED_APPS = (
|
INSTALLED_APPS = (
|
||||||
'wagtail_personalisation',
|
'wagtail_personalisation',
|
||||||
|
|
||||||
'wagtail.contrib.modeladmin',
|
'wagtail.contrib.modeladmin',
|
||||||
'wagtail.wagtailsearch',
|
'wagtail.search',
|
||||||
'wagtail.wagtailsites',
|
'wagtail.sites',
|
||||||
'wagtail.wagtailusers',
|
'wagtail.users',
|
||||||
'wagtail.wagtailimages',
|
'wagtail.images',
|
||||||
'wagtail.wagtaildocs',
|
'wagtail.documents',
|
||||||
'wagtail.wagtailadmin',
|
'wagtail.admin',
|
||||||
'wagtail.wagtailcore',
|
'wagtail.core',
|
||||||
|
|
||||||
'taggit',
|
'taggit',
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import wagtail.wagtailcore.fields
|
import wagtail.core.fields
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
import wagtail_personalisation.models
|
import wagtail_personalisation.models
|
||||||
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
||||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Generated by Django 1.11.1 on 2017-06-02 04:26
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import wagtail.wagtailcore.fields
|
import wagtail.core.fields
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@ -20,7 +19,7 @@ class Migration(migrations.Migration):
|
|||||||
fields=[
|
fields=[
|
||||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
||||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'abstract': False,
|
'abstract': False,
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from wagtail.wagtailadmin.edit_handlers import FieldPanel
|
from wagtail.admin.edit_handlers import FieldPanel
|
||||||
from wagtail.wagtailcore.fields import RichTextField
|
from wagtail.core.fields import RichTextField
|
||||||
from wagtail.wagtailcore.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from wagtail_personalisation.models import PersonalisablePageMixin
|
from wagtail_personalisation.models import PersonalisablePageMixin
|
||||||
|
|
||||||
|
@ -2,12 +2,12 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.conf.urls import include, url
|
from django.conf.urls import include, url
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from wagtail.wagtailadmin import urls as wagtailadmin_urls
|
from wagtail.admin import urls as wagtailadmin_urls
|
||||||
from wagtail.wagtailcore import urls as wagtail_urls
|
from wagtail.core import urls as wagtail_urls
|
||||||
from wagtail.wagtaildocs import urls as wagtaildocs_urls
|
from wagtail.documents import urls as wagtaildocs_urls
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^django-admin/', include(admin.site.urls)),
|
url(r'^django-admin/', admin.site.urls),
|
||||||
|
|
||||||
url(r'^admin/', include(wagtailadmin_urls)),
|
url(r'^admin/', include(wagtailadmin_urls)),
|
||||||
url(r'^documents/', include(wagtaildocs_urls)),
|
url(r'^documents/', include(wagtaildocs_urls)),
|
||||||
|
@ -20,6 +20,23 @@ def test_get_segments(rf):
|
|||||||
assert segments == [segment_1, segment_2]
|
assert segments == [segment_1, segment_2]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_segments_session(rf):
|
||||||
|
request = rf.get('/')
|
||||||
|
|
||||||
|
adapter = adapters.SessionSegmentsAdapter(request)
|
||||||
|
|
||||||
|
segment_1 = SegmentFactory(name='segment-1', persistent=True)
|
||||||
|
segment_2 = SegmentFactory(name='segment-2', persistent=True)
|
||||||
|
|
||||||
|
adapter.set_segments([segment_1, segment_2])
|
||||||
|
assert len(request.session['segments']) == 2
|
||||||
|
|
||||||
|
adapter._segment_cache = None
|
||||||
|
segments = adapter.get_segments()
|
||||||
|
assert segments == [segment_1, segment_2]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_get_segment_by_id(rf):
|
def test_get_segment_by_id(rf):
|
||||||
request = rf.get('/')
|
request = rf.get('/')
|
||||||
|
@ -4,7 +4,7 @@ import datetime
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests.factories.rule import ReferralRuleFactory, QueryRuleFactory
|
from tests.factories.rule import QueryRuleFactory, ReferralRuleFactory
|
||||||
from tests.factories.segment import SegmentFactory
|
from tests.factories.segment import SegmentFactory
|
||||||
from wagtail_personalisation.models import Segment
|
from wagtail_personalisation.models import Segment
|
||||||
from wagtail_personalisation.rules import TimeRule
|
from wagtail_personalisation.rules import TimeRule
|
||||||
@ -45,4 +45,3 @@ def test_query_rule_create():
|
|||||||
|
|
||||||
assert query_rule.parameter == 'query'
|
assert query_rule.parameter == 'query'
|
||||||
assert query_rule.value == 'value'
|
assert query_rule.value == 'value'
|
||||||
assert query_rule.static
|
|
||||||
|
@ -4,7 +4,9 @@ import datetime
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from tests.factories.page import ContentPageFactory
|
||||||
from tests.factories.segment import SegmentFactory
|
from tests.factories.segment import SegmentFactory
|
||||||
|
from tests.site.pages import models
|
||||||
from wagtail_personalisation.rules import TimeRule
|
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
|
canonical = segmented_page.personalisation_metadata.canonical_page
|
||||||
assert canonical.personalisation_metadata.is_canonical
|
assert canonical.personalisation_metadata.is_canonical
|
||||||
assert canonical.personalisation_metadata.has_variants
|
assert canonical.personalisation_metadata.has_variants
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_content_page_model():
|
||||||
|
page = ContentPageFactory()
|
||||||
|
qs = models.ContentPage.objects.all()
|
||||||
|
assert page in qs
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from tests.factories.rule import VisitCountRuleFactory
|
||||||
|
from tests.factories.segment import SegmentFactory
|
||||||
|
from wagtail_personalisation.rules import VisitCountRule
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_visit_count(site, client):
|
def test_visit_count(site, client):
|
||||||
@ -20,3 +24,56 @@ def test_visit_count(site, client):
|
|||||||
visit_count = client.session['visit_count']
|
visit_count = client.session['visit_count']
|
||||||
assert visit_count[0]['count'] == 2
|
assert visit_count[0]['count'] == 2
|
||||||
assert visit_count[1]['count'] == 1
|
assert visit_count[1]['count'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_call_test_user_on_invalid_rule_fails(site, user, mocker):
|
||||||
|
rule = VisitCountRule()
|
||||||
|
assert not (rule.test_user(None, user))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_visit_count_call_test_user_with_user(site, client, user):
|
||||||
|
segment = SegmentFactory(name='VisitCount')
|
||||||
|
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
assert rule.test_user(None, user)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_visit_count_call_test_user_with_user_or_request_fails(site, client, user):
|
||||||
|
segment = SegmentFactory(name='VisitCount')
|
||||||
|
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
assert not rule.test_user(None)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_column_header(site):
|
||||||
|
segment = SegmentFactory(name='VisitCount')
|
||||||
|
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||||
|
|
||||||
|
assert rule.get_column_header() == 'Visit count - Test page'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_get_user_info_string_returns_count(site, client, user):
|
||||||
|
segment = SegmentFactory(name='VisitCount')
|
||||||
|
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||||
|
|
||||||
|
session = client.session
|
||||||
|
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||||
|
session.save()
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
assert rule.get_user_info_string(user) == '2'
|
||||||
|
@ -12,7 +12,7 @@ from wagtail_personalisation.rules import TimeRule, VisitCountRule
|
|||||||
|
|
||||||
|
|
||||||
def form_with_data(segment, *rules):
|
def form_with_data(segment, *rules):
|
||||||
model_fields = ['type', 'status', 'count', 'name', 'match_any']
|
model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent']
|
||||||
|
|
||||||
class TestSegmentAdminForm(SegmentAdminForm):
|
class TestSegmentAdminForm(SegmentAdminForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -35,22 +35,32 @@ def form_with_data(segment, *rules):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_session_added_to_static_segment_at_creation(site, client, user):
|
def test_user_added_to_static_segment_at_creation(site, user, mocker):
|
||||||
session = client.session
|
|
||||||
session.save()
|
|
||||||
client.force_login(user)
|
|
||||||
client.get(site.root_page.url)
|
|
||||||
|
|
||||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
rule = VisitCountRule(counted_page=site.root_page)
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
form = form_with_data(segment, rule)
|
form = form_with_data(segment, rule)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
instance = form.save()
|
instance = form.save()
|
||||||
|
|
||||||
assert user in instance.static_users.all()
|
assert user in instance.static_users.all()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_anonymous_user_not_added_to_static_segment_at_creation(site, client):
|
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 = client.session
|
||||||
session.save()
|
session.save()
|
||||||
client.get(site.root_page.url)
|
client.get(site.root_page.url)
|
||||||
@ -58,43 +68,32 @@ def test_anonymous_user_not_added_to_static_segment_at_creation(site, client):
|
|||||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||||
rule = VisitCountRule(counted_page=site.root_page)
|
rule = VisitCountRule(counted_page=site.root_page)
|
||||||
form = form_with_data(segment, rule)
|
form = form_with_data(segment, rule)
|
||||||
|
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
instance = form.save()
|
instance = form.save()
|
||||||
|
|
||||||
assert not instance.static_users.all()
|
assert not instance.static_users.all()
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_match_any_correct_populates(site, client, django_user_model):
|
def test_match_any_correct_populates(site, django_user_model, mocker):
|
||||||
user = django_user_model.objects.create(username='first')
|
user = django_user_model.objects.create(username='first')
|
||||||
session = client.session
|
|
||||||
client.force_login(user)
|
|
||||||
client.get(site.root_page.url)
|
|
||||||
|
|
||||||
other_user = django_user_model.objects.create(username='second')
|
other_user = django_user_model.objects.create(username='second')
|
||||||
client.cookies.clear()
|
|
||||||
second_session = client.session
|
|
||||||
other_page = site.root_page.get_last_child()
|
other_page = site.root_page.get_last_child()
|
||||||
client.force_login(other_user)
|
|
||||||
client.get(other_page.url)
|
|
||||||
|
|
||||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, match_any=True)
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, match_any=True)
|
||||||
rule_1 = VisitCountRule(counted_page=site.root_page)
|
rule_1 = VisitCountRule(counted_page=site.root_page)
|
||||||
rule_2 = VisitCountRule(counted_page=other_page)
|
rule_2 = VisitCountRule(counted_page=other_page)
|
||||||
form = form_with_data(segment, rule_1, rule_2)
|
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()
|
instance = form.save()
|
||||||
|
|
||||||
assert session.session_key != second_session.session_key
|
|
||||||
assert user in instance.static_users.all()
|
assert user in instance.static_users.all()
|
||||||
assert other_user in instance.static_users.all()
|
assert other_user in instance.static_users.all()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, client, user):
|
def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, mocker):
|
||||||
session = client.session
|
|
||||||
session.save()
|
|
||||||
client.force_login(user)
|
|
||||||
client.get(site.root_page.url)
|
|
||||||
|
|
||||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||||
static_rule = VisitCountRule(counted_page=site.root_page)
|
static_rule = VisitCountRule(counted_page=site.root_page)
|
||||||
non_static_rule = TimeRule(
|
non_static_rule = TimeRule(
|
||||||
@ -102,9 +101,12 @@ def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, client,
|
|||||||
end_time=datetime.time(23, 59, 59),
|
end_time=datetime.time(23, 59, 59),
|
||||||
)
|
)
|
||||||
form = form_with_data(segment, static_rule, non_static_rule)
|
form = form_with_data(segment, static_rule, non_static_rule)
|
||||||
|
|
||||||
|
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
instance = form.save()
|
instance = form.save()
|
||||||
|
|
||||||
assert not instance.static_users.all()
|
assert not instance.static_users.all()
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@ -180,12 +182,7 @@ def test_session_not_added_to_static_segment_after_full(site, client, django_use
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_sessions_not_added_to_static_segment_if_rule_not_static(client, site, user):
|
def test_sessions_not_added_to_static_segment_if_rule_not_static(mocker):
|
||||||
session = client.session
|
|
||||||
session.save()
|
|
||||||
client.force_login(user)
|
|
||||||
client.get(site.root_page.url)
|
|
||||||
|
|
||||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||||
rule = TimeRule(
|
rule = TimeRule(
|
||||||
start_time=datetime.time(0, 0, 0),
|
start_time=datetime.time(0, 0, 0),
|
||||||
@ -193,26 +190,27 @@ def test_sessions_not_added_to_static_segment_if_rule_not_static(client, site, u
|
|||||||
segment=segment,
|
segment=segment,
|
||||||
)
|
)
|
||||||
form = form_with_data(segment, rule)
|
form = form_with_data(segment, rule)
|
||||||
|
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||||
instance = form.save()
|
instance = form.save()
|
||||||
|
|
||||||
assert not instance.static_users.all()
|
assert not instance.static_users.all()
|
||||||
|
assert mock_test_rule.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_does_not_calculate_the_segment_again(site, client, mocker, user):
|
def test_does_not_calculate_the_segment_again(site, client, mocker, user):
|
||||||
session = client.session
|
|
||||||
session.save()
|
|
||||||
client.force_login(user)
|
|
||||||
client.get(site.root_page.url)
|
|
||||||
|
|
||||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=2)
|
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=2)
|
||||||
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
|
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||||
form = form_with_data(segment, rule)
|
form = form_with_data(segment, rule)
|
||||||
|
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||||
instance = form.save()
|
instance = form.save()
|
||||||
|
|
||||||
assert user in instance.static_users.all()
|
assert user in instance.static_users.all()
|
||||||
|
|
||||||
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
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)
|
client.get(site.root_page.url)
|
||||||
assert mock_test_rule.call_count == 0
|
assert mock_test_rule.call_count == 0
|
||||||
|
|
||||||
@ -246,3 +244,331 @@ def test_dynamic_segment_with_non_static_rules_have_a_count():
|
|||||||
)
|
)
|
||||||
form = form_with_data(segment, rule)
|
form = form_with_data(segment, rule)
|
||||||
assert form.is_valid(), form.errors
|
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,59 +1,26 @@
|
|||||||
from wagtail_personalisation.utils import (
|
import pytest
|
||||||
exclude_variants, impersonate_other_page)
|
|
||||||
|
from tests.factories.page import ContentPageFactory
|
||||||
|
from wagtail_personalisation.utils import impersonate_other_page
|
||||||
|
|
||||||
|
|
||||||
class Page(object):
|
@pytest.fixture
|
||||||
def __init__(self, path, depth, url_path, title):
|
def rootpage():
|
||||||
self.path = path
|
return ContentPageFactory(parent=None, path='/', depth=0, title='root')
|
||||||
self.depth = depth
|
|
||||||
self.url_path = url_path
|
|
||||||
self.title = title
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.__dict__ == other.__dict__
|
|
||||||
|
|
||||||
|
|
||||||
def test_impersonate_other_page():
|
@pytest.fixture
|
||||||
page = Page(path="/", depth=0, url_path="/", title="Hoi")
|
def page(rootpage):
|
||||||
other_page = Page(path="/other", depth=1, url_path="/other", title="Doei")
|
return ContentPageFactory(parent=rootpage, path='/hi', title='Hi')
|
||||||
|
|
||||||
impersonate_other_page(page, other_page)
|
|
||||||
|
|
||||||
assert page == other_page
|
|
||||||
|
|
||||||
|
|
||||||
class Metadata(object):
|
@pytest.fixture
|
||||||
def __init__(self, is_canonical=True):
|
def otherpage(rootpage):
|
||||||
self.is_canonical = is_canonical
|
return ContentPageFactory(parent=rootpage, path='/bye', title='Bye')
|
||||||
|
|
||||||
|
|
||||||
class PersonalisationMetadataPage(object):
|
@pytest.mark.django_db
|
||||||
def __init__(self):
|
def test_impersonate_other_page(page, otherpage):
|
||||||
self.personalisation_metadata = Metadata()
|
impersonate_other_page(page, otherpage)
|
||||||
|
assert page.title == otherpage.title == 'Bye'
|
||||||
|
assert page.path == otherpage.path
|
||||||
def test_exclude_variants_includes_pages_with_no_metadata_property():
|
|
||||||
page = PersonalisationMetadataPage()
|
|
||||||
del page.personalisation_metadata
|
|
||||||
result = exclude_variants([page])
|
|
||||||
assert result == [page]
|
|
||||||
|
|
||||||
|
|
||||||
def test_exclude_variants_includes_pages_with_metadata_none():
|
|
||||||
page = PersonalisationMetadataPage()
|
|
||||||
page.personalisation_metadata = None
|
|
||||||
result = exclude_variants([page])
|
|
||||||
assert result == [page]
|
|
||||||
|
|
||||||
|
|
||||||
def test_exclude_variants_includes_pages_with_metadata_canonical():
|
|
||||||
page = PersonalisationMetadataPage()
|
|
||||||
result = exclude_variants([page])
|
|
||||||
assert result == [page]
|
|
||||||
|
|
||||||
|
|
||||||
def test_exclude_variants_excludes_pages_with_metadata_not_canonical():
|
|
||||||
page = PersonalisationMetadataPage()
|
|
||||||
page.personalisation_metadata.is_canonical = False
|
|
||||||
result = exclude_variants([page])
|
|
||||||
assert result == []
|
|
||||||
|
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'
|
18
tox.ini
18
tox.ini
@ -1,26 +1,26 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py{27}-django{111}-wagtail{113},lint
|
envlist = py{36}-django{20}-wagtail{20,21},lint
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
basepython = python3.6
|
||||||
commands = coverage run --parallel -m pytest {posargs}
|
commands = coverage run --parallel -m pytest {posargs}
|
||||||
extras = test
|
extras = test
|
||||||
deps =
|
deps =
|
||||||
django111: django>=1.11,<1.12
|
django20: django>=2.0,<2.1
|
||||||
wagtail19: wagtail>=1.13,<1.14
|
wagtail20: wagtail>=2.0,<2.1
|
||||||
|
wagtail21: wagtail>=2.1,<2.2
|
||||||
|
|
||||||
[testenv:coverage-report]
|
[testenv:coverage-report]
|
||||||
basepython = python2.7
|
basepython = python3.6
|
||||||
deps = coverage
|
deps = coverage
|
||||||
pip_pre = true
|
pip_pre = true
|
||||||
skip_install = true
|
skip_install = true
|
||||||
commands =
|
commands =
|
||||||
coverage combine
|
coverage report --include="src/**/" --omit="src/**/migrations/*.py"
|
||||||
coverage report
|
|
||||||
|
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
basepython = python2.7
|
basepython = python3.6
|
||||||
deps = flake8==3.5.0
|
deps = flake8
|
||||||
commands =
|
commands =
|
||||||
flake8 src tests setup.py
|
flake8 src tests setup.py
|
||||||
isort -q --recursive --diff src/ tests/
|
isort -q --recursive --diff src/ tests/
|
||||||
|
Reference in New Issue
Block a user