Merge pull request #7 from praekeltfoundation/feature/SAS-85-calculate-matching-users
Count number of users that match static rules for a segment
This commit is contained in:
@ -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.all()
|
||||||
|
|
||||||
|
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:
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -82,6 +82,9 @@ 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)
|
||||||
|
|
||||||
objects = SegmentQuerySet.as_manager()
|
objects = SegmentQuerySet.as_manager()
|
||||||
|
|
||||||
base_form_class = SegmentAdminForm
|
base_form_class = SegmentAdminForm
|
||||||
|
@ -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):
|
||||||
|
@ -8,7 +8,8 @@ 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):
|
||||||
@ -246,3 +247,129 @@ 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_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_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
|
||||||
|
Reference in New Issue
Block a user