7

Compare commits

...

28 Commits

Author SHA1 Message Date
7bf1bc3f19 Merge branch 'release/0.10.6' 2018-02-06 15:26:02 +02:00
4c60bcbe6b Version 0.10.6 2018-02-06 15:25:51 +02:00
ad4f75d471 Merge pull request #11 from praekeltfoundation/feature/SAS-93-use-randomisation-percentage
Use randomisation percentage
2018-02-06 14:51:56 +02:00
086168954d Test randomisation of static segments at creation 2018-02-05 12:30:12 +02:00
881090f2f9 Test randomisation for dynamic segments 2018-02-05 12:21:09 +02:00
d073c7d268 Randomise into static segments when they are created 2018-02-05 12:20:10 +02:00
7200b5b4c4 If a session passes segment rules randomise them into the segement 2018-02-05 12:19:36 +02:00
6f97c76958 Add method to randomise matching sessions into the segment 2018-02-05 12:18:22 +02:00
ecb4f928fb Merge pull request #10 from praekeltfoundation/feature/SAS-92-store-randomisation-percentage
Add randomisation percentage to segment model
2018-02-05 11:54:23 +02:00
29aa91477e Migrations 2018-02-02 10:15:20 +02:00
5c3acc6661 Display randomisation percentages on segment dashboard 2018-02-02 10:15:04 +02:00
602919d2d4 Test randomisation percentage added to segments 2018-02-02 10:14:18 +02:00
ae97118c3f Store randomisation percentage on segment model 2018-02-02 10:13:18 +02:00
51774b939e Version 0.10.5 2018-01-26 17:57:43 +02:00
908f85e295 Merge pull request #9 from praekeltfoundation/feature/SAS-86-display-record-counter-on-dash
Display record counter on segments in dash
2018-01-26 17:51:34 +02:00
99f9700ed0 Display record counter for active segments 2018-01-26 16:21:15 +02:00
7fa8ee1a46 Merge pull request #8 from praekeltfoundation/feature/SAS-85-only-count-active-frontend-users
Don't include staff and inactive users when counting matched users
2018-01-26 16:01:02 +02:00
5ad70d68f6 Don't include staff and inactive users when counting matched users 2018-01-26 15:38:26 +02:00
06bfe77901 Merge pull request #7 from praekeltfoundation/feature/SAS-85-calculate-matching-users
Count number of users that match static rules for a segment
2018-01-26 08:58:00 +02:00
d5e89d374b Remove unnecessary imports 2018-01-25 19:51:50 +02:00
5b39e82f80 Fixed test for adding user counter to segment 2018-01-25 18:42:38 +02:00
fbcebb43a4 Store record count on a segment when it is created 2018-01-25 15:14:19 +02:00
ef271587ec Test count_matching_users method 2018-01-25 13:26:05 +02:00
786a8801b1 Migrations for Segment.matched_user_count 2018-01-25 11:26:57 +02:00
caf73aa43c Add matched_users_count field to segments 2018-01-25 11:12:46 +02:00
4021d2c915 Add method to calculate the number of users that match a segment 2018-01-24 22:00:28 +02:00
33f96af4a3 Allow test_user() for static rules to accept a user 2018-01-24 15:14:24 +02:00
6299feb497 Merge tag '0.10.4' into develop
0.10.4
2018-01-22 12:58:49 +02:00
12 changed files with 457 additions and 16 deletions

11
CHANGES
View File

@ -1,3 +1,14 @@
0.10.6
==================
- Accepts and stores randomisation percentage for segment
- Adds users to segment based on random number relative to percentage
0.10.5
==================
- Count how many users match a segments rules before saving the segment
- Stores count on the segment and displays in the dashboard
- Enables testing users against rules if there isn't an active request
0.10.0 0.10.0
================== ==================
- Adds static and dynamic segments - Adds static and dynamic segments

View File

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

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.10.4 current_version = 0.10.6
commit = true commit = true
tag = true tag = true
tag_name = {new_version} tag_name = {new_version}

View File

@ -32,7 +32,7 @@ with open('README.rst') as fh:
setup( setup(
name='wagtail-personalisation-molo', name='wagtail-personalisation-molo',
version='0.10.4', version='0.10.6',
description='A forked version of Wagtail add-on for showing personalized content', description='A forked version of Wagtail add-on for showing personalized content',
author='Praekelt.org', author='Praekelt.org',
author_email='dev@praekeltfoundation.org', author_email='dev@praekeltfoundation.org',

View File

@ -184,13 +184,12 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
result = self._test_rules(segment_rules, self.request, result = self._test_rules(segment_rules, self.request,
match_any=segment.match_any) match_any=segment.match_any)
if result and segment.randomise_into_segment():
if result and segment.is_static and not segment.is_full: if segment.is_static and not segment.is_full:
if self.request.user.is_authenticated(): if self.request.user.is_authenticated():
segment.static_users.add(self.request.user) segment.static_users.add(self.request.user)
else:
if result: additional_segments.append(segment)
additional_segments.append(segment)
self.set_segments(current_segments + additional_segments) self.set_segments(current_segments + additional_segments)
self.update_visit_count() self.update_visit_count()

View File

@ -1,5 +1,6 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from datetime import datetime
from importlib import import_module from importlib import import_module
from itertools import takewhile from itertools import takewhile
@ -26,6 +27,29 @@ def user_from_data(user_id):
class SegmentAdminForm(WagtailAdminModelForm): class SegmentAdminForm(WagtailAdminModelForm):
def count_matching_users(self, rules, match_any):
""" Calculates how many users match the given static rules
"""
count = 0
static_rules = [rule for rule in rules if rule.static]
if not static_rules:
return count
User = get_user_model()
users = User.objects.filter(is_active=True, is_staff=False)
for user in users.iterator():
if match_any:
if any(rule.test_user(None, user) for rule in static_rules):
count += 1
elif all(rule.test_user(None, user) for rule in static_rules):
count += 1
return count
def clean(self): def clean(self):
cleaned_data = super(SegmentAdminForm, self).clean() cleaned_data = super(SegmentAdminForm, self).clean()
Segment = self._meta.model Segment = self._meta.model
@ -63,6 +87,16 @@ class SegmentAdminForm(WagtailAdminModelForm):
if not self.instance.is_static: if not self.instance.is_static:
self.instance.count = 0 self.instance.count = 0
if is_new:
rules = [
form.instance for formset in self.formsets.values()
for form in formset
if form not in formset.deleted_forms
]
self.instance.matched_users_count = self.count_matching_users(
rules, self.instance.match_any)
self.instance.matched_count_updated_at = datetime.now()
instance = super(SegmentAdminForm, self).save(*args, **kwargs) instance = super(SegmentAdminForm, self).save(*args, **kwargs)
if is_new and instance.is_static and instance.all_rules_static: if is_new and instance.is_static and instance.all_rules_static:
@ -85,7 +119,7 @@ class SegmentAdminForm(WagtailAdminModelForm):
request.user = user request.user = user
request.session = SessionStore(session_key=session.session_key) request.session = SessionStore(session_key=session.session_key)
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any) passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
if passes: if passes and instance.randomise_into_segment():
users_to_add.append(user) users_to_add.append(user)
instance.static_users.add(*users_to_add) instance.static_users.add(*users_to_add)

View File

@ -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),
),
]

View File

@ -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)]),
),
]

View File

@ -1,7 +1,9 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import random
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models, transaction from django.db import models, transaction
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
@ -82,6 +84,19 @@ class Segment(ClusterableModel):
settings.AUTH_USER_MODEL, 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() objects = SegmentQuerySet.as_manager()
base_form_class = SegmentAdminForm base_form_class = SegmentAdminForm
@ -97,6 +112,7 @@ class Segment(ClusterableModel):
FieldPanel('match_any'), FieldPanel('match_any'),
FieldPanel('type', widget=forms.RadioSelect), FieldPanel('type', widget=forms.RadioSelect),
FieldPanel('count', classname='count_field'), FieldPanel('count', classname='count_field'),
FieldPanel('randomisation_percent', classname='percent_field'),
], heading="Segment"), ], heading="Segment"),
MultiFieldPanel([ MultiFieldPanel([
InlinePanel( InlinePanel(
@ -167,6 +183,19 @@ class Segment(ClusterableModel):
if save: if save:
self.save() self.save()
def randomise_into_segment(self):
""" Returns True if randomisation_percent is not set or it generates
a random number less than the randomisation_percent
This is so there is some randomisation in which users are added to the
segment
"""
if self.randomisation_percent is None:
return True
if random.randint(1, 100) <= self.randomisation_percent:
return True
return False
class PersonalisablePageMetadata(ClusterableModel): class PersonalisablePageMetadata(ClusterableModel):
"""The personalisable page model. Allows creation of variants with linked """The personalisable page model. Allows creation of variants with linked

View File

@ -220,7 +220,12 @@ class VisitCountRule(AbstractBaseRule):
class Meta: class Meta:
verbose_name = _('Visit count Rule') 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 operator = self.operator
segment_count = self.count segment_count = self.count
@ -276,7 +281,13 @@ class QueryRule(AbstractBaseRule):
class Meta: class Meta:
verbose_name = _('Query Rule') 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 return request.GET.get(self.parameter, '') == self.value
def description(self): def description(self):

View File

@ -70,6 +70,13 @@
{% endif %} {% endif %}
</li> </li>
{% if segment.randomisation_percent is not None %}
<li class="stat_card">
<span>{{ segment.randomisation_percent }} %</span>
{% trans "Chance that visitors matching the rules are added to the segment" %}
</li>
{% endif %}
{% for rule in segment.get_rules %} {% for rule in segment.get_rules %}
<li class="stat_card {{ rule.encoded_name }}"> <li class="stat_card {{ rule.encoded_name }}">
{{ rule.description.title }} {{ rule.description.title }}
@ -80,6 +87,11 @@
{% endif %} {% endif %}
</li> </li>
{% endfor %} {% endfor %}
{% if segment.matched_users_count > 0 %}
<li class="stat_card">
<span class="icon icon-fa-user"> {{ segment.matched_users_count }} {% trans "user" %}{{ segment.matched_users_count|pluralize }}</span> {% trans "were possible matches for this segment at creation" %}
</li>
{% endif %}
</ul> </ul>
</div> </div>

View File

@ -8,11 +8,12 @@ from django.forms.models import model_to_dict
from tests.factories.segment import SegmentFactory from tests.factories.segment import SegmentFactory
from wagtail_personalisation.forms import SegmentAdminForm from wagtail_personalisation.forms import SegmentAdminForm
from wagtail_personalisation.models import Segment from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import TimeRule, VisitCountRule from wagtail_personalisation.rules import (AbstractBaseRule, TimeRule,
VisitCountRule)
def form_with_data(segment, *rules): def form_with_data(segment, *rules):
model_fields = ['type', 'status', 'count', 'name', 'match_any'] model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent']
class TestSegmentAdminForm(SegmentAdminForm): class TestSegmentAdminForm(SegmentAdminForm):
class Meta: class Meta:
@ -246,3 +247,301 @@ def test_dynamic_segment_with_non_static_rules_have_a_count():
) )
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
assert form.is_valid(), form.errors assert form.is_valid(), form.errors
@pytest.mark.django_db
def test_randomisation_percentage_added_to_segment_at_creation(site, client, mocker, django_user_model):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
segment.randomisation_percent = 80
rule = VisitCountRule()
form = form_with_data(segment, rule)
instance = form.save()
assert instance.randomisation_percent == 80
@pytest.mark.django_db
def test_randomisation_percentage_min_zero(site, client, mocker, django_user_model):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
segment.randomisation_percent = -1
rule = VisitCountRule()
form = form_with_data(segment, rule)
assert not form.is_valid()
@pytest.mark.django_db
def test_randomisation_percentage_max_100(site, client, mocker, django_user_model):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
segment.randomisation_percent = 101
rule = VisitCountRule()
form = form_with_data(segment, rule)
assert not form.is_valid()
@pytest.mark.django_db
def test_in_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