7

Compare commits

...

50 Commits

Author SHA1 Message Date
0bdb80f25a improve SessionSegmentAdapter 2018-05-26 16:04:11 +02:00
2a48eb3498 fix django version in tox.ini 2018-05-26 14:59:25 +02:00
4ad097b4fa include wagtail-2.1 in test matrix 2018-05-26 14:57:03 +02:00
12f110d913 remove customer manager again for now 2018-05-26 14:35:53 +02:00
c8fe62d2b1 remove praekholt deployment target from travis setup 2018-05-26 14:35:53 +02:00
84ac76f33e Adjust tox.ini for wagtail 2.1 2018-05-26 12:56:02 +02:00
f6598ca1f7 Adjust requirements 2018-05-26 12:55:29 +02:00
726c0cd70f update travis setup 2018-05-26 12:32:39 +02:00
4f3f9a4d40 lint 2018-05-26 12:28:01 +02:00
3a378830e0 fix basepython 2018-05-26 12:27:52 +02:00
8a151e3bab python2 cleanups 2018-05-26 12:06:35 +02:00
bb34bddaf4 add custom model manager 2018-05-26 12:01:26 +02:00
9710d3b479 post-merge cleanups 2018-05-26 11:45:28 +02:00
5536adc3ec Merge branch 'develop' into feature/djangoconf-sprint 2018-05-26 10:48:33 +02:00
5b8d578493 only test for wagtail2 and django2 on python3 2018-05-26 09:54:56 +02:00
bdba6b65cf use new wagtail_factories package 2018-03-22 14:41:54 +01:00
cbcd80d248 update tox.ini 2018-03-17 11:56:14 +01:00
9b1c5a6ab6 fixes test runs
added dependency link to Makefile until Michael releases new
wagtail-factories
2018-03-17 11:37:11 +01:00
62d258fd9e fixes wagtail2 compatibility
return QuerySets instead of lists
2018-03-17 11:26:56 +01:00
32e73329c3 Revert wagtail-factories setting 2018-03-16 11:51:25 +01:00
fde53ea0ef Fix all tests for django and wagtail 2 2018-03-16 11:45:07 +01:00
22a7367211 Update module paths for tests 2018-03-16 11:16:47 +01:00
0d89d47735 Prevent webpack copy error from img dir 2018-03-16 11:14:19 +01:00
92189a3be8 Fix dashboard edit links 2018-03-16 11:14:19 +01:00
6c9d8b2730 remove typo 2018-03-16 11:14:19 +01:00
e141e5396e make Makefile more portable 2018-03-16 11:14:19 +01:00
c0e2b969e8 Set site ID in sandbox settings 2018-03-16 11:14:19 +01:00
7b5e3d4c9d Fix exampledata 2018-03-16 11:14:19 +01:00
6b7a1ed591 Updated requirements and module paths 2018-03-16 11:14:19 +01:00
9b25cd2a94 Add missing dependency `pytest-pythonpath` 2018-03-16 11:10:45 +01:00
3a86c189dc Merge tag '0.11.3' into develop
Bugfix: Handle errors when testing an invalid visit count rule
2018-03-09 20:35:33 +02:00
82c26f9772 Merge branch 'release/0.11.3' 2018-03-09 20:35:20 +02:00
03eb812e45 Version 0.11.3 2018-03-09 20:35:08 +02:00
e3522d0acb Merge pull request #26 from praekeltfoundation/feature/catch-exceptions-when-visit-count-rule-is-blank
Handle exceptions for empty VisitCountRule
2018-03-09 20:32:05 +02:00
7f5e958ee3 Catch the exception if the visit count rule doesn't have a page 2018-03-09 19:20:30 +02:00
241bfb5240 Merge tag '0.11.2' into develop
Bugfix: Stop populating static segments when the count is reached
2018-03-08 14:00:58 +02:00
d5df6e0e58 Merge branch 'release/0.11.2' 2018-03-08 14:00:38 +02:00
865efd0792 Version 0.11.2 2018-03-08 13:59:23 +02:00
454c936e0f Merge pull request #25 from praekeltfoundation/feature/fix-static-segment-population
Fix off-by-one error in static segment population
2018-03-08 13:24:48 +02:00
74d3123084 Ensure static segments don't have one extra user 2018-03-08 13:14:29 +02:00
9bfd816430 Merge tag '0.11.1' into develop
Populate entirely static segments from registered Users not active Sessions
2018-03-01 16:26:46 +02:00
02e06bd9f3 Merge branch 'release/0.11.1' 2018-03-01 16:26:35 +02:00
c7ad3251cf Version 0.11.1 2018-03-01 16:26:20 +02:00
cb8b7da496 Merge pull request #23 from praekeltfoundation/feature/populate-static-segments-from-db-at-creation
Populate static segments from database
2018-03-01 16:23:10 +02:00
0efd3ae937 Update tests for new static segment population 2018-02-26 14:34:02 +02:00
d335e4fd7b Populate static segments even if the count is 0 2018-02-26 14:31:56 +02:00
db2f82967e Only loop through users once when saving static segments
When saving a new static segment we count the matching users and
populate the segment from the database. Each of these requires us
to loop through all of the users, so it's better to do them at the
same time.
2018-02-25 15:43:12 +02:00
37243365a7 Merge branch 'develop' into feature/populate-static-segments-from-db-at-creation 2018-02-25 15:08:36 +02:00
43a2b590b4 Merge tag '0.11.0' into develop
Bug Fix: Query rule should not be static
Enable retrieval of user data for static rules through csv download
2018-02-23 17:02:53 +02:00
8f789b3e17 Fill static segments with users from the database at creation 2018-02-15 13:20:48 +02:00
41 changed files with 306 additions and 336 deletions

2
.gitignore vendored
View File

@ -23,3 +23,5 @@ tests/sandbox/assets
node_modules
.DS_Store
.pytest_cache/

View File

@ -4,11 +4,12 @@ language: python
matrix:
include:
- python: 2.7
- python: 3.6
env: lint
- python: 2.7
env: TOXENV=py27-django111-wagtail113
- python: 3.6
env: TOXENV=py36-django20-wagtail20
- python: 3.6
env: TOXENV=py36-django20-wagtail21
install:
- pip install tox codecov
@ -19,13 +20,3 @@ script:
after_success:
- tox -e coverage-report
- codecov
deploy:
provider: pypi
distributions: sdist bdist_wheel
user: praekelt.org
password:
secure: IxPnc95OFNQsl7kFfLlLc38KfHh/W79VXnhEkdb2x1GZznOsG167QlhpAnyXIJysCQxgMMwLMuPOOdk1WIxOpGNM1/M80hNzPAfxMYWPuwposDdwoIc8SyREPJt16BXWAY+rAH8SHxld9p1YdRbOEPSSglODe4dCmQWsCbKjV3aKv+gZxBvf6pMxUglp2fBIlCwrE77MyMYh9iW0AGzD6atC2xN9NgAm4f+2WFlKCUL/MztLhNWdvWEiibQav6Tts7p8tWrsBVzywDW2IOy3O0ihPgRdISZ7QrxhiJTjlHYPAy4eRGOnYSQXvp6Dy8ONE5a6Uv5g3Xw2UmQo85sSMUs2VxR0G7d+PQgL0A7ENBQ5i7eSAFHYs8EswiGilW2A7mM4qRXwg9URLelYSdkM+aNXvR+25dCsXakiO4NjCz/BPgNzxPeQLlBdxR5vHugeM/XYuhy6CHlZrR/uueaO0X8RyhJcqeDjBy58IrwYS3Mpj7QCfBpQ9PqsqXEWV9BKwKiBXM2+3hkhawFDWa0GW2PDbORKtSLy/ORfGLx5Y9qxQYKEGvFQA3iqkTjrLj+KeUziKtuvEAcvsdBIJVIxeoHwdl+qqxEm8A7YuRBnWVxWc3jE6wBXboeFP92gVe/ueoXmY22riK9Ja0pli3TyNga8by9WM8Qp4D2ZqkVXHwg=
on:
tags: true
condition: $TOXENV = py27-django111-wagtail113

12
CHANGES
View File

@ -1,3 +1,15 @@
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

View File

@ -1,13 +1,13 @@
.PHONY: all clean requirements develop test lint flake8 isort dist sandbox docs
all: clean requirements dist
default: develop
all: clean requirements dist
clean:
find src -name '*.pyc' -delete
find tests -name '*.pyc' -delete
find . -name '*.egg-info' -delete
find . -name '*.egg-info' |xargs rm -rf
requirements:
pip install --upgrade -e .[docs,test]
@ -38,7 +38,8 @@ isort:
isort --recursive src tests
dist:
./setup.py sdist bdist_wheel
pip install wheel
python ./setup.py sdist bdist_wheel
sandbox:
pip install -r sandbox/requirements.txt

View File

@ -55,10 +55,10 @@ author = 'Lab Digital BV'
# built documents.
#
# The short X.Y version.
version = '0.11.0'
version = '0.11.3'
# The full version, including alpha/beta/rc tags.
release = '0.11.0'
release = '0.11.3'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

0
frontend/img/.gitkeep Normal file
View File

View File

@ -1,4 +1,4 @@
Django>=1.11,<1.12
wagtail>=1.10,<1.11
django-debug-toolbar==1.8
Django>=2.0,<2.1
wagtail>=2.0,<2.2
django-debug-toolbar==1.9.1
-e .[docs,test]

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals
from django.db import migrations
import wagtail.wagtailcore.fields
import wagtail.core.fields
import wagtail_personalisation
@ -17,14 +17,14 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='homepage',
name='intro',
field=wagtail.wagtailcore.fields.RichTextField(
field=wagtail.core.fields.RichTextField(
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
preserve_default=False,
),
migrations.AddField(
model_name='homepage',
name='body',
field=wagtail.wagtailcore.fields.StreamField((('personalisable_paragraph', wagtail.wagtailcore.blocks.StructBlock((('segment', wagtail.wagtailcore.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
field=wagtail.core.fields.StreamField((('personalisable_paragraph', wagtail.core.blocks.StructBlock((('segment', wagtail.core.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.core.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
preserve_default=False,
),
]

View File

@ -1,9 +1,9 @@
from __future__ import absolute_import, unicode_literals
from wagtail.wagtailadmin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.fields import RichTextField, StreamField
from wagtail.wagtailcore.models import Page
from wagtail.admin.edit_handlers import RichTextFieldPanel, StreamFieldPanel
from wagtail.core import blocks
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Page
from wagtail_personalisation.models import PersonalisablePageMixin
from wagtail_personalisation.blocks import PersonalisedStructBlock

View File

@ -3,8 +3,8 @@ from __future__ import absolute_import, unicode_literals
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import render
from wagtail.wagtailcore.models import Page
from wagtail.wagtailsearch.models import Query
from wagtail.core.models import Page
from wagtail.search.models import Query
def search(request):

View File

@ -29,21 +29,30 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
SITE_ID = 1
# Application definition
INSTALLED_APPS = [
'wagtail.wagtailforms',
'wagtail.wagtailredirects',
'wagtail.wagtailembeds',
'wagtail.wagtailsites',
'wagtail.wagtailusers',
'wagtail.wagtailsnippets',
'wagtail.wagtaildocs',
'wagtail.wagtailimages',
'wagtail.wagtailsearch',
'wagtail.wagtailadmin',
'wagtail.wagtailcore',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.staticfiles',
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail.core',
'wagtail.contrib.modeladmin',
'wagtailfontawesome',
@ -51,13 +60,6 @@ INSTALLED_APPS = [
'taggit',
'debug_toolbar',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'wagtail_personalisation',
'sandbox.apps.home',
@ -68,17 +70,17 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'wagtail.wagtailcore.middleware.SiteMiddleware',
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
'wagtail.core.middleware.SiteMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
]
ROOT_URLCONF = 'sandbox.urls'

View File

@ -4,14 +4,14 @@ import debug_toolbar
from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
from sandbox.apps.search import views as search_views
urlpatterns = [
url(r'^admin/', include(admin.site.urls)),
url(r'^admin/', admin.site.urls),
url(r'^cms/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.11.0
current_version = 0.11.3
commit = true
tag = true
tag_name = {new_version}
@ -21,9 +21,9 @@ exclude =
[wheel]
universal = 1
[coverage:run]
omit =
src/**/migrations/*.py
[coverage]
include = src/**/
omit = src/**/migrations/*.py
[bumpversion:file:setup.py]

View File

@ -2,24 +2,25 @@ import re
from setuptools import find_packages, setup
install_requires = [
'wagtail>=1.10,<1.14',
'user-agents>=1.0.1',
'wagtailfontawesome>=1.0.6',
'wagtail>=2.0,<2.2',
'user-agents>=1.1.0',
'wagtailfontawesome>=1.1.3',
]
tests_require = [
'factory_boy==2.8.1',
'flake8',
'flake8-blind-except',
'flake8-debugger',
'flake8-imports',
'flake8',
'freezegun==0.3.8',
'pytest-cov==2.4.0',
'pytest-cov==2.5.1',
'pytest-django==3.1.2',
'pytest-sugar==0.7.1',
'pytest-pythonpath==0.7.2',
'pytest-sugar==0.9.1',
'pytest==3.4.2',
'wagtail_factories==1.0.0',
'pytest-mock==1.6.3',
'pytest==3.1.0',
'wagtail_factories==0.3.0',
]
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)
setup(
name='wagtail-personalisation-molo',
version='0.11.0',
description='A forked version of Wagtail add-on for showing personalized content',
author='Praekelt.org',
author_email='dev@praekeltfoundation.org',
url='https://github.com/praekeltfoundation/wagtail-personalisation/',
name='wagtail-personalisation',
version='0.12.0',
description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV and others',
author_email='opensource@labdigital.nl',
url='https://labdigital.nl/',
install_requires=install_requires,
tests_require=tests_require,
extras_require={
@ -54,16 +55,10 @@ setup(
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2',
'Topic :: Internet :: WWW/HTTP :: Site Management',
],
)

View File

@ -9,7 +9,7 @@ from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import create_segment_dictionary
class BaseSegmentsAdapter(object):
class BaseSegmentsAdapter:
"""Base segments adapter."""
def __init__(self, request):
@ -66,6 +66,17 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
self.request.session.setdefault('segments', [])
self._segment_cache = None
def _segments(self, ids=None):
if not ids:
ids = []
segments = (
Segment.objects
.enabled()
.filter(persistent=True)
.filter(pk__in=ids)
)
return segments
def get_segments(self, key="segments"):
"""Return the persistent segments stored in the request session.
@ -83,16 +94,12 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
raw_segments = self.request.session[key]
segment_ids = [segment['id'] for segment in raw_segments]
segments = (
Segment.objects
.enabled()
.filter(persistent=True)
.in_bulk(segment_ids))
segments = self._segments(ids=segment_ids)
retval = [segments[pk] for pk in segment_ids if pk in segments]
result = list(segments)
if key == "segments":
self._segment_cache = retval
return retval
self._segment_cache = result
return result
def set_segments(self, segments, key="segments"):
"""Set the currently active segments
@ -128,9 +135,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
:rtype: wagtail_personalisation.models.Segment or None
"""
for segment in self.get_segments():
if segment.pk == segment_id:
return segment
segments = self._segments(ids=[segment_id])
if segments.exists():
return segments.get()
def add_page_visit(self, page):
"""Mark the page as visited by the user"""
@ -180,6 +187,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
current_segments = self.get_segments()
excluded_segments = self.get_segments("excluded_segments")
current_segments = list(
set(current_segments) - set(excluded_segments)
)
# Run tests on all remaining enabled segments to verify applicability.
additional_segments = []
@ -199,11 +209,11 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
if result and segment.randomise_into_segment():
if segment.is_static and not segment.is_full:
if self.request.user.is_authenticated():
if self.request.user.is_authenticated:
segment.static_users.add(self.request.user)
additional_segments.append(segment)
elif result:
if segment.is_static and self.request.user.is_authenticated():
if segment.is_static and self.request.user.is_authenticated:
segment.excluded_users.add(self.request.user)
else:
excluded_segments += [segment]

View File

@ -1,7 +1,7 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailcore import blocks
from wagtail.core import blocks
from wagtail_personalisation.adapters import get_segment_adapter
from wagtail_personalisation.models import Segment

View File

@ -2,17 +2,15 @@ from __future__ import absolute_import, unicode_literals
from datetime import datetime
from importlib import import_module
from itertools import takewhile
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.sessions.models import Session
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.test.client import RequestFactory
from django.utils.lru_cache import lru_cache
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin.forms import WagtailAdminModelForm
from wagtail.admin.forms import WagtailAdminModelForm
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@ -87,7 +85,7 @@ class SegmentAdminForm(WagtailAdminModelForm):
if not self.instance.is_static:
self.instance.count = 0
if is_new:
if is_new and self.instance.is_static and not self.instance.all_rules_static:
rules = [
form.instance for formset in self.formsets.values()
for form in formset
@ -108,23 +106,24 @@ class SegmentAdminForm(WagtailAdminModelForm):
users_to_add = []
users_to_exclude = []
sessions = Session.objects.iterator()
take_session = takewhile(
lambda x: instance.count == 0 or len(users_to_add) <= instance.count,
sessions
)
for session in take_session:
session_data = session.get_decoded()
user = user_from_data(session_data.get('_auth_user_id'))
if user.is_authenticated():
request.user = user
request.session = SessionStore(session_key=session.session_key)
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
if passes and instance.randomise_into_segment():
users_to_add.append(user)
elif passes:
users_to_exclude.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.excluded_users.add(*users_to_exclude)

View File

@ -1,4 +1,3 @@
from __future__ import absolute_import, unicode_literals
import random
from django import forms
@ -11,9 +10,9 @@ from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel
from wagtail.wagtailadmin.edit_handlers import (
from wagtail.admin.edit_handlers import (
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
from wagtail.wagtailcore.models import Page
from wagtail.core.models import Page
from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import count_active_days
@ -163,15 +162,11 @@ class Segment(ClusterableModel):
def get_used_pages(self):
"""Return the pages that have variants using this segment."""
pages = list(PersonalisablePageMetadata.objects.filter(segment=self))
return pages
return PersonalisablePageMetadata.objects.filter(segment=self)
def get_created_variants(self):
"""Return the variants using this segment."""
pages = Page.objects.filter(_personalisable_page_metadata__segment=self)
return pages
return Page.objects.filter(_personalisable_page_metadata__segment=self)
def get_rules(self):
"""Retrieve all rules in the segment."""
@ -215,10 +210,13 @@ class PersonalisablePageMetadata(ClusterableModel):
)
variant = models.OneToOneField(
Page, related_name='_personalisable_page_metadata')
Page, related_name='_personalisable_page_metadata',
on_delete=models.CASCADE)
segment = models.ForeignKey(
Segment, related_name='page_metadata', null=True, blank=True)
Segment, related_name='page_metadata',
on_delete=models.SET_NULL,
null=True, blank=True)
@cached_property
def has_variants(self):
@ -289,7 +287,7 @@ class PersonalisablePageMetadata(ClusterableModel):
return Segment.objects.none()
class PersonalisablePageMixin(object):
class PersonalisablePageMixin:
"""The personalisable page model. Allows creation of variants with linked
segments.

View File

@ -7,14 +7,15 @@ from importlib import import_module
from django.apps import apps
from django.conf import settings
from django.contrib.sessions.models import Session
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.template.defaultfilters import slugify
from django.test.client import RequestFactory
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from django.test.client import RequestFactory
from modelcluster.fields import ParentalKey
from user_agents import parse
from wagtail.wagtailadmin.edit_handlers import (
from wagtail.admin.edit_handlers import (
FieldPanel, FieldRowPanel, PageChooserPanel)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@ -239,6 +240,12 @@ class VisitCountRule(AbstractBaseRule):
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('/')

File diff suppressed because one or more lines are too long

View File

@ -24,6 +24,7 @@
{% for segment in object_list %}
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
<h2>{{ segment }}</h2>
<div class="inspect_container">
<ul class="inspect segment_stats">
<li class="stat_card">

View File

@ -1,5 +1,6 @@
import time
from django.db.models import F
from django.template.base import FilterExpression, kwarg_re
from django.utils import timezone
@ -98,22 +99,11 @@ def parse_tag(token, parser):
def exclude_variants(pages):
"""Checks if page is not a variant
:param pages: List of pages to check
:type pages: list
:return: List of pages that aren't variants
:rtype: list
:param pages: Set of pages to check
:type pages: QuerySet
:return: Queryset of pages that aren't variants
:rtype: QuerySet
"""
return [
page for page in pages
if (
(
hasattr(page, 'personalisation_metadata') is False
) or
(
hasattr(page, 'personalisation_metadata') and page.personalisation_metadata is None
) or
(
hasattr(page, 'personalisation_metadata') and page.personalisation_metadata.is_canonical
)
)
]
return pages.filter(
personalisable_canonical_metadata__canonical_page_id=F(
'personalisable_canonical_metadata__variant__id'))

View File

@ -1,14 +1,16 @@
from __future__ import absolute_import, unicode_literals
import csv
from django import forms
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect
from django.http import (
HttpResponse, HttpResponseForbidden, HttpResponseRedirect)
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.contrib.modeladmin.views import IndexView
from wagtail.wagtailcore.models import Page
from wagtail.core.models import Page
from wagtail_personalisation.models import Segment

View File

@ -3,14 +3,14 @@ from __future__ import absolute_import, unicode_literals
import logging
from django.conf.urls import include, url
from django.core.urlresolvers import reverse
from django.template.defaultfilters import pluralize
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin.site_summary import PagesSummaryItem, SummaryItem
from wagtail.wagtailadmin.widgets import Button, ButtonWithDropdownFromHook
from wagtail.wagtailcore import hooks
from wagtail.wagtailcore.models import Page
from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem
from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook
from wagtail.core import hooks
from wagtail.core.models import Page
from wagtail_personalisation import admin_urls, models, utils
from wagtail_personalisation.adapters import get_segment_adapter
@ -23,9 +23,7 @@ def register_admin_urls():
"""Adds the administration urls for the personalisation apps."""
return [
url(r'^personalisation/', include(
admin_urls,
app_name='wagtail_personalisation',
namespace='wagtail_personalisation')),
admin_urls, namespace='wagtail_personalisation')),
]
@ -35,7 +33,7 @@ def set_visit_count(page, request, serve_args, serve_kwargs):
to a segment.
:param page: The page being served
:type page: wagtail.wagtailcore.models.Page
:type page: wagtail.core.models.Page
:param request: The http request
:type request: django.http.HttpRequest
@ -49,7 +47,7 @@ def segment_user(page, request, serve_args, serve_kwargs):
"""Apply a segment to a visitor before serving the page.
:param page: The page being served
:type page: wagtail.wagtailcore.models.Page
:type page: wagtail.core.models.Page
:param request: The http request
:type request: django.http.HttpRequest
@ -63,12 +61,12 @@ def serve_variant(page, request, serve_args, serve_kwargs):
"""Apply a segment to a visitor before serving the page.
:param page: The page being served
:type page: wagtail.wagtailcore.models.Page
:type page: wagtail.core.models.Page
:param request: The http request
:type request: django.http.HttpRequest
:returns: A variant if one is available for the visitor's segment,
otherwise the original page
:rtype: wagtail.wagtailcore.models.Page
:rtype: wagtail.core.models.Page
"""
user_segments = []

View File

@ -9,7 +9,7 @@ pytest_plugins = [
@pytest.fixture(scope='session')
def django_db_setup(django_db_setup, django_db_blocker):
from wagtail.wagtailcore.models import Page, Site
from wagtail.core.models import Page, Site
with django_db_blocker.unblock():
# Remove some initial data that is brought by the tests.site module

View File

@ -8,6 +8,7 @@ from tests.site.pages import models
class ContentPageFactory(PageFactory):
parent = None
title = 'Test page'
slug = factory.LazyAttribute(lambda obj: slugify(obj.title))

View File

@ -1,5 +1,5 @@
import factory
from wagtail.wagtailcore.models import Site
from wagtail.core.models import Site
from tests.factories.page import ContentPageFactory

View File

@ -2,9 +2,6 @@ from __future__ import absolute_import, unicode_literals
import os
import django
from pkg_resources import parse_version as V
DATABASES = {
'default': {
'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():
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()
'wagtail.core.middleware.SiteMiddleware',
)
INSTALLED_APPS = (
'wagtail_personalisation',
'wagtail.contrib.modeladmin',
'wagtail.wagtailsearch',
'wagtail.wagtailsites',
'wagtail.wagtailusers',
'wagtail.wagtailimages',
'wagtail.wagtaildocs',
'wagtail.wagtailadmin',
'wagtail.wagtailcore',
'wagtail.search',
'wagtail.sites',
'wagtail.users',
'wagtail.images',
'wagtail.documents',
'wagtail.admin',
'wagtail.core',
'taggit',

View File

@ -3,7 +3,7 @@
from __future__ import unicode_literals
import django.db.models.deletion
import wagtail.wagtailcore.fields
import wagtail.core.fields
from django.db import migrations, models
import wagtail_personalisation.models
@ -23,7 +23,7 @@ class Migration(migrations.Migration):
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
('subtitle', models.CharField(blank=True, default='', max_length=255)),
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
],
options={
'abstract': False,

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on 2017-06-02 04:26
from __future__ import unicode_literals
import django.db.models.deletion
import wagtail.wagtailcore.fields
import wagtail.core.fields
from django.db import migrations, models
@ -20,7 +19,7 @@ class Migration(migrations.Migration):
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
('subtitle', models.CharField(blank=True, default='', max_length=255)),
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
],
options={
'abstract': False,

View File

@ -1,9 +1,7 @@
from __future__ import absolute_import, unicode_literals
from django.db import models
from wagtail.wagtailadmin.edit_handlers import FieldPanel
from wagtail.wagtailcore.fields import RichTextField
from wagtail.wagtailcore.models import Page
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.core.fields import RichTextField
from wagtail.core.models import Page
from wagtail_personalisation.models import PersonalisablePageMixin

View File

@ -2,12 +2,12 @@ from __future__ import absolute_import, unicode_literals
from django.conf.urls import include, url
from django.contrib import admin
from wagtail.wagtailadmin import urls as wagtailadmin_urls
from wagtail.wagtailcore import urls as wagtail_urls
from wagtail.wagtaildocs import urls as wagtaildocs_urls
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
urlpatterns = [
url(r'^django-admin/', include(admin.site.urls)),
url(r'^django-admin/', admin.site.urls),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),

View File

@ -20,6 +20,23 @@ def test_get_segments(rf):
assert segments == [segment_1, segment_2]
@pytest.mark.django_db
def test_get_segments_session(rf):
request = rf.get('/')
adapter = adapters.SessionSegmentsAdapter(request)
segment_1 = SegmentFactory(name='segment-1', persistent=True)
segment_2 = SegmentFactory(name='segment-2', persistent=True)
adapter.set_segments([segment_1, segment_2])
assert len(request.session['segments']) == 2
adapter._segment_cache = None
segments = adapter.get_segments()
assert segments == [segment_1, segment_2]
@pytest.mark.django_db
def test_get_segment_by_id(rf):
request = rf.get('/')

View File

@ -4,7 +4,7 @@ import datetime
import pytest
from tests.factories.rule import ReferralRuleFactory, QueryRuleFactory
from tests.factories.rule import QueryRuleFactory, ReferralRuleFactory
from tests.factories.segment import SegmentFactory
from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import TimeRule

View File

@ -4,7 +4,9 @@ import datetime
import pytest
from tests.factories.page import ContentPageFactory
from tests.factories.segment import SegmentFactory
from tests.site.pages import models
from wagtail_personalisation.rules import TimeRule
@ -25,3 +27,10 @@ def test_metadata_page_has_variants(segmented_page):
canonical = segmented_page.personalisation_metadata.canonical_page
assert canonical.personalisation_metadata.is_canonical
assert canonical.personalisation_metadata.has_variants
@pytest.mark.django_db
def test_content_page_model():
page = ContentPageFactory()
qs = models.ContentPage.objects.all()
assert page in qs

View File

@ -2,6 +2,7 @@ import pytest
from tests.factories.rule import VisitCountRuleFactory
from tests.factories.segment import SegmentFactory
from wagtail_personalisation.rules import VisitCountRule
@pytest.mark.django_db
@ -25,6 +26,12 @@ def test_visit_count(site, client):
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')

View File

@ -35,22 +35,32 @@ def form_with_data(segment, *rules):
@pytest.mark.django_db
def test_session_added_to_static_segment_at_creation(site, client, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
def test_user_added_to_static_segment_at_creation(site, user, mocker):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
instance = form.save()
assert user in instance.static_users.all()
@pytest.mark.django_db
def test_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.save()
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)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
instance = form.save()
assert not instance.static_users.all()
assert mock_test_rule.call_count == 0
@pytest.mark.django_db
def test_match_any_correct_populates(site, client, django_user_model):
def test_match_any_correct_populates(site, django_user_model, mocker):
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')
client.cookies.clear()
second_session = client.session
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)
rule_1 = VisitCountRule(counted_page=site.root_page)
rule_2 = VisitCountRule(counted_page=other_page)
form = form_with_data(segment, rule_1, rule_2)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', side_effect=[True, False, True, False])
instance = form.save()
assert session.session_key != second_session.session_key
assert user in instance.static_users.all()
assert other_user in instance.static_users.all()
@pytest.mark.django_db
def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, client, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, mocker):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
static_rule = VisitCountRule(counted_page=site.root_page)
non_static_rule = TimeRule(
@ -102,9 +101,12 @@ def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, client,
end_time=datetime.time(23, 59, 59),
)
form = form_with_data(segment, static_rule, non_static_rule)
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
instance = form.save()
assert not instance.static_users.all()
assert mock_test_rule.call_count == 0
@pytest.mark.django_db
@ -180,12 +182,7 @@ def test_session_not_added_to_static_segment_after_full(site, client, django_use
@pytest.mark.django_db
def test_sessions_not_added_to_static_segment_if_rule_not_static(client, site, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
def test_sessions_not_added_to_static_segment_if_rule_not_static(mocker):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = TimeRule(
start_time=datetime.time(0, 0, 0),
@ -193,26 +190,27 @@ def test_sessions_not_added_to_static_segment_if_rule_not_static(client, site, u
segment=segment,
)
form = form_with_data(segment, rule)
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
instance = form.save()
assert not instance.static_users.all()
assert mock_test_rule.call_count == 0
@pytest.mark.django_db
def test_does_not_calculate_the_segment_again(site, client, mocker, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=2)
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
instance = form.save()
assert user in instance.static_users.all()
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
assert mock_test_rule.call_count == 0
@ -389,16 +387,12 @@ def test_always_in_segment_if_percentage_is_100(site, client, mocker, user):
@pytest.mark.django_db
def test_not_added_to_static_segment_at_creation_if_random_above_percent(site, client, mocker, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
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()
@ -406,16 +400,12 @@ def test_not_added_to_static_segment_at_creation_if_random_above_percent(site, c
@pytest.mark.django_db
def test_added_to_static_segment_at_creation_if_random_below_percent(site, client, mocker, user):
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
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()
@ -471,7 +461,7 @@ def test_rules_check_skipped_if_dynamic_segment_in_excluded(site, client, mocker
@pytest.mark.django_db
def test_matched_user_count_added_to_segment_at_creation(site, client, mocker, django_user_model):
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')
@ -479,6 +469,7 @@ def test_matched_user_count_added_to_segment_at_creation(site, client, mocker, d
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()

View File

@ -1,59 +1,26 @@
from wagtail_personalisation.utils import (
exclude_variants, impersonate_other_page)
import pytest
from tests.factories.page import ContentPageFactory
from wagtail_personalisation.utils import impersonate_other_page
class Page(object):
def __init__(self, path, depth, url_path, title):
self.path = path
self.depth = depth
self.url_path = url_path
self.title = title
def __eq__(self, other):
return self.__dict__ == other.__dict__
@pytest.fixture
def rootpage():
return ContentPageFactory(parent=None, path='/', depth=0, title='root')
def test_impersonate_other_page():
page = Page(path="/", depth=0, url_path="/", title="Hoi")
other_page = Page(path="/other", depth=1, url_path="/other", title="Doei")
impersonate_other_page(page, other_page)
assert page == other_page
@pytest.fixture
def page(rootpage):
return ContentPageFactory(parent=rootpage, path='/hi', title='Hi')
class Metadata(object):
def __init__(self, is_canonical=True):
self.is_canonical = is_canonical
@pytest.fixture
def otherpage(rootpage):
return ContentPageFactory(parent=rootpage, path='/bye', title='Bye')
class PersonalisationMetadataPage(object):
def __init__(self):
self.personalisation_metadata = Metadata()
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 == []
@pytest.mark.django_db
def test_impersonate_other_page(page, otherpage):
impersonate_other_page(page, otherpage)
assert page.title == otherpage.title == 'Bye'
assert page.path == otherpage.path

View File

@ -1,7 +1,6 @@
import pytest
from django.urls import reverse
from django.contrib.auth.models import Permission
from django.core.urlresolvers import reverse
from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import VisitCountRule
@ -25,9 +24,8 @@ def test_segment_user_data_view_requires_admin_access(site, client, django_user_
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')
permission = Permission.objects.get(codename='access_admin')
admin_user.user_permissions.add(permission)
admin_user = django_user_model.objects.create(
username='admin', is_superuser=True)
segment = Segment(type=Segment.TYPE_STATIC, count=1)
segment.save()

18
tox.ini
View File

@ -1,26 +1,26 @@
[tox]
envlist = py{27}-django{111}-wagtail{113},lint
envlist = py{36}-django{20}-wagtail{20,21},lint
[testenv]
basepython = python3.6
commands = coverage run --parallel -m pytest {posargs}
extras = test
deps =
django111: django>=1.11,<1.12
wagtail19: wagtail>=1.13,<1.14
django20: django>=2.0,<2.1
wagtail20: wagtail>=2.0,<2.1
wagtail21: wagtail>=2.1,<2.2
[testenv:coverage-report]
basepython = python2.7
basepython = python3.6
deps = coverage
pip_pre = true
skip_install = true
commands =
coverage combine
coverage report
coverage report --include="src/**/" --omit="src/**/migrations/*.py"
[testenv:lint]
basepython = python2.7
deps = flake8==3.5.0
basepython = python3.6
deps = flake8
commands =
flake8 src tests setup.py
isort -q --recursive --diff src/ tests/