diff --git a/src/wagtail_personalisation/forms.py b/src/wagtail_personalisation/forms.py index 0c9cd4f..9d342d9 100644 --- a/src/wagtail_personalisation/forms.py +++ b/src/wagtail_personalisation/forms.py @@ -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: diff --git a/src/wagtail_personalisation/migrations/0016_auto_20180125_0918.py b/src/wagtail_personalisation/migrations/0016_auto_20180125_0918.py new file mode 100644 index 0000000..ae7bdd1 --- /dev/null +++ b/src/wagtail_personalisation/migrations/0016_auto_20180125_0918.py @@ -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), + ), + ] diff --git a/src/wagtail_personalisation/models.py b/src/wagtail_personalisation/models.py index f2fcce1..2f9bfcf 100644 --- a/src/wagtail_personalisation/models.py +++ b/src/wagtail_personalisation/models.py @@ -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 diff --git a/src/wagtail_personalisation/rules.py b/src/wagtail_personalisation/rules.py index d94fc50..f4e6fa7 100644 --- a/src/wagtail_personalisation/rules.py +++ b/src/wagtail_personalisation/rules.py @@ -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): diff --git a/tests/unit/test_static_dynamic_segments.py b/tests/unit/test_static_dynamic_segments.py index 0796699..c43e776 100644 --- a/tests/unit/test_static_dynamic_segments.py +++ b/tests/unit/test_static_dynamic_segments.py @@ -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