Compare commits
83 Commits
Author | SHA1 | Date | |
---|---|---|---|
7bf1bc3f19 | |||
4c60bcbe6b | |||
ad4f75d471 | |||
086168954d | |||
881090f2f9 | |||
d073c7d268 | |||
7200b5b4c4 | |||
6f97c76958 | |||
ecb4f928fb | |||
29aa91477e | |||
5c3acc6661 | |||
602919d2d4 | |||
ae97118c3f | |||
51774b939e | |||
908f85e295 | |||
99f9700ed0 | |||
7fa8ee1a46 | |||
5ad70d68f6 | |||
06bfe77901 | |||
d5e89d374b | |||
5b39e82f80 | |||
fbcebb43a4 | |||
ef271587ec | |||
786a8801b1 | |||
caf73aa43c | |||
4021d2c915 | |||
33f96af4a3 | |||
6299feb497 | |||
7ced6db126 | |||
c6ce67c9c9 | |||
3df3fc0b16 | |||
a00929846e | |||
49fba11049 | |||
e3488e87ad | |||
808aa6d202 | |||
efb060cc6e | |||
414afa5269 | |||
b3f0ac2d58 | |||
4f9c18d2cf | |||
a4a283e4f3 | |||
30318549e2 | |||
f19de241b0 | |||
95ecd8d200 | |||
6436b85b1d | |||
06471248d3 | |||
e3df03f559 | |||
0a42ce3eeb | |||
e5068894c3 | |||
fdc2b97194 | |||
a8d3aeab68 | |||
c76d6d1617 | |||
a8c4b66d6e | |||
f3fbee99a2 | |||
4918c99b5f | |||
330c3bd377 | |||
9c9a9d3acd | |||
51e9aa9724 | |||
a5705fd53c | |||
9d1f3074c0 | |||
3bfd5b8e8f | |||
232609fb4e | |||
35fd4836b0 | |||
b786b0a4d2 | |||
23b1456438 | |||
1f4a4536ab | |||
b8bf27fb99 | |||
d07e06b4f0 | |||
71d7faba1f | |||
743d3f668e | |||
bc0b69fde5 | |||
7cf22d05f6 | |||
9e0fc8e6fd | |||
a116b14d57 | |||
44cc95617e | |||
c6ff2801c5 | |||
0d2834a55f | |||
ff236a095d | |||
ef20580334 | |||
cf41be4b76 | |||
f339879907 | |||
aa2a239aec | |||
8c96fffd4e | |||
675d219f1f |
36
.travis.yml
36
.travis.yml
@ -4,33 +4,11 @@ language: python
|
||||
|
||||
matrix:
|
||||
include:
|
||||
# Django 1.9, Wagtail 1.9
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django19-wagtail19
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django19-wagtail19
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django19-wagtail19
|
||||
env: lint
|
||||
|
||||
# Django 1.10, Wagtail 1.10
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django110-wagtail110
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django110-wagtail110
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django110-wagtail110
|
||||
|
||||
# Django 1.11, Wagtail 1.10
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django111-wagtail110
|
||||
- python: 3.5
|
||||
env: TOXENV=py35-django111-wagtail110
|
||||
- python: 3.6
|
||||
env: TOXENV=py36-django111-wagtail110
|
||||
|
||||
allow_failures:
|
||||
- python: 3.5
|
||||
env: TOXENV=lint
|
||||
env: TOXENV=py27-django111-wagtail113
|
||||
|
||||
install:
|
||||
- pip install tox codecov
|
||||
@ -41,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
|
||||
|
15
CHANGES
15
CHANGES
@ -1,3 +1,18 @@
|
||||
0.10.6
|
||||
==================
|
||||
- Accepts and stores randomisation percentage for segment
|
||||
- Adds users to segment based on random number relative to percentage
|
||||
|
||||
0.10.5
|
||||
==================
|
||||
- Count how many users match a segments rules before saving the segment
|
||||
- Stores count on the segment and displays in the dashboard
|
||||
- Enables testing users against rules if there isn't an active request
|
||||
|
||||
0.10.0
|
||||
==================
|
||||
- Adds static and dynamic segments
|
||||
|
||||
0.9.1 (tbd)
|
||||
==================
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
include README.rst
|
||||
|
||||
recursive-include src
|
||||
recursive-include src *
|
||||
|
||||
recursive-exclude src __pycache__
|
||||
recursive-exclude src *.py[co]
|
||||
|
@ -55,10 +55,10 @@ author = 'Lab Digital BV'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.9.1'
|
||||
version = '0.10.6'
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.9.1'
|
||||
release = '0.10.6'
|
||||
|
||||
# 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;
|
||||
|
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.9.1
|
||||
current_version = 0.10.6
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = {new_version}
|
||||
|
16
setup.py
16
setup.py
@ -1,9 +1,8 @@
|
||||
import re
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
|
||||
install_requires = [
|
||||
'wagtail>=1.9,<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.1',
|
||||
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.6',
|
||||
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={
|
||||
|
@ -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,21 @@ 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.randomise_into_segment():
|
||||
if segment.is_static and not segment.is_full:
|
||||
if self.request.user.is_authenticated():
|
||||
segment.static_users.add(self.request.user)
|
||||
else:
|
||||
additional_segments.append(segment)
|
||||
|
||||
self.set_segments(current_segments + additional_segments)
|
||||
self.update_visit_count()
|
||||
|
135
src/wagtail_personalisation/forms.py
Normal file
135
src/wagtail_personalisation/forms.py
Normal file
@ -0,0 +1,135 @@
|
||||
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
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
def user_from_data(user_id):
|
||||
User = get_user_model()
|
||||
try:
|
||||
return User.objects.get(id=user_id)
|
||||
except User.DoesNotExist:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
class SegmentAdminForm(WagtailAdminModelForm):
|
||||
|
||||
def count_matching_users(self, rules, match_any):
|
||||
""" Calculates how many users match the given static rules
|
||||
"""
|
||||
count = 0
|
||||
|
||||
static_rules = [rule for rule in rules if rule.static]
|
||||
|
||||
if not static_rules:
|
||||
return count
|
||||
|
||||
User = get_user_model()
|
||||
users = User.objects.filter(is_active=True, is_staff=False)
|
||||
|
||||
for user in users.iterator():
|
||||
if match_any:
|
||||
if any(rule.test_user(None, user) for rule in static_rules):
|
||||
count += 1
|
||||
elif all(rule.test_user(None, user) for rule in static_rules):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(SegmentAdminForm, self).clean()
|
||||
Segment = self._meta.model
|
||||
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
for form in formset
|
||||
if form not in formset.deleted_forms
|
||||
]
|
||||
consistent = rules and Segment.all_static(rules)
|
||||
|
||||
if cleaned_data.get('type') == Segment.TYPE_STATIC and not cleaned_data.get('count') and not consistent:
|
||||
self.add_error('count', _('Static segments with non-static compatible rules must include a count.'))
|
||||
|
||||
if self.instance.id and self.instance.is_static:
|
||||
if self.has_changed():
|
||||
self.add_error_to_fields(self, excluded=['name', 'enabled'])
|
||||
|
||||
for formset in self.formsets.values():
|
||||
if formset.has_changed():
|
||||
for form in formset:
|
||||
if form not in formset.deleted_forms:
|
||||
self.add_error_to_fields(form)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def add_error_to_fields(self, form, excluded=list()):
|
||||
for field in form.changed_data:
|
||||
if field not in excluded:
|
||||
form.add_error(field, _('Cannot update a static segment'))
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
|
||||
if not self.instance.is_static:
|
||||
self.instance.count = 0
|
||||
|
||||
if is_new:
|
||||
rules = [
|
||||
form.instance for formset in self.formsets.values()
|
||||
for form in formset
|
||||
if form not in formset.deleted_forms
|
||||
]
|
||||
self.instance.matched_users_count = self.count_matching_users(
|
||||
rules, self.instance.match_any)
|
||||
self.instance.matched_count_updated_at = datetime.now()
|
||||
|
||||
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
|
||||
|
||||
if is_new and instance.is_static and instance.all_rules_static:
|
||||
from .adapters import get_segment_adapter
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.session = SessionStore()
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
users_to_add = []
|
||||
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)
|
||||
|
||||
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
|
@ -2,8 +2,8 @@
|
||||
# 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):
|
||||
|
@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-10-17 11:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('sessions', '0001_initial'),
|
||||
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='count',
|
||||
field=models.PositiveSmallIntegerField(default=0, help_text='If this number is set for a static segment users will be added to the set until the number is reached. After this no more users will be added.'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
field=models.ManyToManyField(to='sessions.Session'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='type',
|
||||
field=models.CharField(choices=[('dynamic', 'Dynamic'), ('static', 'Static')], default='dynamic', help_text='\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ', max_length=20),
|
||||
),
|
||||
]
|
26
src/wagtail_personalisation/migrations/0015_static_users.py
Normal file
26
src/wagtail_personalisation/migrations/0015_static_users.py
Normal file
@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.6 on 2017-11-01 15:58
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0013_add_dynamic_static_to_segment'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='segment',
|
||||
name='sessions',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='static_users',
|
||||
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-01-25 09:18
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0015_static_users'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='matched_count_updated_at',
|
||||
field=models.DateTimeField(editable=False, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='matched_users_count',
|
||||
field=models.PositiveIntegerField(default=0, editable=False),
|
||||
),
|
||||
]
|
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.8 on 2018-01-31 16:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0016_auto_20180125_0918'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='randomisation_percent',
|
||||
field=models.PositiveSmallIntegerField(blank=True, default=None, help_text='If this number is set each user matching the rules will have this percentage chance of being placed in the segment.', null=True, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
@ -1,9 +1,14 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import random
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from modelcluster.models import ClusterableModel
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
@ -13,6 +18,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):
|
||||
@ -30,6 +37,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)
|
||||
@ -44,9 +59,48 @@ 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,
|
||||
)
|
||||
|
||||
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
|
||||
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
|
||||
|
||||
randomisation_percent = models.PositiveSmallIntegerField(
|
||||
null=True, blank=True, default=None,
|
||||
help_text=_(
|
||||
"If this number is set each user matching the rules will "
|
||||
"have this percentage chance of being placed in the segment."
|
||||
), validators=[
|
||||
MaxValueValidator(100),
|
||||
MinValueValidator(0)
|
||||
])
|
||||
|
||||
objects = SegmentQuerySet.as_manager()
|
||||
|
||||
base_form_class = SegmentAdminForm
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
Segment.panels = [
|
||||
MultiFieldPanel([
|
||||
@ -56,11 +110,17 @@ class Segment(ClusterableModel):
|
||||
FieldPanel('persistent'),
|
||||
]),
|
||||
FieldPanel('match_any'),
|
||||
FieldPanel('type', widget=forms.RadioSelect),
|
||||
FieldPanel('count', classname='count_field'),
|
||||
FieldPanel('randomisation_percent', classname='percent_field'),
|
||||
], heading="Segment"),
|
||||
MultiFieldPanel([
|
||||
InlinePanel(
|
||||
"{}_related".format(rule_model._meta.db_table),
|
||||
label=rule_model._meta.verbose_name,
|
||||
label='{}{}'.format(
|
||||
rule_model._meta.verbose_name,
|
||||
' ({})'.format(_('Static compatible')) if rule_model.static else ''
|
||||
),
|
||||
) for rule_model in AbstractBaseRule.__subclasses__()
|
||||
], heading=_("Rules")),
|
||||
]
|
||||
@ -70,6 +130,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())
|
||||
@ -106,6 +183,19 @@ class Segment(ClusterableModel):
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
def randomise_into_segment(self):
|
||||
""" Returns True if randomisation_percent is not set or it generates
|
||||
a random number less than the randomisation_percent
|
||||
This is so there is some randomisation in which users are added to the
|
||||
segment
|
||||
"""
|
||||
if self.randomisation_percent is None:
|
||||
return True
|
||||
|
||||
if random.randint(1, 100) <= self.randomisation_percent:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PersonalisablePageMetadata(ClusterableModel):
|
||||
"""The personalisable page model. Allows creation of variants with linked
|
||||
|
@ -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")),
|
||||
@ -218,7 +220,12 @@ class VisitCountRule(AbstractBaseRule):
|
||||
class Meta:
|
||||
verbose_name = _('Visit count Rule')
|
||||
|
||||
def test_user(self, request):
|
||||
def test_user(self, request, user=None):
|
||||
if user:
|
||||
# This rule currently does not support testing a user directly
|
||||
# TODO: Make this test a user directly when the rule uses
|
||||
# historical data
|
||||
return False
|
||||
operator = self.operator
|
||||
segment_count = self.count
|
||||
|
||||
@ -227,7 +234,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
|
||||
@ -259,6 +266,7 @@ class QueryRule(AbstractBaseRule):
|
||||
|
||||
"""
|
||||
icon = 'fa-link'
|
||||
static = True
|
||||
|
||||
parameter = models.SlugField(_("The query parameter to search for"),
|
||||
max_length=20)
|
||||
@ -273,7 +281,13 @@ class QueryRule(AbstractBaseRule):
|
||||
class Meta:
|
||||
verbose_name = _('Query Rule')
|
||||
|
||||
def test_user(self, request):
|
||||
def test_user(self, request, user=None):
|
||||
if user:
|
||||
# This rule currently does not support testing a user directly
|
||||
# TODO: Make this test a user directly if/when the rule uses
|
||||
# historical data
|
||||
return False
|
||||
|
||||
return request.GET.get(self.parameter, '') == self.value
|
||||
|
||||
def description(self):
|
||||
|
@ -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>
|
||||
@ -57,8 +70,15 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
{% if segment.randomisation_percent is not None %}
|
||||
<li class="stat_card">
|
||||
<span>{{ segment.randomisation_percent }} %</span>
|
||||
{% trans "Chance that visitors matching the rules are added to the segment" %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for rule in segment.get_rules %}
|
||||
<li class="{{ rule.encoded_name }}">
|
||||
<li class="stat_card {{ rule.encoded_name }}">
|
||||
{{ rule.description.title }}
|
||||
{% if rule.description.code %}
|
||||
<pre>{{ rule.description.value }}</pre>
|
||||
@ -67,6 +87,11 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if segment.matched_users_count > 0 %}
|
||||
<li class="stat_card">
|
||||
<span class="icon icon-fa-user"> {{ segment.matched_users_count }} {% trans "user" %}{{ segment.matched_users_count|pluralize }}</span> {% trans "were possible matches for this segment at creation" %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -77,7 +102,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>
|
||||
|
@ -103,9 +103,17 @@ def exclude_variants(pages):
|
||||
: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)]
|
||||
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
|
||||
)
|
||||
)
|
||||
]
|
||||
|
@ -7,7 +7,7 @@ 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, PagesSummaryItem
|
||||
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
|
||||
|
@ -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')
|
||||
|
@ -1,10 +1,9 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import os
|
||||
from pkg_resources import parse_version as V
|
||||
|
||||
import django
|
||||
|
||||
from pkg_resources import parse_version as V
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
@ -56,6 +55,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_middleware_settings():
|
||||
return (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
@ -69,6 +69,7 @@ def get_middleware_settings():
|
||||
'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()
|
||||
|
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
||||
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,9 +2,9 @@
|
||||
# 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):
|
||||
@ -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):
|
||||
|
547
tests/unit/test_static_dynamic_segments.py
Normal file
547
tests/unit/test_static_dynamic_segments.py
Normal file
@ -0,0 +1,547 @@
|
||||
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 (AbstractBaseRule, TimeRule,
|
||||
VisitCountRule)
|
||||
|
||||
|
||||
def form_with_data(segment, *rules):
|
||||
model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent']
|
||||
|
||||
class TestSegmentAdminForm(SegmentAdminForm):
|
||||
class Meta:
|
||||
model = Segment
|
||||
fields = model_fields
|
||||
|
||||
data = model_to_dict(segment, model_fields)
|
||||
for formset in TestSegmentAdminForm().formsets.values():
|
||||
rule_data = {}
|
||||
count = 0
|
||||
for rule in rules:
|
||||
if isinstance(rule, formset.model):
|
||||
rule_data = model_to_dict(rule)
|
||||
for key, value in rule_data.items():
|
||||
data['{}-{}-{}'.format(formset.prefix, count, key)] = value
|
||||
count += 1
|
||||
data['{}-INITIAL_FORMS'.format(formset.prefix)] = 0
|
||||
data['{}-TOTAL_FORMS'.format(formset.prefix)] = count
|
||||
return TestSegmentAdminForm(data)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_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
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_randomisation_percentage_added_to_segment_at_creation(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = 80
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert instance.randomisation_percent == 80
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_randomisation_percentage_min_zero(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = -1
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_randomisation_percentage_max_100(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = 101
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_in_segment_if_random_is_below_percentage(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=39)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_in_segment_if_random_is_above_percentage(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=41)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert user not in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_in_segment_if_percentage_is_0(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=0)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert user not in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_always_in_segment_if_percentage_is_100(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=100)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
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)
|
||||
instance = form.save()
|
||||
|
||||
assert user not in instance.static_users.all()
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
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)
|
||||
instance = form.save()
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_matched_user_count_added_to_segment_at_creation(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
instance = form.save()
|
||||
|
||||
assert mock_test_user.call_count == 2
|
||||
instance.matched_users_count = 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_users_matching_static_rules(site, client, django_user_model):
|
||||
class TestStaticRule(AbstractBaseRule):
|
||||
static = True
|
||||
|
||||
class Meta:
|
||||
app_label = 'wagtail_personalisation'
|
||||
|
||||
def test_user(self, request, user):
|
||||
return True
|
||||
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = TestStaticRule()
|
||||
form = form_with_data(segment, rule)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_excludes_staff(site, client, django_user_model):
|
||||
class TestStaticRule(AbstractBaseRule):
|
||||
static = True
|
||||
|
||||
class Meta:
|
||||
app_label = 'wagtail_personalisation'
|
||||
|
||||
def test_user(self, request, user):
|
||||
return True
|
||||
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second', is_staff=True)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = TestStaticRule()
|
||||
form = form_with_data(segment, rule)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_excludes_inactive(site, client, django_user_model):
|
||||
class TestStaticRule(AbstractBaseRule):
|
||||
static = True
|
||||
|
||||
class Meta:
|
||||
app_label = 'wagtail_personalisation'
|
||||
|
||||
def test_user(self, request, user):
|
||||
return True
|
||||
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second', is_active=False)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = TestStaticRule()
|
||||
form = form_with_data(segment, rule)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_only_counts_static_rules(site, client, django_user_model):
|
||||
class TestStaticRule(AbstractBaseRule):
|
||||
class Meta:
|
||||
app_label = 'wagtail_personalisation'
|
||||
|
||||
def test_user(self, request, user):
|
||||
return True
|
||||
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = TestStaticRule()
|
||||
form = form_with_data(segment, rule)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_handles_match_any(site, client, django_user_model):
|
||||
class TestStaticRuleFirst(AbstractBaseRule):
|
||||
static = True
|
||||
|
||||
class Meta:
|
||||
app_label = 'wagtail_personalisation'
|
||||
|
||||
def test_user(self, request, user):
|
||||
if user.username == 'first':
|
||||
return True
|
||||
return False
|
||||
|
||||
class TestStaticRuleSecond(AbstractBaseRule):
|
||||
static = True
|
||||
|
||||
class Meta:
|
||||
app_label = 'wagtail_personalisation'
|
||||
|
||||
def test_user(self, request, user):
|
||||
if user.username == 'second':
|
||||
return True
|
||||
return False
|
||||
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
first_rule = TestStaticRuleFirst()
|
||||
second_rule = TestStaticRuleSecond()
|
||||
form = form_with_data(segment, first_rule, second_rule)
|
||||
|
||||
assert form.count_matching_users([first_rule, second_rule], True) is 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_handles_match_all(site, client, django_user_model):
|
||||
class TestStaticRuleFirst(AbstractBaseRule):
|
||||
static = True
|
||||
|
||||
class Meta:
|
||||
app_label = 'wagtail_personalisation'
|
||||
|
||||
def test_user(self, request, user):
|
||||
if user.username == 'first':
|
||||
return True
|
||||
return False
|
||||
|
||||
class TestStaticRuleContainsS(AbstractBaseRule):
|
||||
static = True
|
||||
|
||||
class Meta:
|
||||
app_label = 'wagtail_personalisation'
|
||||
|
||||
def test_user(self, request, user):
|
||||
if 's' in user.username:
|
||||
return True
|
||||
return False
|
||||
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second')
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
first_rule = TestStaticRuleFirst()
|
||||
s_rule = TestStaticRuleContainsS()
|
||||
form = form_with_data(segment, first_rule, s_rule)
|
||||
|
||||
assert form.count_matching_users([first_rule, s_rule], False) is 1
|
@ -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 == []
|
||||
|
13
tox.ini
13
tox.ini
@ -1,18 +1,15 @@
|
||||
[tox]
|
||||
envlist = py{27,35,36}-django{19,110,111}-wagtail{19,110},lint
|
||||
envlist = py{27}-django{111}-wagtail{113},lint
|
||||
|
||||
[testenv]
|
||||
commands = coverage run --parallel -m pytest {posargs}
|
||||
extras = test
|
||||
deps =
|
||||
django19: django>=1.9,<1.10
|
||||
django110: django>=1.10<1.11
|
||||
django111: django>=1.11,<1.12
|
||||
wagtail19: wagtail>=1.9,<1.10
|
||||
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
|
||||
@ -22,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