Compare commits
71 Commits
Author | SHA1 | Date | |
---|---|---|---|
7ced6db126 | |||
c6ce67c9c9 | |||
3df3fc0b16 | |||
a00929846e | |||
49fba11049 | |||
e3488e87ad | |||
808aa6d202 | |||
efb060cc6e | |||
414afa5269 | |||
b3f0ac2d58 | |||
4f9c18d2cf | |||
a4a283e4f3 | |||
30318549e2 | |||
f19de241b0 | |||
95ecd8d200 | |||
6436b85b1d | |||
06471248d3 | |||
e3df03f559 | |||
0a42ce3eeb | |||
e5068894c3 | |||
fdc2b97194 | |||
a8d3aeab68 | |||
c76d6d1617 | |||
a8c4b66d6e | |||
f3fbee99a2 | |||
4918c99b5f | |||
330c3bd377 | |||
9c9a9d3acd | |||
51e9aa9724 | |||
a5705fd53c | |||
9d1f3074c0 | |||
3bfd5b8e8f | |||
232609fb4e | |||
35fd4836b0 | |||
b786b0a4d2 | |||
23b1456438 | |||
1f4a4536ab | |||
b8bf27fb99 | |||
d07e06b4f0 | |||
71d7faba1f | |||
743d3f668e | |||
bc0b69fde5 | |||
7cf22d05f6 | |||
9e0fc8e6fd | |||
a116b14d57 | |||
44cc95617e | |||
c6ff2801c5 | |||
0d2834a55f | |||
ff236a095d | |||
ef20580334 | |||
cf41be4b76 | |||
f339879907 | |||
aa2a239aec | |||
8c96fffd4e | |||
675d219f1f | |||
0d9e4aab0c | |||
ac9f32c570 | |||
bc91d64770 | |||
821ee5863e | |||
1a2777835c | |||
d160bb5217 | |||
b021164309 | |||
6f6d6e3a06 | |||
fb7ed4936d | |||
80e33a467e | |||
b553295fc2 | |||
0da7f111e3 | |||
fe6a26e1fd | |||
94c947a435 | |||
ef08403ba3 | |||
8905f471ee |
21
.travis.yml
21
.travis.yml
@ -5,15 +5,10 @@ language: python
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django111-wagtail110
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django111-wagtail110
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django111-wagtail110
|
||||
env: lint
|
||||
|
||||
allow_failures:
|
||||
- python: 3.5
|
||||
env: TOXENV=lint
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django111-wagtail113
|
||||
|
||||
install:
|
||||
- pip install tox codecov
|
||||
@ -24,3 +19,13 @@ 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
|
||||
all_branches: true
|
||||
|
9
.tx/config
Normal file
9
.tx/config
Normal file
@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[wagtail_personalisation]
|
||||
file_filter = src/wagtail_personalisation/locale/<lang>/LC_MESSAGES/django.po
|
||||
source_file = src/wagtail_personalisation/locale/en/LC_MESSAGES/django.po
|
||||
source_lang = en
|
||||
type = PO
|
||||
|
12
CHANGES
12
CHANGES
@ -1,3 +1,15 @@
|
||||
0.10.0
|
||||
==================
|
||||
- Adds static and dynamic segments
|
||||
|
||||
0.9.1 (tbd)
|
||||
==================
|
||||
|
||||
- Fixes import for reverse resolver for older Django versions (<1.10)
|
||||
- Bases migrations off of older wagtail dependencies
|
||||
- Adds more dashboard panels and fixes exclude variants function
|
||||
|
||||
|
||||
0.9.0 (2017-06-02)
|
||||
==================
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
include README.rst
|
||||
|
||||
recursive-include src
|
||||
recursive-include src *
|
||||
|
||||
recursive-exclude src __pycache__
|
||||
recursive-exclude src *.py[co]
|
||||
|
@ -35,7 +35,7 @@ in the admin interface.
|
||||
|
||||
Instructions
|
||||
------------
|
||||
Wagtail Personalisation requires Wagtail 1.10 and Django 1.11.
|
||||
Wagtail Personalisation requires Wagtail 1.9 or 1.10 and Django 1.9, 1.10 or 1.11.
|
||||
|
||||
To install the package with pip::
|
||||
|
||||
|
@ -55,10 +55,10 @@ author = 'Lab Digital BV'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.9.0'
|
||||
version = '0.10.4'
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.9.0'
|
||||
release = '0.10.4'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -86,6 +86,11 @@
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
.stat_card {
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.block_container .block span.icon::before {
|
||||
@ -93,11 +98,6 @@
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.block_container .block .inspect_container .inspect li {
|
||||
display: inline-block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.block_container .block .inspect_container .inspect li span {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
|
22
setup.cfg
22
setup.cfg
@ -1,3 +1,9 @@
|
||||
[bumpversion]
|
||||
current_version = 0.10.4
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = {new_version}
|
||||
|
||||
[tool:pytest]
|
||||
DJANGO_SETTINGS_MODULE = tests.settings
|
||||
minversion = 3.0
|
||||
@ -7,25 +13,19 @@ testpaths = tests
|
||||
python_paths = .
|
||||
|
||||
[flake8]
|
||||
ignore=E731
|
||||
ignore = E731
|
||||
max-line-length = 120
|
||||
exclude=
|
||||
src/**/migrations/*.py
|
||||
exclude =
|
||||
src/**/migrations/*.py
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[coverage:run]
|
||||
omit =
|
||||
src/**/migrations/*.py
|
||||
|
||||
|
||||
[bumpversion]
|
||||
current_version = 0.9.0
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = {new_version}
|
||||
src/**/migrations/*.py
|
||||
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
[bumpversion:file:docs/conf.py]
|
||||
|
||||
|
17
setup.py
17
setup.py
@ -1,9 +1,8 @@
|
||||
import re
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
install_requires = [
|
||||
'wagtail>=1.10,<1.11',
|
||||
'wagtail>=1.10,<1.14',
|
||||
'user-agents>=1.0.1',
|
||||
'wagtailfontawesome>=1.0.6',
|
||||
]
|
||||
@ -18,6 +17,7 @@ tests_require = [
|
||||
'pytest-cov==2.4.0',
|
||||
'pytest-django==3.1.2',
|
||||
'pytest-sugar==0.7.1',
|
||||
'pytest-mock==1.6.3',
|
||||
'pytest==3.1.0',
|
||||
'wagtail_factories==0.3.0',
|
||||
]
|
||||
@ -31,12 +31,12 @@ with open('README.rst') as fh:
|
||||
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
|
||||
|
||||
setup(
|
||||
name='wagtail-personalisation',
|
||||
version='0.9.0',
|
||||
description='A Wagtail add-on for showing personalized content',
|
||||
author='Lab Digital BV',
|
||||
author_email='opensource@labdigital.nl',
|
||||
url='http://labdigital.nl',
|
||||
name='wagtail-personalisation-molo',
|
||||
version='0.10.4',
|
||||
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/',
|
||||
install_requires=install_requires,
|
||||
tests_require=tests_require,
|
||||
extras_require={
|
||||
@ -61,7 +61,6 @@ setup(
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 1.8',
|
||||
'Framework :: Django :: 1.9',
|
||||
'Framework :: Django :: 1.10',
|
||||
'Framework :: Django :: 1.11',
|
||||
|
@ -132,18 +132,19 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
if page_visits:
|
||||
for page_visit in page_visits:
|
||||
page_visit['count'] += 1
|
||||
page_visit['path'] = page.url_path if page else self.request.path
|
||||
self.request.session.modified = True
|
||||
else:
|
||||
visit_count.append({
|
||||
'slug': page.slug,
|
||||
'id': page.pk,
|
||||
'path': self.request.path,
|
||||
'path': page.url_path if page else self.request.path,
|
||||
'count': 1,
|
||||
})
|
||||
|
||||
def get_visit_count(self, page=None):
|
||||
"""Return the number of visits on the current request or given page"""
|
||||
path = page.path if page else self.request.path
|
||||
path = page.url_path if page else self.request.path
|
||||
visit_count = self.request.session.setdefault('visit_count', [])
|
||||
for visit in visit_count:
|
||||
if visit['path'] == path:
|
||||
@ -174,15 +175,22 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
# Run tests on all remaining enabled segments to verify applicability.
|
||||
additional_segments = []
|
||||
for segment in enabled_segments:
|
||||
segment_rules = []
|
||||
for rule_model in rule_models:
|
||||
segment_rules.extend(rule_model.objects.filter(segment=segment))
|
||||
|
||||
result = self._test_rules(segment_rules, self.request,
|
||||
match_any=segment.match_any)
|
||||
|
||||
if result:
|
||||
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
|
||||
additional_segments.append(segment)
|
||||
elif not segment.is_static or not segment.is_full:
|
||||
segment_rules = []
|
||||
for rule_model in rule_models:
|
||||
segment_rules.extend(rule_model.objects.filter(segment=segment))
|
||||
|
||||
result = self._test_rules(segment_rules, self.request,
|
||||
match_any=segment.match_any)
|
||||
|
||||
if result and segment.is_static and not segment.is_full:
|
||||
if self.request.user.is_authenticated():
|
||||
segment.static_users.add(self.request.user)
|
||||
|
||||
if result:
|
||||
additional_segments.append(segment)
|
||||
|
||||
self.set_segments(current_segments + additional_segments)
|
||||
self.update_visit_count()
|
||||
|
101
src/wagtail_personalisation/forms.py
Normal file
101
src/wagtail_personalisation/forms.py
Normal file
@ -0,0 +1,101 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
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
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def user_from_data(user_id):
|
||||
User = get_user_model()
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
class SegmentAdminForm(WagtailAdminModelForm):
|
||||
def clean(self):
|
||||
cleaned_data = super(SegmentAdminForm, self).clean()
|
||||
Segment = self._meta.model
|
||||
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
for form in formset
|
||||
if form not in formset.deleted_forms
|
||||
]
|
||||
consistent = rules and Segment.all_static(rules)
|
||||
|
||||
if cleaned_data.get('type') == Segment.TYPE_STATIC and not cleaned_data.get('count') and not consistent:
|
||||
self.add_error('count', _('Static segments with non-static compatible rules must include a count.'))
|
||||
|
||||
if self.instance.id and self.instance.is_static:
|
||||
if self.has_changed():
|
||||
self.add_error_to_fields(self, excluded=['name', 'enabled'])
|
||||
|
||||
for formset in self.formsets.values():
|
||||
if formset.has_changed():
|
||||
for form in formset:
|
||||
if form not in formset.deleted_forms:
|
||||
self.add_error_to_fields(form)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def add_error_to_fields(self, form, excluded=list()):
|
||||
for field in form.changed_data:
|
||||
if field not in excluded:
|
||||
form.add_error(field, _('Cannot update a static segment'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
|
||||
if not self.instance.is_static:
|
||||
self.instance.count = 0
|
||||
|
||||
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
|
||||
|
||||
if is_new and instance.is_static and instance.all_rules_static:
|
||||
from .adapters import get_segment_adapter
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.session = SessionStore()
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
users_to_add = []
|
||||
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:
|
||||
users_to_add.append(user)
|
||||
|
||||
instance.static_users.add(*users_to_add)
|
||||
|
||||
return instance
|
||||
|
||||
@property
|
||||
def media(self):
|
||||
media = super(SegmentAdminForm, self).media
|
||||
media.add_js(
|
||||
[static('js/segment_form_control.js')]
|
||||
)
|
||||
return media
|
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0030_index_on_pagerevision_created_at'),
|
||||
('wagtailcore', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
@ -2,14 +2,14 @@
|
||||
# Generated by Django 1.11.1 on 2017-05-31 14:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||
('wagtailcore', '0001_initial'),
|
||||
('wagtail_personalisation', '0010_auto_20170531_1101'),
|
||||
]
|
||||
|
||||
|
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-10-17 11:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sessions', '0001_initial'),
|
||||
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='count',
|
||||
field=models.PositiveSmallIntegerField(default=0, help_text='If this number is set for a static segment users will be added to the set until the number is reached. After this no more users will be added.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
field=models.ManyToManyField(to='sessions.Session'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('dynamic', 'Dynamic'), ('static', 'Static')], default='dynamic', help_text='\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ', max_length=20),
|
||||
),
|
||||
]
|
26
src/wagtail_personalisation/migrations/0015_static_users.py
Normal file
26
src/wagtail_personalisation/migrations/0015_static_users.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-11-01 15:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0013_add_dynamic_static_to_segment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='static_users',
|
||||
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
@ -1,10 +1,12 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import itertools
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db import models, transaction
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from modelcluster.models import ClusterableModel
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
@ -14,6 +16,8 @@ from wagtail.wagtailcore.models import Page
|
||||
from wagtail_personalisation.rules import AbstractBaseRule
|
||||
from wagtail_personalisation.utils import count_active_days
|
||||
|
||||
from .forms import SegmentAdminForm
|
||||
|
||||
|
||||
class SegmentQuerySet(models.QuerySet):
|
||||
def enabled(self):
|
||||
@ -31,6 +35,14 @@ class Segment(ClusterableModel):
|
||||
(STATUS_DISABLED, _('Disabled')),
|
||||
)
|
||||
|
||||
TYPE_DYNAMIC = 'dynamic'
|
||||
TYPE_STATIC = 'static'
|
||||
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_DYNAMIC, _('Dynamic')),
|
||||
(TYPE_STATIC, _('Static')),
|
||||
)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
create_date = models.DateTimeField(auto_now_add=True)
|
||||
edit_date = models.DateTimeField(auto_now=True)
|
||||
@ -45,9 +57,35 @@ class Segment(ClusterableModel):
|
||||
default=False,
|
||||
help_text=_("Should the segment match all the rules or just one of them?")
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=20,
|
||||
choices=TYPE_CHOICES,
|
||||
default=TYPE_DYNAMIC,
|
||||
help_text=mark_safe(_("""
|
||||
</br></br><strong>Dynamic:</strong> Users in this segment will change
|
||||
as more or less meet the rules specified in the segment.
|
||||
</br><strong>Static:</strong> If the segment contains only static
|
||||
compatible rules the segment will contain the members that pass
|
||||
those rules when the segment is created. Mixed static segments or
|
||||
those containing entirely non static compatible rules will be
|
||||
populated using the count variable.
|
||||
"""))
|
||||
)
|
||||
count = models.PositiveSmallIntegerField(
|
||||
default=0,
|
||||
help_text=_(
|
||||
"If this number is set for a static segment users will be added to the "
|
||||
"set until the number is reached. After this no more users will be added."
|
||||
)
|
||||
)
|
||||
static_users = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
)
|
||||
|
||||
objects = SegmentQuerySet.as_manager()
|
||||
|
||||
base_form_class = SegmentAdminForm
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Segment.panels = [
|
||||
MultiFieldPanel([
|
||||
@ -57,11 +95,16 @@ class Segment(ClusterableModel):
|
||||
FieldPanel('persistent'),
|
||||
]),
|
||||
FieldPanel('match_any'),
|
||||
FieldPanel('type', widget=forms.RadioSelect),
|
||||
FieldPanel('count', classname='count_field'),
|
||||
], heading="Segment"),
|
||||
MultiFieldPanel([
|
||||
InlinePanel(
|
||||
"{}_related".format(rule_model._meta.db_table),
|
||||
label=rule_model._meta.verbose_name,
|
||||
label='{}{}'.format(
|
||||
rule_model._meta.verbose_name,
|
||||
' ({})'.format(_('Static compatible')) if rule_model.static else ''
|
||||
),
|
||||
) for rule_model in AbstractBaseRule.__subclasses__()
|
||||
], heading=_("Rules")),
|
||||
]
|
||||
@ -71,6 +114,23 @@ class Segment(ClusterableModel):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def is_static(self):
|
||||
return self.type == self.TYPE_STATIC
|
||||
|
||||
@classmethod
|
||||
def all_static(cls, rules):
|
||||
return all(rule.static for rule in rules)
|
||||
|
||||
@property
|
||||
def all_rules_static(self):
|
||||
rules = self.get_rules()
|
||||
return rules and self.all_static(rules)
|
||||
|
||||
@property
|
||||
def is_full(self):
|
||||
return self.static_users.count() >= self.count
|
||||
|
||||
def encoded_name(self):
|
||||
"""Return a string with a slug for the segment."""
|
||||
return slugify(self.name.lower())
|
||||
|
@ -18,6 +18,7 @@ from wagtail.wagtailadmin.edit_handlers import (
|
||||
class AbstractBaseRule(models.Model):
|
||||
"""Base for creating rules to segment users with."""
|
||||
icon = 'fa-circle-o'
|
||||
static = False
|
||||
|
||||
segment = ParentalKey(
|
||||
'wagtail_personalisation.Segment',
|
||||
@ -190,6 +191,7 @@ class VisitCountRule(AbstractBaseRule):
|
||||
|
||||
"""
|
||||
icon = 'fa-calculator'
|
||||
static = True
|
||||
|
||||
OPERATOR_CHOICES = (
|
||||
('more_than', _("More than")),
|
||||
@ -227,7 +229,7 @@ class VisitCountRule(AbstractBaseRule):
|
||||
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
visit_count = adapter.get_visit_count()
|
||||
visit_count = adapter.get_visit_count(self.counted_page)
|
||||
if visit_count and operator == "more_than":
|
||||
if visit_count > segment_count:
|
||||
return True
|
||||
@ -258,7 +260,8 @@ class QueryRule(AbstractBaseRule):
|
||||
present in the request query.
|
||||
|
||||
"""
|
||||
icon = 'fa-link-o'
|
||||
icon = 'fa-link'
|
||||
static = True
|
||||
|
||||
parameter = models.SlugField(_("The query parameter to search for"),
|
||||
max_length=20)
|
||||
|
@ -1,2 +1,2 @@
|
||||
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li{display:inline-block;margin-bottom:5px}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
|
||||
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block .inspect_container .inspect .stat_card{display:inline-block;margin-bottom:5px;margin-right:10px}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
|
||||
/*# sourceMappingURL=dashboard.css.map*/
|
@ -1 +1 @@
|
||||
{"version":3,"sources":["webpack:///./scss/dashboard.scss"],"names":[],"mappings":"AAAA,cACI,kBACA,kBAAmB,CAGvB,iBACI,cACA,eAAgB,CAGpB,wBACI,cACA,WACA,8BAAsB,sBACtB,kBACA,uBACA,iBACA,kBACA,mBACA,yBACA,kBACA,sBACA,+DAAkE,uDAClE,2GAA8F,2UAC9F,cAAe,CAGnB,0FAEI,UAAY,CAGZ,2BACI,qBACA,UAAW,CAGnB,uCACI,iBAAkB,CAGlB,uCACI,gBACA,SACA,SAAU,CAGV,0CACI,WACA,iBAAkB,CAGtB,qDACI,cAAe,CAG3B,mCACI,yBAA0B,CAG9B,8BACI,sBACA,uEAAkE,+DAGtE,yBACI,wBACI,WACA,cAAe,CAClB,CAGL,2CACI,oBAAa,iCACb,8BAAmB,uEACnB,qBAAiB,iBACjB,yBAA8B,oDAC9B,0BAAoB,2CACpB,kBAAmB,CAGvB,oDACI,cACA,WACA,uBACA,UACA,SACA,eAAgB,CAGhB,yCACI,kBACA,qBAAsB,CAG1B,uDACI,qBACA,iBAAkB,CAGtB,4DACI,cACA,eACA,gBACA,aACA,wBAAyB,CAG7B,2DACI,kBACA,8BAAsB,sBACtB,WACA,sBACA,sBACA,sBACA,gBACA,qBACA,qBACA,iBAAkB,CAG1B,oDACI,cACA,kBACA,wBACA,kBACA,QACA,mCAA2B,2BAC3B,cACA,eACA,eAAgB","file":"../css/dashboard.css","sourcesContent":[".nice-padding {\n padding-left: 50px;\n padding-right: 50px;\n}\n\n.block_container {\n display: block;\n margin-top: 30px;\n}\n\n.block_container .block {\n display: block;\n float: left;\n box-sizing: border-box;\n position: relative;\n width: calc(50% - 10px);\n min-height: 216px;\n padding: 10px 20px;\n margin-bottom: 20px;\n border: 1px solid #d9d9d9;\n border-radius: 3px;\n background-color: #fff;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);\n cursor: pointer;\n}\n\n.block_container .block--disabled h2,\n.block_container .block--disabled .inspect_container {\n opacity: 0.5;\n}\n\n .block_container .block h2 {\n display: inline-block;\n width: auto;\n }\n\n.block_container .block:nth-child(odd) {\n margin-right: 20px;\n}\n\n .block_container .block .block_actions {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n .block_container .block .block_actions li {\n float: left;\n margin-right: 10px;\n }\n\n .block_container .block .block_actions li:last-child {\n margin-right: 0;\n }\n\n.block_container .block.suggestion {\n border: 1px dashed #d9d9d9;\n}\n\n.block_container .block:hover {\n border: 1px solid #fff;\n box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);\n}\n\n@media (max-width: 699px) {\n .block_container .block {\n width: 100%;\n margin-right: 0;\n }\n}\n\n.block_container .block .inspect_container {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: stretch;\n margin-bottom: 10px;\n}\n\n.block_container .block .inspect_container .inspect {\n display: block;\n float: left;\n width: calc(50% - 10px);\n padding: 0;\n margin: 0;\n list-style: none;\n}\n\n .block_container .block span.icon::before {\n margin-right: 0.3em;\n vertical-align: bottom;\n }\n\n .block_container .block .inspect_container .inspect li {\n display: inline-block;\n margin-bottom: 5px;\n }\n\n .block_container .block .inspect_container .inspect li span {\n display: block;\n font-size: 20px;\n font-weight: bold;\n margin: 5px 0;\n overflow-wrap: break-word;\n }\n\n .block_container .block .inspect_container .inspect li pre {\n position: relative;\n box-sizing: border-box;\n width: auto;\n background-color: #eee;\n border: 1px solid #ccc;\n margin: 5px 0 5px 21px;\n padding: 2px 5px;\n word-wrap: break-word;\n word-break: break-all;\n border-radius: 3px;\n }\n\n.block_container .block.suggestion .suggestive_text {\n display: block;\n position: absolute;\n width: calc(100% - 40px);\n text-align: center;\n top: 50%;\n transform: translateY(-50%);\n color: #d9d9d9;\n font-size: 20px;\n font-weight: 100;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./scss/dashboard.scss"],"sourceRoot":""}
|
||||
{"version":3,"sources":["webpack:///./scss/dashboard.scss"],"names":[],"mappings":"AAAA,cACI,kBACA,kBAAmB,CAGvB,iBACI,cACA,eAAgB,CAGpB,wBACI,cACA,WACA,8BAAsB,sBACtB,kBACA,uBACA,iBACA,kBACA,mBACA,yBACA,kBACA,sBACA,+DAAkE,uDAClE,2GAA8F,2UAC9F,cAAe,CAGnB,0FAEI,UAAY,CAGZ,2BACI,qBACA,UAAW,CAGnB,uCACI,iBAAkB,CAGlB,uCACI,gBACA,SACA,SAAU,CAGV,0CACI,WACA,iBAAkB,CAGtB,qDACI,cAAe,CAG3B,mCACI,yBAA0B,CAG9B,8BACI,sBACA,uEAAkE,+DAGtE,yBACI,wBACI,WACA,cAAe,CAClB,CAGL,2CACI,oBAAa,iCACb,8BAAmB,uEACnB,qBAAiB,iBACjB,yBAA8B,oDAC9B,0BAAoB,2CACpB,kBAAmB,CAGvB,oDACI,cACA,WACA,uBACA,UACA,SACA,eAAgB,CAMnB,+DAJO,qBACA,kBACA,iBAAkB,CAItB,yCACI,kBACA,qBAAsB,CAG1B,4DACI,cACA,eACA,gBACA,aACA,wBAAyB,CAG7B,2DACI,kBACA,8BAAsB,sBACtB,WACA,sBACA,sBACA,sBACA,gBACA,qBACA,qBACA,iBAAkB,CAG1B,oDACI,cACA,kBACA,wBACA,kBACA,QACA,mCAA2B,2BAC3B,cACA,eACA,eAAgB","file":"../css/dashboard.css","sourcesContent":[".nice-padding {\n padding-left: 50px;\n padding-right: 50px;\n}\n\n.block_container {\n display: block;\n margin-top: 30px;\n}\n\n.block_container .block {\n display: block;\n float: left;\n box-sizing: border-box;\n position: relative;\n width: calc(50% - 10px);\n min-height: 216px;\n padding: 10px 20px;\n margin-bottom: 20px;\n border: 1px solid #d9d9d9;\n border-radius: 3px;\n background-color: #fff;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);\n cursor: pointer;\n}\n\n.block_container .block--disabled h2,\n.block_container .block--disabled .inspect_container {\n opacity: 0.5;\n}\n\n .block_container .block h2 {\n display: inline-block;\n width: auto;\n }\n\n.block_container .block:nth-child(odd) {\n margin-right: 20px;\n}\n\n .block_container .block .block_actions {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n .block_container .block .block_actions li {\n float: left;\n margin-right: 10px;\n }\n\n .block_container .block .block_actions li:last-child {\n margin-right: 0;\n }\n\n.block_container .block.suggestion {\n border: 1px dashed #d9d9d9;\n}\n\n.block_container .block:hover {\n border: 1px solid #fff;\n box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);\n}\n\n@media (max-width: 699px) {\n .block_container .block {\n width: 100%;\n margin-right: 0;\n }\n}\n\n.block_container .block .inspect_container {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: stretch;\n margin-bottom: 10px;\n}\n\n.block_container .block .inspect_container .inspect {\n display: block;\n float: left;\n width: calc(50% - 10px);\n padding: 0;\n margin: 0;\n list-style: none;\n .stat_card {\n display: inline-block;\n margin-bottom: 5px;\n margin-right: 10px;\n }\n}\n\n .block_container .block span.icon::before {\n margin-right: 0.3em;\n vertical-align: bottom;\n }\n\n .block_container .block .inspect_container .inspect li span {\n display: block;\n font-size: 20px;\n font-weight: bold;\n margin: 5px 0;\n overflow-wrap: break-word;\n }\n\n .block_container .block .inspect_container .inspect li pre {\n position: relative;\n box-sizing: border-box;\n width: auto;\n background-color: #eee;\n border: 1px solid #ccc;\n margin: 5px 0 5px 21px;\n padding: 2px 5px;\n word-wrap: break-word;\n word-break: break-all;\n border-radius: 3px;\n }\n\n.block_container .block.suggestion .suggestive_text {\n display: block;\n position: absolute;\n width: calc(100% - 40px);\n text-align: center;\n top: 50%;\n transform: translateY(-50%);\n color: #d9d9d9;\n font-size: 20px;\n font-weight: 100;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./scss/dashboard.scss"],"sourceRoot":""}
|
@ -0,0 +1,20 @@
|
||||
(function($) {
|
||||
$(document).ready( () => {
|
||||
var count = $('.count_field');
|
||||
var typeRadio = $('input:radio[name="type"]');
|
||||
|
||||
var updateCountDispay = function(value) {
|
||||
if (value == 'dynamic') {
|
||||
count.slideUp(250);
|
||||
} else {
|
||||
count.slideDown(250);
|
||||
}
|
||||
};
|
||||
|
||||
updateCountDispay(typeRadio.filter(':checked').val());
|
||||
|
||||
typeRadio.change( event => {
|
||||
updateCountDispay(event.target.value);
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
@ -22,24 +22,37 @@
|
||||
<div class="nice-padding block_container">
|
||||
{% if all_count %}
|
||||
{% for segment in object_list %}
|
||||
<div class="block block--{{ segment.status }}" onclick="location.href = 'edit/{{ segment.pk }}'">
|
||||
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
|
||||
<h2>{{ segment }}</h2>
|
||||
<div class="inspect_container">
|
||||
<ul class="inspect segment_stats">
|
||||
<li class="visit_stat">
|
||||
<li class="stat_card">
|
||||
{% trans "This segment has been visited" %}
|
||||
<span class="icon icon-fa-rocket">{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
|
||||
</li>
|
||||
<li class="days_stat">
|
||||
<li class="stat_card">
|
||||
{% trans "This segment has been active for" %}
|
||||
<span class="icon icon-fa-calendar">{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
|
||||
</li>
|
||||
{% if segment.is_static %}
|
||||
<li class="stat_card">
|
||||
{% trans "This segment is Static" %}
|
||||
<span class="icon icon-fa-user">
|
||||
{{ segment.sessions.count|localize }}
|
||||
{% if segment.sessions.count < segment.count %}
|
||||
/ {{ segment.count }} {% trans "member" %}{{ segment.count|pluralize }}
|
||||
{% else %}
|
||||
{% trans "member" %}{{ segment.sessions.count|pluralize }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<hr />
|
||||
|
||||
<ul class="inspect segment_rules">
|
||||
<li class="match_state {{ segment.match_any|yesno:"any,all" }}">
|
||||
<li class="stat_card {{ segment.match_any|yesno:"any,all" }}">
|
||||
{% trans "The visitor must match" %}
|
||||
{% if segment.match_any %}
|
||||
<span class="icon icon-fa-cube">{% trans "Any rule" %}</span>
|
||||
@ -48,7 +61,7 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
<li class="persistent_state {{ segment.persistent|yesno:"persistent,fleeting" }}">
|
||||
<li class="stat_card {{ segment.persistent|yesno:"persistent,fleeting" }}">
|
||||
{% trans "The persistence of this segment is" %}
|
||||
{% if segment.persistent %}
|
||||
<span class="icon icon-fa-bookmark" title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
|
||||
@ -58,7 +71,7 @@
|
||||
</li>
|
||||
|
||||
{% for rule in segment.get_rules %}
|
||||
<li class="{{ rule.encoded_name }}">
|
||||
<li class="stat_card {{ rule.encoded_name }}">
|
||||
{{ rule.description.title }}
|
||||
{% if rule.description.code %}
|
||||
<pre>{{ rule.description.value }}</pre>
|
||||
@ -77,7 +90,7 @@
|
||||
{% elif segment.status == segment.STATUS_ENABLED %}
|
||||
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
||||
{% endif %}
|
||||
<li><a href="edit/{{ segment.pk }}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
||||
<li><a href="{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -93,3 +93,27 @@ def parse_tag(token, parser):
|
||||
args.append(FilterExpression(bit, parser))
|
||||
|
||||
return (tag_name, args, kwargs)
|
||||
|
||||
|
||||
def exclude_variants(pages):
|
||||
"""Checks if page is not a variant
|
||||
|
||||
:param pages: List of pages to check
|
||||
:type pages: list
|
||||
:return: List of pages that aren't variants
|
||||
:rtype: list
|
||||
"""
|
||||
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
|
||||
)
|
||||
)
|
||||
]
|
||||
|
@ -1,8 +1,9 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django import forms
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, reverse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
||||
from wagtail.contrib.modeladmin.views import IndexView
|
||||
|
@ -4,15 +4,16 @@ import logging
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.defaultfilters import pluralize
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailadmin.site_summary import SummaryItem
|
||||
from wagtail.wagtailadmin.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_personalisation import admin_urls, models
|
||||
from wagtail_personalisation import admin_urls, models, utils
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import PersonalisablePageMixin, Segment
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -71,7 +72,7 @@ def serve_variant(page, request, serve_args, serve_kwargs):
|
||||
|
||||
"""
|
||||
user_segments = []
|
||||
if not isinstance(page, PersonalisablePageMixin):
|
||||
if not isinstance(page, models.PersonalisablePageMixin):
|
||||
return
|
||||
|
||||
adapter = get_segment_adapter(request)
|
||||
@ -89,9 +90,7 @@ def serve_variant(page, request, serve_args, serve_kwargs):
|
||||
|
||||
@hooks.register('construct_explorer_page_queryset')
|
||||
def dont_show_variant(parent_page, pages, request):
|
||||
return [page for page in pages
|
||||
if (page.personalisation_metadata is None)
|
||||
or (page.personalisation_metadata.is_canonical)]
|
||||
return utils.exclude_variants(pages)
|
||||
|
||||
|
||||
@hooks.register('register_page_listing_buttons')
|
||||
@ -147,15 +146,33 @@ def page_listing_more_buttons(page, page_perms, is_parent=False):
|
||||
priority=200)
|
||||
|
||||
|
||||
class CorrectedPagesSummaryPanel(PagesSummaryItem):
|
||||
def get_context(self):
|
||||
context = super(CorrectedPagesSummaryPanel, self).get_context()
|
||||
|
||||
pages = utils.exclude_variants(Page.objects.all().specific())
|
||||
|
||||
context['total_pages'] = len(pages) - 1
|
||||
return context
|
||||
|
||||
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
def add_corrected_pages_summary_panel(request, items):
|
||||
"""Replaces the Pages summary panel to hide variants."""
|
||||
for index, item in enumerate(items):
|
||||
if item.__class__ is PagesSummaryItem:
|
||||
items[index] = CorrectedPagesSummaryPanel(request)
|
||||
|
||||
|
||||
class SegmentSummaryPanel(SummaryItem):
|
||||
"""The segment summary panel showing the total amount of segments on the
|
||||
site and allowing quick access to the Segment dashboard.
|
||||
|
||||
"""
|
||||
order = 500
|
||||
order = 2000
|
||||
|
||||
def render(self):
|
||||
segment_count = Segment.objects.count()
|
||||
segment_count = models.Segment.objects.count()
|
||||
target_url = reverse('wagtail_personalisation_segment_modeladmin_index')
|
||||
title = _("Segments")
|
||||
return mark_safe("""
|
||||
@ -164,11 +181,39 @@ class SegmentSummaryPanel(SummaryItem):
|
||||
</li>""".format(target_url, segment_count, title))
|
||||
|
||||
|
||||
class PersonalisedPagesSummaryPanel(PagesSummaryItem):
|
||||
order = 2100
|
||||
|
||||
def render(self):
|
||||
page_count = models.PersonalisablePageMetadata.objects.filter(
|
||||
segment__isnull=True).count()
|
||||
title = _("Personalised Page")
|
||||
return mark_safe("""
|
||||
<li class="icon icon-fa-file-o">
|
||||
<span>{}</span>{}{}
|
||||
</li>""".format(page_count, title, pluralize(page_count)))
|
||||
|
||||
|
||||
class VariantPagesSummaryPanel(PagesSummaryItem):
|
||||
order = 2200
|
||||
|
||||
def render(self):
|
||||
page_count = models.PersonalisablePageMetadata.objects.filter(
|
||||
segment__isnull=False).count()
|
||||
title = _("Variant")
|
||||
return mark_safe("""
|
||||
<li class="icon icon-fa-files-o">
|
||||
<span>{}</span>{}{}
|
||||
</li>""".format(page_count, title, pluralize(page_count)))
|
||||
|
||||
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
def add_segment_summary_panel(request, items):
|
||||
def add_personalisation_summary_panels(request, items):
|
||||
"""Adds a summary panel to the Wagtail dashboard showing the total amount
|
||||
of segments on the site and allowing quick access to the Segment
|
||||
dashboard.
|
||||
|
||||
"""
|
||||
items.append(SegmentSummaryPanel(request))
|
||||
items.append(PersonalisedPagesSummaryPanel(request))
|
||||
items.append(VariantPagesSummaryPanel(request))
|
||||
|
@ -44,3 +44,8 @@ class RequestFactory(BaseRequestFactory):
|
||||
request.session = SessionStore()
|
||||
request._messages = FallbackStorage(request)
|
||||
return request
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user(django_user_model):
|
||||
return django_user_model.objects.create(username='user')
|
||||
|
@ -2,6 +2,9 @@ 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'),
|
||||
@ -52,17 +55,26 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
MIDDLEWARE = (
|
||||
'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',
|
||||
)
|
||||
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()
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'wagtail_personalisation',
|
||||
|
@ -14,14 +14,14 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||
('wagtailcore', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContentPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
||||
],
|
||||
|
@ -2,15 +2,15 @@
|
||||
# Generated by Django 1.11.1 on 2017-06-02 04:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import wagtail.wagtailcore.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0033_remove_golive_expiry_help_text'),
|
||||
('wagtailcore', '0001_initial'),
|
||||
('pages', '0001_initial'),
|
||||
]
|
||||
|
||||
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='RegularPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
||||
],
|
||||
|
@ -4,16 +4,14 @@ import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.factories.page import ContentPageFactory
|
||||
from tests.factories.rule import (
|
||||
DayRuleFactory, DeviceRuleFactory, ReferralRuleFactory, TimeRuleFactory)
|
||||
from tests.factories.rule import ReferralRuleFactory, QueryRuleFactory
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from tests.factories.site import SiteFactory
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import TimeRule
|
||||
|
||||
# Factory tests
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_segment_create():
|
||||
factoried_segment = SegmentFactory()
|
||||
@ -27,8 +25,6 @@ def test_segment_create():
|
||||
assert factoried_segment.status == segment.status
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_referral_rule_create():
|
||||
segment = SegmentFactory(name='Referral')
|
||||
@ -37,3 +33,16 @@ def test_referral_rule_create():
|
||||
segment=segment)
|
||||
|
||||
assert referral_rule.regex_string == 'test.test'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_query_rule_create():
|
||||
segment = SegmentFactory(name='Query')
|
||||
query_rule = QueryRuleFactory(
|
||||
parameter="query",
|
||||
value="value",
|
||||
segment=segment)
|
||||
|
||||
assert query_rule.parameter == 'query'
|
||||
assert query_rule.value == 'value'
|
||||
assert query_rule.static
|
||||
|
@ -16,6 +16,8 @@ def test_time_rule_create():
|
||||
segment=segment)
|
||||
|
||||
assert time_rule.start_time == datetime.time(8, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@freeze_time("10:00:00")
|
||||
def test_requesttime_segment(client, site):
|
||||
|
248
tests/unit/test_static_dynamic_segments.py
Normal file
248
tests/unit/test_static_dynamic_segments.py
Normal file
@ -0,0 +1,248 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from wagtail_personalisation.forms import SegmentAdminForm
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import TimeRule, VisitCountRule
|
||||
|
||||
|
||||
def form_with_data(segment, *rules):
|
||||
model_fields = ['type', 'status', 'count', 'name', 'match_any']
|
||||
|
||||
class TestSegmentAdminForm(SegmentAdminForm):
|
||||
class Meta:
|
||||
model = Segment
|
||||
fields = model_fields
|
||||
|
||||
data = model_to_dict(segment, model_fields)
|
||||
for formset in TestSegmentAdminForm().formsets.values():
|
||||
rule_data = {}
|
||||
count = 0
|
||||
for rule in rules:
|
||||
if isinstance(rule, formset.model):
|
||||
rule_data = model_to_dict(rule)
|
||||
for key, value in rule_data.items():
|
||||
data['{}-{}-{}'.format(formset.prefix, count, key)] = value
|
||||
count += 1
|
||||
data['{}-INITIAL_FORMS'.format(formset.prefix)] = 0
|
||||
data['{}-TOTAL_FORMS'.format(formset.prefix)] = count
|
||||
return TestSegmentAdminForm(data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_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)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
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):
|
||||
session = client.session
|
||||
session.save()
|
||||
client.get(site.root_page.url)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_match_any_correct_populates(site, client, django_user_model):
|
||||
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)
|
||||
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)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
static_rule = VisitCountRule(counted_page=site.root_page)
|
||||
non_static_rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
)
|
||||
form = form_with_data(segment, static_rule, non_static_rule)
|
||||
instance = form.save()
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_session_not_added_to_static_segment_after_creation(site, client, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_session_added_to_static_segment_after_creation(site, client, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_anonymou_user_not_added_to_static_segment_after_creation(site, client):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_session_not_added_to_static_segment_after_full(site, client, django_user_model):
|
||||
user = django_user_model.objects.create(username='first')
|
||||
other_user = django_user_model.objects.create(username='second')
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
session = client.session
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert instance.static_users.count() == 1
|
||||
|
||||
client.cookies.clear()
|
||||
second_session = client.session
|
||||
client.force_login(other_user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert session.session_key != second_session.session_key
|
||||
assert instance.static_users.count() == 1
|
||||
assert user in instance.static_users.all()
|
||||
assert other_user not in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_sessions_not_added_to_static_segment_if_rule_not_static(client, site, user):
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
|
||||
rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
segment=segment,
|
||||
)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
|
||||
@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)
|
||||
instance = form.save()
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
|
||||
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
client.get(site.root_page.url)
|
||||
assert mock_test_rule.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_non_static_rules_have_a_count():
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||
rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
segment=segment,
|
||||
)
|
||||
form = form_with_data(segment, rule)
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_static_segment_with_static_rules_needs_no_count(site):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
|
||||
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||
form = form_with_data(segment, rule)
|
||||
assert form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dynamic_segment_with_non_static_rules_have_a_count():
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC, count=0)
|
||||
rule = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
)
|
||||
form = form_with_data(segment, rule)
|
||||
assert form.is_valid(), form.errors
|
@ -1,4 +1,5 @@
|
||||
from wagtail_personalisation.utils import impersonate_other_page
|
||||
from wagtail_personalisation.utils import (
|
||||
exclude_variants, impersonate_other_page)
|
||||
|
||||
|
||||
class Page(object):
|
||||
@ -19,3 +20,40 @@ def test_impersonate_other_page():
|
||||
impersonate_other_page(page, other_page)
|
||||
|
||||
assert page == other_page
|
||||
|
||||
|
||||
class Metadata(object):
|
||||
def __init__(self, is_canonical=True):
|
||||
self.is_canonical = is_canonical
|
||||
|
||||
|
||||
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 == []
|
||||
|
10
tox.ini
10
tox.ini
@ -1,15 +1,15 @@
|
||||
[tox]
|
||||
envlist = py{27,35,36}-django{111}-wagtail{110},lint
|
||||
envlist = py{27}-django{111}-wagtail{113},lint
|
||||
|
||||
[testenv]
|
||||
commands = coverage run --parallel -m pytest {posargs}
|
||||
extras = test
|
||||
deps =
|
||||
django111: django>=1.11,<1.12
|
||||
wagtail110: wagtail>=1.10,<1.11
|
||||
wagtail19: wagtail>=1.13,<1.14
|
||||
|
||||
[testenv:coverage-report]
|
||||
basepython = python3.5
|
||||
basepython = python2.7
|
||||
deps = coverage
|
||||
pip_pre = true
|
||||
skip_install = true
|
||||
@ -19,8 +19,8 @@ commands =
|
||||
|
||||
|
||||
[testenv:lint]
|
||||
basepython = python3.5
|
||||
deps = flake8
|
||||
basepython = python2.7
|
||||
deps = flake8==3.5.0
|
||||
commands =
|
||||
flake8 src tests setup.py
|
||||
isort -q --recursive --diff src/ tests/
|
||||
|
Reference in New Issue
Block a user