Handles both Python 2 & 3, and multiple optimisations & simplifications.
This commit is contained in:
@ -4,9 +4,9 @@ from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import AbstractBaseRule
|
||||
from wagtail_personalisation.utils import create_segment_dictionary
|
||||
from .models import Segment
|
||||
from .rules import AbstractBaseRule
|
||||
from .utils import create_segment_dictionary
|
||||
|
||||
|
||||
class BaseSegmentsAdapter(object):
|
||||
@ -23,28 +23,22 @@ class BaseSegmentsAdapter(object):
|
||||
|
||||
def setup(self):
|
||||
"""Prepare the adapter for segment storage."""
|
||||
return None
|
||||
|
||||
def get_segments(self):
|
||||
"""Return the segments stored in the adapter storage."""
|
||||
return None
|
||||
|
||||
def get_segment_by_id(self):
|
||||
"""Return a single segment stored in the adapter storage."""
|
||||
return None
|
||||
|
||||
def add(self):
|
||||
"""Add a new segment to the adapter storage."""
|
||||
return None
|
||||
|
||||
def refresh(self):
|
||||
"""Refresh the segments stored in the adapter storage."""
|
||||
return None
|
||||
|
||||
def _test_rules(self, rules, request, match_any=False):
|
||||
"""Tests the provided rules to see if the request still belongs
|
||||
to a segment.
|
||||
|
||||
:param rules: The rules to test for
|
||||
:type rules: list of wagtail_personalisation.rules
|
||||
:param request: The http request
|
||||
@ -53,20 +47,12 @@ class BaseSegmentsAdapter(object):
|
||||
:type match_any: bool
|
||||
:returns: A boolean indicating the segment matches the request
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if len(rules) > 0:
|
||||
for rule in rules:
|
||||
result = rule.test_user(request)
|
||||
if not rules:
|
||||
return False
|
||||
if match_any:
|
||||
if result is True:
|
||||
return result
|
||||
|
||||
elif result is False:
|
||||
return False
|
||||
if not match_any:
|
||||
return True
|
||||
return False
|
||||
return any(rule.test_user(request) for rule in rules)
|
||||
return all(rule.test_user(request) for rule in rules)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
@ -134,8 +120,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
:rtype: wagtail_personalisation.models.Segment or None
|
||||
|
||||
"""
|
||||
segments = self.get_segments()
|
||||
return next((s for s in segments if s.pk == segment_id), None)
|
||||
for segment in self.get_segments():
|
||||
if segment.pk == segment_id:
|
||||
return segment
|
||||
|
||||
def add_page_visit(self, page):
|
||||
"""Mark the page as visited by the user"""
|
||||
@ -179,20 +166,20 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
still apply to the requesting visitor.
|
||||
|
||||
"""
|
||||
all_segments = Segment.objects.filter(status=Segment.STATUS_ENABLED)
|
||||
enabled_segments = Segment.objects.filter(status=Segment.STATUS_ENABLED)
|
||||
rule_models = AbstractBaseRule.get_descendant_models()
|
||||
|
||||
current_segments = self.get_segments()
|
||||
rules = AbstractBaseRule.__subclasses__()
|
||||
|
||||
# Run tests on all remaining enabled segments to verify applicability.
|
||||
additional_segments = []
|
||||
for segment in all_segments:
|
||||
for segment in enabled_segments:
|
||||
segment_rules = []
|
||||
for rule in rules:
|
||||
segment_rules += rule.objects.filter(segment=segment)
|
||||
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)
|
||||
result = self._test_rules(segment_rules, self.request,
|
||||
match_any=segment.match_any)
|
||||
|
||||
if result:
|
||||
additional_segments.append(segment)
|
||||
|
@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from wagtail_personalisation import models, rules
|
||||
from . import models, rules
|
||||
|
||||
|
||||
class UserIsLoggedInRuleAdminInline(admin.TabularInline):
|
||||
|
@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from wagtail_personalisation import views
|
||||
from . import views
|
||||
|
||||
app_name = 'segment'
|
||||
|
||||
|
@ -3,13 +3,13 @@ from __future__ import absolute_import, unicode_literals
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailcore import blocks
|
||||
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import Segment
|
||||
from .adapters import get_segment_adapter
|
||||
from .models import Segment
|
||||
|
||||
|
||||
def list_segment_choices():
|
||||
for segment in Segment.objects.all():
|
||||
yield (segment.pk, segment.name)
|
||||
for pk, name in Segment.objects.values_list('pk', 'name'):
|
||||
yield pk, name
|
||||
|
||||
|
||||
class PersonalisedStructBlock(blocks.StructBlock):
|
||||
|
@ -11,9 +11,9 @@ from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel, ObjectList,
|
||||
PageChooserPanel, TabbedInterface)
|
||||
|
||||
from wagtail_personalisation.forms import AdminPersonalisablePageForm
|
||||
from wagtail_personalisation.rules import AbstractBaseRule
|
||||
from wagtail_personalisation.utils import count_active_days
|
||||
from .forms import AdminPersonalisablePageForm
|
||||
from .rules import AbstractBaseRule
|
||||
from .utils import count_active_days
|
||||
|
||||
|
||||
class SegmentQuerySet(models.QuerySet):
|
||||
@ -61,9 +61,9 @@ class Segment(ClusterableModel):
|
||||
], heading="Segment"),
|
||||
MultiFieldPanel([
|
||||
InlinePanel(
|
||||
"{}_related".format(rule._meta.db_table),
|
||||
label=rule.__str__,
|
||||
) for rule in AbstractBaseRule.__subclasses__()
|
||||
"{}_related".format(rule_model._meta.db_table),
|
||||
label=rule_model._meta.verbose_name,
|
||||
) for rule_model in AbstractBaseRule.__subclasses__()
|
||||
], heading="Rules"),
|
||||
]
|
||||
|
||||
@ -82,10 +82,10 @@ class Segment(ClusterableModel):
|
||||
|
||||
def get_rules(self):
|
||||
"""Retrieve all rules in the segment."""
|
||||
rules = AbstractBaseRule.__subclasses__()
|
||||
segment_rules = []
|
||||
for rule in rules:
|
||||
segment_rules += rule.objects.filter(segment=self)
|
||||
for rule_model in AbstractBaseRule.get_descendant_models():
|
||||
segment_rules.extend(
|
||||
rule_model._default_manager.filter(segment=self))
|
||||
return segment_rules
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ class PersonalisablePageMixin(models.Model):
|
||||
blank=True, null=True
|
||||
)
|
||||
segment = models.ForeignKey(
|
||||
Segment, related_name='segments', on_delete=models.PROTECT,
|
||||
Segment, related_name='pages', on_delete=models.PROTECT,
|
||||
blank=True, null=True
|
||||
)
|
||||
is_segmented = models.BooleanField(default=False)
|
||||
@ -117,9 +117,6 @@ class PersonalisablePageMixin(models.Model):
|
||||
|
||||
base_form_class = AdminPersonalisablePageForm
|
||||
|
||||
def __str__(self):
|
||||
return "{}".format(self.title)
|
||||
|
||||
@cached_property
|
||||
def has_variations(self):
|
||||
"""Return a boolean indicating whether or not the personalisable page
|
||||
@ -137,12 +134,20 @@ class PersonalisablePageMixin(models.Model):
|
||||
"""Return a boolean indicating whether or not the personalisable page
|
||||
is a canonical page.
|
||||
|
||||
:returns: A boolean indicating whether or not the personalisable page
|
||||
:returns: A boolean indicating whether or not the personalisable
|
||||
page
|
||||
is a canonical page.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
return not self.canonical_page and self.has_variations
|
||||
return self.canonical_page_id is None
|
||||
|
||||
def get_unused_segments(self):
|
||||
if not hasattr(self, '_unused_segments'):
|
||||
self._unused_segments = (
|
||||
Segment.objects.exclude(pages__canonical_page=self)
|
||||
if self.is_canonical else Segment.objects.none())
|
||||
return self._unused_segments
|
||||
|
||||
def copy_for_segment(self, segment):
|
||||
slug = "{}-{}".format(self.slug, segment.encoded_name())
|
||||
|
@ -3,9 +3,10 @@ from __future__ import absolute_import, unicode_literals
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.encoding import python_2_unicode_compatible, force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from modelcluster.fields import ParentalKey
|
||||
from user_agents import parse
|
||||
@ -26,7 +27,7 @@ class AbstractBaseRule(models.Model):
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return _('Abstract segmentation rule')
|
||||
return force_text(self._meta.verbose_name)
|
||||
|
||||
def test_user(self):
|
||||
"""Test if the user matches this rule."""
|
||||
@ -34,7 +35,7 @@ class AbstractBaseRule(models.Model):
|
||||
|
||||
def encoded_name(self):
|
||||
"""Return a string with a slug for the rule."""
|
||||
return slugify(self.__str__().lower())
|
||||
return slugify(force_text(self).lower())
|
||||
|
||||
def description(self):
|
||||
"""Return a description explaining the functionality of the rule.
|
||||
@ -51,8 +52,16 @@ class AbstractBaseRule(models.Model):
|
||||
|
||||
return description
|
||||
|
||||
@classmethod
|
||||
def get_descendant_models(cls):
|
||||
return [model for model in apps.get_models()
|
||||
if issubclass(model, AbstractBaseRule)]
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = 'Abstract segmentation rule'
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class TimeRule(AbstractBaseRule):
|
||||
"""Time rule to segment users based on a start and end time.
|
||||
|
||||
@ -70,18 +79,14 @@ class TimeRule(AbstractBaseRule):
|
||||
]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return _('Time Rule')
|
||||
class Meta:
|
||||
verbose_name = _('Time Rule')
|
||||
|
||||
def test_user(self, request=None):
|
||||
current_time = datetime.now().time()
|
||||
starting_time = self.start_time
|
||||
ending_time = self.end_time
|
||||
|
||||
return starting_time <= current_time <= ending_time
|
||||
return self.start_time <= datetime.now().time() <= self.end_time
|
||||
|
||||
def description(self):
|
||||
description = {
|
||||
return {
|
||||
'title': _('These users visit between'),
|
||||
'value': _('{} and {}').format(
|
||||
self.start_time.strftime("%H:%M"),
|
||||
@ -89,10 +94,7 @@ class TimeRule(AbstractBaseRule):
|
||||
),
|
||||
}
|
||||
|
||||
return description
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DayRule(AbstractBaseRule):
|
||||
"""Day rule to segment users based on the day(s) of a visit.
|
||||
|
||||
@ -118,39 +120,28 @@ class DayRule(AbstractBaseRule):
|
||||
FieldPanel('sun'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return _('Day Rule')
|
||||
class Meta:
|
||||
verbose_name = _('Day Rule')
|
||||
|
||||
def test_user(self, request=None):
|
||||
current_day = datetime.today().weekday()
|
||||
|
||||
days = [self.mon, self.tue, self.wed, self.thu,
|
||||
self.fri, self.sat, self.sun]
|
||||
|
||||
return days[current_day]
|
||||
return [self.mon, self.tue, self.wed, self.thu,
|
||||
self.fri, self.sat, self.sun][datetime.today().weekday()]
|
||||
|
||||
def description(self):
|
||||
days = {
|
||||
'mon': self.mon, 'tue': self.tue, 'wed': self.wed,
|
||||
'thu': self.thu, 'fri': self.fri, 'sat': self.sat,
|
||||
'sun': self.sun
|
||||
}
|
||||
days = (
|
||||
('mon', self.mon), ('tue', self.tue), ('wed', self.wed),
|
||||
('thu', self.thu), ('fri', self.fri), ('sat', self.sat),
|
||||
('sun', self.sun),
|
||||
)
|
||||
|
||||
chosen_days = []
|
||||
chosen_days = [day_name for day_name, chosen in days if chosen]
|
||||
|
||||
for key, value in days.items():
|
||||
if days[key]:
|
||||
chosen_days.append(key)
|
||||
|
||||
description = {
|
||||
return {
|
||||
'title': _('These users visit on'),
|
||||
'value': (', '.join(_(day) for day in chosen_days)).title()
|
||||
'value': ", ".join([day for day in chosen_days]).title(),
|
||||
}
|
||||
|
||||
return description
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ReferralRule(AbstractBaseRule):
|
||||
"""Referral rule to segment users based on a regex test.
|
||||
|
||||
@ -165,8 +156,8 @@ class ReferralRule(AbstractBaseRule):
|
||||
FieldPanel('regex_string'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return _('Referral Rule')
|
||||
class Meta:
|
||||
verbose_name = _('Referral Rule')
|
||||
|
||||
def test_user(self, request):
|
||||
pattern = re.compile(self.regex_string)
|
||||
@ -178,18 +169,13 @@ class ReferralRule(AbstractBaseRule):
|
||||
return False
|
||||
|
||||
def description(self):
|
||||
description = {
|
||||
return {
|
||||
'title': _('These visits originate from'),
|
||||
'value': _('{}').format(
|
||||
self.regex_string
|
||||
),
|
||||
'value': self.regex_string,
|
||||
'code': True
|
||||
}
|
||||
|
||||
return description
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class VisitCountRule(AbstractBaseRule):
|
||||
"""Visit count rule to segment users based on amount of visits to a
|
||||
specified page.
|
||||
@ -222,6 +208,9 @@ class VisitCountRule(AbstractBaseRule):
|
||||
]),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Visit count Rule')
|
||||
|
||||
def test_user(self, request):
|
||||
operator = self.operator
|
||||
segment_count = self.count
|
||||
@ -243,11 +232,8 @@ class VisitCountRule(AbstractBaseRule):
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return _('Visit count Rule')
|
||||
|
||||
def description(self):
|
||||
description = {
|
||||
return {
|
||||
'title': _('These users visited {}').format(
|
||||
self.counted_page
|
||||
),
|
||||
@ -257,10 +243,7 @@ class VisitCountRule(AbstractBaseRule):
|
||||
),
|
||||
}
|
||||
|
||||
return description
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class QueryRule(AbstractBaseRule):
|
||||
"""Query rule to segment users based on matching queries.
|
||||
|
||||
@ -278,18 +261,14 @@ class QueryRule(AbstractBaseRule):
|
||||
FieldPanel('value'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return _('Query Rule')
|
||||
class Meta:
|
||||
verbose_name = _('Query Rule')
|
||||
|
||||
def test_user(self, request):
|
||||
parameter = self.parameter
|
||||
value = self.value
|
||||
|
||||
req_value = request.GET.get(parameter, '')
|
||||
return req_value == value
|
||||
return request.GET.get(self.parameter, '') == self.value
|
||||
|
||||
def description(self):
|
||||
description = {
|
||||
return {
|
||||
'title': _('These users used a url with the query'),
|
||||
'value': _('?{}={}').format(
|
||||
self.parameter,
|
||||
@ -298,10 +277,7 @@ class QueryRule(AbstractBaseRule):
|
||||
'code': True
|
||||
}
|
||||
|
||||
return description
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class DeviceRule(AbstractBaseRule):
|
||||
"""Device rule to segment users based on matching devices.
|
||||
|
||||
@ -319,8 +295,8 @@ class DeviceRule(AbstractBaseRule):
|
||||
FieldPanel('desktop'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return _('Device Rule')
|
||||
class Meta:
|
||||
verbose_name = _('Device Rule')
|
||||
|
||||
def test_user(self, request=None):
|
||||
ua_header = request.META['HTTP_USER_AGENT']
|
||||
@ -328,15 +304,13 @@ class DeviceRule(AbstractBaseRule):
|
||||
|
||||
if user_agent.is_mobile:
|
||||
return self.mobile
|
||||
elif user_agent.is_tablet:
|
||||
if user_agent.is_tablet:
|
||||
return self.tablet
|
||||
elif user_agent.is_pc:
|
||||
if user_agent.is_pc:
|
||||
return self.desktop
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class UserIsLoggedInRule(AbstractBaseRule):
|
||||
"""User is logged in rule to segment users based on their authentication
|
||||
status.
|
||||
@ -350,22 +324,14 @@ class UserIsLoggedInRule(AbstractBaseRule):
|
||||
FieldPanel('is_logged_in'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return _('Logged In Rule')
|
||||
class Meta:
|
||||
verbose_name = _('Logged In Rule')
|
||||
|
||||
def test_user(self, request=None):
|
||||
return request.user.is_authenticated() == self.is_logged_in
|
||||
|
||||
def description(self):
|
||||
status = _('Logged in')
|
||||
if self.is_logged_in is False:
|
||||
status = _('Not logged in')
|
||||
|
||||
description = {
|
||||
return {
|
||||
'title': _('These visitors are'),
|
||||
'value': _('{}').format(
|
||||
status
|
||||
),
|
||||
'value': _('Logged in') if self.is_logged_in else _('Not logged in'),
|
||||
}
|
||||
|
||||
return description
|
||||
|
@ -3,18 +3,17 @@ from __future__ import absolute_import, unicode_literals
|
||||
import logging
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.shortcuts import reverse
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailadmin.site_summary import SummaryItem
|
||||
from wagtail.wagtailadmin.widgets import Button, ButtonWithDropdownFromHook
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page
|
||||
|
||||
from wagtail_personalisation import admin_urls
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import PersonalisablePageMixin, Segment
|
||||
from wagtail_personalisation.utils import impersonate_other_page
|
||||
from . import admin_urls
|
||||
from .adapters import get_segment_adapter
|
||||
from .models import PersonalisablePageMixin, Segment
|
||||
from .utils import impersonate_other_page
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -72,11 +71,10 @@ def serve_variation(page, request, serve_args, serve_kwargs):
|
||||
:rtype: wagtail.wagtailcore.models.Page
|
||||
|
||||
"""
|
||||
user_segments = []
|
||||
adapter = get_segment_adapter(request)
|
||||
user_segments = adapter.get_segments()
|
||||
|
||||
if len(user_segments) > 0:
|
||||
if user_segments:
|
||||
variations = _check_for_variations(user_segments, page)
|
||||
|
||||
if variations:
|
||||
@ -109,14 +107,7 @@ def page_listing_variant_buttons(page, page_perms, is_parent=False):
|
||||
the page (if any) and a 'Create a new variant' button.
|
||||
|
||||
"""
|
||||
|
||||
if not hasattr(page, 'segment'):
|
||||
return
|
||||
pages = page.__class__.objects.filter(pk=page.pk)
|
||||
segments = Segment.objects.all()
|
||||
|
||||
if pages and len(segments) > 0 and not (
|
||||
any(item.segment for item in pages)):
|
||||
if isinstance(page, PersonalisablePageMixin) and page.get_unused_segments():
|
||||
yield ButtonWithDropdownFromHook(
|
||||
_('Variants'),
|
||||
hook_name='register_page_listing_variant_buttons',
|
||||
@ -133,16 +124,9 @@ def page_listing_more_buttons(page, page_perms, is_parent=False):
|
||||
create a new variant for the selected segment.
|
||||
|
||||
"""
|
||||
model = page.__class__
|
||||
segments = Segment.objects.all()
|
||||
available_segments = [
|
||||
item for item in segments
|
||||
if not model.objects.filter(segment=item, pk=page.pk)
|
||||
]
|
||||
|
||||
for segment in available_segments:
|
||||
for segment in page.get_unused_segments():
|
||||
yield Button(segment.name,
|
||||
reverse('segment:copy_page', args=[page.id, segment.id]),
|
||||
reverse('segment:copy_page', args=[page.pk, segment.pk]),
|
||||
attrs={"title": _('Create this variant')})
|
||||
|
||||
|
||||
@ -166,7 +150,8 @@ class SegmentSummaryPanel(SummaryItem):
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
def add_segment_summary_panel(request, items):
|
||||
"""Adds a summary panel to the Wagtail dashboard showing the total amount
|
||||
of segments on the site and allowing quick access to the Segment dashboard.
|
||||
of segments on the site and allowing quick access to the Segment
|
||||
dashboard.
|
||||
|
||||
"""
|
||||
return items.append(SegmentSummaryPanel(request))
|
||||
items.append(SegmentSummaryPanel(request))
|
||||
|
Reference in New Issue
Block a user