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 datetime import datetime
|
||||
from importlib import import_module
|
||||
from itertools import takewhile
|
||||
|
||||
@ -26,6 +27,29 @@ def user_from_data(user_id):
|
||||
|
||||
|
||||
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):
|
||||
cleaned_data = super(SegmentAdminForm, self).clean()
|
||||
Segment = self._meta.model
|
||||
@ -63,6 +87,16 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
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:
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
|
||||
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
|
||||
|
||||
objects = SegmentQuerySet.as_manager()
|
||||
|
||||
base_form_class = SegmentAdminForm
|
||||
|
@ -220,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
|
||||
|
||||
@ -276,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):
|
||||
|
@ -8,7 +8,8 @@ from django.forms.models import model_to_dict
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from wagtail_personalisation.forms import SegmentAdminForm
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import TimeRule, VisitCountRule
|
||||
from wagtail_personalisation.rules import (AbstractBaseRule, TimeRule,
|
||||
VisitCountRule)
|
||||
|
||||
|
||||
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)
|
||||
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