8

Add the logic for static segments

This commit is contained in:
Todd Dembrey
2017-10-17 16:57:07 +01:00
parent 0d9e4aab0c
commit 675d219f1f
5 changed files with 200 additions and 8 deletions

View File

@@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
from django.conf import settings from django.conf import settings
from django.db.models import F from django.db.models import F
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
from django.utils import timezone
from wagtail_personalisation.models import Segment from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import AbstractBaseRule from wagtail_personalisation.rules import AbstractBaseRule
@@ -174,15 +175,25 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
# Run tests on all remaining enabled segments to verify applicability. # Run tests on all remaining enabled segments to verify applicability.
additional_segments = [] additional_segments = []
for segment in enabled_segments: for segment in enabled_segments:
segment_rules = [] if segment.is_static and self.request.session in segment.sessions.all():
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:
additional_segments.append(segment) 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.is_static and not segment.is_full:
session = self.request.session.model.objects.get(
session_key=self.request.session.session_key,
expire_date__gt=timezone.now(),
)
segment.sessions.add(session)
if result:
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

@@ -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='The users in a dynamic segment will change as more or less users meet the rules specified in the segment. Static segments will contain the members that existed at creation.', max_length=20),
),
]

View File

@@ -1,9 +1,19 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from importlib import import_module
from django import forms
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.db import models, transaction from django.db import models, transaction
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.test.client import RequestFactory
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.lru_cache import lru_cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel from modelcluster.models import ClusterableModel
from wagtail.wagtailadmin.edit_handlers import ( from wagtail.wagtailadmin.edit_handlers import (
@@ -14,11 +24,23 @@ from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import count_active_days from wagtail_personalisation.utils import count_active_days
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
class SegmentQuerySet(models.QuerySet): class SegmentQuerySet(models.QuerySet):
def enabled(self): def enabled(self):
return self.filter(status=self.model.STATUS_ENABLED) return self.filter(status=self.model.STATUS_ENABLED)
@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
@python_2_unicode_compatible @python_2_unicode_compatible
class Segment(ClusterableModel): class Segment(ClusterableModel):
"""The segment model.""" """The segment model."""
@@ -30,6 +52,14 @@ class Segment(ClusterableModel):
(STATUS_DISABLED, _('Disabled')), (STATUS_DISABLED, _('Disabled')),
) )
TYPE_DYNAMIC = 'dynamic'
TYPE_STATIC = 'static'
TYPE_CHOICES = (
(TYPE_DYNAMIC, _('Dynamic')),
(TYPE_STATIC, _('Static')),
)
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
create_date = models.DateTimeField(auto_now_add=True) create_date = models.DateTimeField(auto_now_add=True)
edit_date = models.DateTimeField(auto_now=True) edit_date = models.DateTimeField(auto_now=True)
@@ -44,6 +74,24 @@ class Segment(ClusterableModel):
default=False, default=False,
help_text=_("Should the segment match all the rules or just one of them?") 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=_(
"The users in a dynamic segment will change as more or less users meet "
"the rules specified in the segment. Static segments will contain the "
"members that existed at creation."
)
)
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."
)
)
sessions = models.ManyToManyField(Session)
objects = SegmentQuerySet.as_manager() objects = SegmentQuerySet.as_manager()
@@ -56,6 +104,8 @@ class Segment(ClusterableModel):
FieldPanel('persistent'), FieldPanel('persistent'),
]), ]),
FieldPanel('match_any'), FieldPanel('match_any'),
FieldPanel('type', widget=forms.RadioSelect),
FieldPanel('count'),
], heading="Segment"), ], heading="Segment"),
MultiFieldPanel([ MultiFieldPanel([
InlinePanel( InlinePanel(
@@ -70,6 +120,14 @@ class Segment(ClusterableModel):
def __str__(self): def __str__(self):
return self.name return self.name
@property
def is_static(self):
return self.type == self.TYPE_STATIC
@property
def is_full(self):
return self.sessions.count() >= self.count
def encoded_name(self): def encoded_name(self):
"""Return a string with a slug for the segment.""" """Return a string with a slug for the segment."""
return slugify(self.name.lower()) return slugify(self.name.lower())
@@ -106,6 +164,21 @@ class Segment(ClusterableModel):
if save: if save:
self.save() self.save()
def save(self, *args, **kwargs):
super(Segment, self).save(*args, **kwargs)
if self.is_static:
request = RequestFactory().get('/')
for session in Session.objects.filter(
expire_date__gt=timezone.now(),
).iterator():
session_data = session.get_decoded()
user = user_from_data(session_data.get('_auth_id'))
request.user = user
request.session = SessionStore(session_key=session.session_key)
if all(rule.test_user(request) for rule in self.get_rules() if rule.static):
self.sessions.add(session)
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

@@ -18,6 +18,7 @@ from wagtail.wagtailadmin.edit_handlers import (
class AbstractBaseRule(models.Model): class AbstractBaseRule(models.Model):
"""Base for creating rules to segment users with.""" """Base for creating rules to segment users with."""
icon = 'fa-circle-o' icon = 'fa-circle-o'
static = False
segment = ParentalKey( segment = ParentalKey(
'wagtail_personalisation.Segment', 'wagtail_personalisation.Segment',
@@ -190,6 +191,7 @@ class VisitCountRule(AbstractBaseRule):
""" """
icon = 'fa-calculator' icon = 'fa-calculator'
static = True
OPERATOR_CHOICES = ( OPERATOR_CHOICES = (
('more_than', _("More than")), ('more_than', _("More than")),

View File

@@ -0,0 +1,75 @@
from __future__ import absolute_import, unicode_literals
import datetime
import pytest
from tests.factories.segment import SegmentFactory
from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import TimeRule, VisitCountRule
@pytest.mark.django_db
def test_session_added_to_static_segment_at_creation(rf, site, client):
session = client.session
session.save()
client.get(site.root_page.url)
segment = SegmentFactory(type=Segment.TYPE_STATIC)
VisitCountRule.objects.create(counted_page=site.root_page, segment=segment)
assert session.session_key in segment.sessions.values_list('session_key', flat=True)
@pytest.mark.django_db
def test_session_not_added_to_static_segment_after_creation(rf, site, client):
segment = SegmentFactory(type=Segment.TYPE_STATIC)
VisitCountRule.objects.create(counted_page=site.root_page, segment=segment)
session = client.session
session.save()
client.get(site.root_page.url)
assert not segment.sessions.all()
@pytest.mark.django_db
def test_session_added_to_static_segment_after_creation(rf, site, client):
segment = SegmentFactory(type=Segment.TYPE_STATIC, count=1)
VisitCountRule.objects.create(counted_page=site.root_page, segment=segment)
session = client.session
session.save()
client.get(site.root_page.url)
assert session.session_key in segment.sessions.values_list('session_key', flat=True)
@pytest.mark.django_db
def test_session_not_added_to_static_segment_after_full(rf, site, client):
segment = SegmentFactory(type=Segment.TYPE_STATIC, count=1)
VisitCountRule.objects.create(counted_page=site.root_page, segment=segment)
session = client.session
session.save()
client.get(site.root_page.url)
second_session = client.session
second_session.create()
client.get(site.root_page.url)
assert session.session_key != second_session.session_key
assert segment.sessions.count() == 1
assert session.session_key in segment.sessions.values_list('session_key', flat=True)
assert second_session.session_key not in segment.sessions.values_list('session_key', flat=True)
@pytest.mark.django_db
def test_sessions_not_added_to_static_segment_if_rule_not_static():
segment = SegmentFactory(type=Segment.TYPE_STATIC)
TimeRule.objects.create(
start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0),
segment=segment)
assert not segment.sessions.all()