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.db.models import F
from django.utils.module_loading import import_string
from django.utils import timezone
from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import AbstractBaseRule
@@ -174,6 +175,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
# Run tests on all remaining enabled segments to verify applicability.
additional_segments = []
for segment in enabled_segments:
if segment.is_static and self.request.session in segment.sessions.all():
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))
@@ -181,6 +185,13 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
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)

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 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.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.functional import cached_property
from django.utils.lru_cache import lru_cache
from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel
from wagtail.wagtailadmin.edit_handlers import (
@@ -14,11 +24,23 @@ from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import count_active_days
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
class SegmentQuerySet(models.QuerySet):
def enabled(self):
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
class Segment(ClusterableModel):
"""The segment model."""
@@ -30,6 +52,14 @@ class Segment(ClusterableModel):
(STATUS_DISABLED, _('Disabled')),
)
TYPE_DYNAMIC = 'dynamic'
TYPE_STATIC = 'static'
TYPE_CHOICES = (
(TYPE_DYNAMIC, _('Dynamic')),
(TYPE_STATIC, _('Static')),
)
name = models.CharField(max_length=255)
create_date = models.DateTimeField(auto_now_add=True)
edit_date = models.DateTimeField(auto_now=True)
@@ -44,6 +74,24 @@ class Segment(ClusterableModel):
default=False,
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()
@@ -56,6 +104,8 @@ class Segment(ClusterableModel):
FieldPanel('persistent'),
]),
FieldPanel('match_any'),
FieldPanel('type', widget=forms.RadioSelect),
FieldPanel('count'),
], heading="Segment"),
MultiFieldPanel([
InlinePanel(
@@ -70,6 +120,14 @@ class Segment(ClusterableModel):
def __str__(self):
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):
"""Return a string with a slug for the segment."""
return slugify(self.name.lower())
@@ -106,6 +164,21 @@ class Segment(ClusterableModel):
if 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):
"""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):
"""Base for creating rules to segment users with."""
icon = 'fa-circle-o'
static = False
segment = ParentalKey(
'wagtail_personalisation.Segment',
@@ -190,6 +191,7 @@ class VisitCountRule(AbstractBaseRule):
"""
icon = 'fa-calculator'
static = True
OPERATOR_CHOICES = (
('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()