Compare commits
80 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc1dd337bb | |||
a677846ff7 | |||
7d7861b862 | |||
8e854d0abe | |||
0051061d96 | |||
f898dfe017 | |||
8ced5bd81c | |||
9a86b0c8cc | |||
9408f90789 | |||
ba6056e3f8 | |||
fdc0a7f2e1 | |||
12b0cd9231 | |||
330557be8d | |||
aa917dee9c | |||
364cb1a7e6 | |||
bedbe06c65 | |||
362f15e5ff | |||
8a0dba2efb | |||
59f4877e04 | |||
2ff29cc375 | |||
8527e6ff23 | |||
d7c07cb238 | |||
6e83366df6 | |||
55364f8906 | |||
4fd0b30c66 | |||
c909852b08 | |||
ea1ecc2a98 | |||
0f0aecf673 | |||
c11960f921 | |||
37d49dcdfb | |||
869237360d | |||
33277a0b20 | |||
2cd643fb2d | |||
0f18024ebc | |||
521222f748 | |||
56a8e106d8 | |||
3162191a16 | |||
8c7e99313b | |||
824e42174f | |||
d114bb2570 | |||
7bba1e57cc | |||
3017f32b6b | |||
6b1a7cf1f2 | |||
1525b7946c | |||
7bf1bc3f19 | |||
4c60bcbe6b | |||
ad4f75d471 | |||
086168954d | |||
881090f2f9 | |||
d073c7d268 | |||
7200b5b4c4 | |||
6f97c76958 | |||
ecb4f928fb | |||
29aa91477e | |||
5c3acc6661 | |||
602919d2d4 | |||
ae97118c3f | |||
51774b939e | |||
908f85e295 | |||
99f9700ed0 | |||
7fa8ee1a46 | |||
5ad70d68f6 | |||
06bfe77901 | |||
d5e89d374b | |||
5b39e82f80 | |||
fbcebb43a4 | |||
ef271587ec | |||
786a8801b1 | |||
caf73aa43c | |||
4021d2c915 | |||
33f96af4a3 | |||
6299feb497 | |||
7ced6db126 | |||
c6ce67c9c9 | |||
3df3fc0b16 | |||
a00929846e | |||
49fba11049 | |||
e3488e87ad | |||
808aa6d202 | |||
efb060cc6e |
@ -4,6 +4,9 @@ language: python
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
env: lint
|
||||
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-django111-wagtail113
|
||||
|
||||
@ -25,4 +28,4 @@ deploy:
|
||||
secure: IxPnc95OFNQsl7kFfLlLc38KfHh/W79VXnhEkdb2x1GZznOsG167QlhpAnyXIJysCQxgMMwLMuPOOdk1WIxOpGNM1/M80hNzPAfxMYWPuwposDdwoIc8SyREPJt16BXWAY+rAH8SHxld9p1YdRbOEPSSglODe4dCmQWsCbKjV3aKv+gZxBvf6pMxUglp2fBIlCwrE77MyMYh9iW0AGzD6atC2xN9NgAm4f+2WFlKCUL/MztLhNWdvWEiibQav6Tts7p8tWrsBVzywDW2IOy3O0ihPgRdISZ7QrxhiJTjlHYPAy4eRGOnYSQXvp6Dy8ONE5a6Uv5g3Xw2UmQo85sSMUs2VxR0G7d+PQgL0A7ENBQ5i7eSAFHYs8EswiGilW2A7mM4qRXwg9URLelYSdkM+aNXvR+25dCsXakiO4NjCz/BPgNzxPeQLlBdxR5vHugeM/XYuhy6CHlZrR/uueaO0X8RyhJcqeDjBy58IrwYS3Mpj7QCfBpQ9PqsqXEWV9BKwKiBXM2+3hkhawFDWa0GW2PDbORKtSLy/ORfGLx5Y9qxQYKEGvFQA3iqkTjrLj+KeUziKtuvEAcvsdBIJVIxeoHwdl+qqxEm8A7YuRBnWVxWc3jE6wBXboeFP92gVe/ueoXmY22riK9Ja0pli3TyNga8by9WM8Qp4D2ZqkVXHwg=
|
||||
on:
|
||||
tags: true
|
||||
all_branches: true
|
||||
condition: $TOXENV = py27-django111-wagtail113
|
||||
|
31
CHANGES
31
CHANGES
@ -1,3 +1,34 @@
|
||||
0.11.0
|
||||
==================
|
||||
- Bug Fix: Query rule should not be static
|
||||
- Enable retrieval of user data for static rules through csv download
|
||||
|
||||
0.10.9
|
||||
==================
|
||||
- Bug Fix: Display the number of users in a static segment on dashboard
|
||||
|
||||
0.10.8
|
||||
==================
|
||||
- Don't add users to exclude list for dynamic segments
|
||||
- Store segments a user is excluded from in the session
|
||||
|
||||
0.10.7
|
||||
==================
|
||||
- Bug Fix: Ensure static segment members are show the survey immediately
|
||||
- Records users excluded by randomisation on the segment
|
||||
- Don't re-check excluded users
|
||||
|
||||
0.10.6
|
||||
==================
|
||||
- Accepts and stores randomisation percentage for segment
|
||||
- Adds users to segment based on random number relative to percentage
|
||||
|
||||
0.10.5
|
||||
==================
|
||||
- Count how many users match a segments rules before saving the segment
|
||||
- Stores count on the segment and displays in the dashboard
|
||||
- Enables testing users against rules if there isn't an active request
|
||||
|
||||
0.10.0
|
||||
==================
|
||||
- Adds static and dynamic segments
|
||||
|
@ -55,10 +55,10 @@ author = 'Lab Digital BV'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.10.3'
|
||||
version = '0.11.0'
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.10.3'
|
||||
release = '0.11.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.10.3
|
||||
current_version = 0.11.0
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = {new_version}
|
||||
|
2
setup.py
2
setup.py
@ -32,7 +32,7 @@ with open('README.rst') as fh:
|
||||
|
||||
setup(
|
||||
name='wagtail-personalisation-molo',
|
||||
version='0.10.3',
|
||||
version='0.11.0',
|
||||
description='A forked version of Wagtail add-on for showing personalized content',
|
||||
author='Praekelt.org',
|
||||
author_email='dev@praekeltfoundation.org',
|
||||
|
@ -3,7 +3,6 @@ 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
|
||||
@ -67,17 +66,21 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
self.request.session.setdefault('segments', [])
|
||||
self._segment_cache = None
|
||||
|
||||
def get_segments(self):
|
||||
def get_segments(self, key="segments"):
|
||||
"""Return the persistent segments stored in the request session.
|
||||
|
||||
:param key: The key under which the segments are stored
|
||||
:type key: String
|
||||
:returns: The segments in the request session
|
||||
:rtype: list of wagtail_personalisation.models.Segment or empty list
|
||||
|
||||
"""
|
||||
if self._segment_cache is not None:
|
||||
if key == "segments" and self._segment_cache is not None:
|
||||
return self._segment_cache
|
||||
|
||||
raw_segments = self.request.session['segments']
|
||||
if key not in self.request.session:
|
||||
return []
|
||||
raw_segments = self.request.session[key]
|
||||
segment_ids = [segment['id'] for segment in raw_segments]
|
||||
|
||||
segments = (
|
||||
@ -87,14 +90,17 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
.in_bulk(segment_ids))
|
||||
|
||||
retval = [segments[pk] for pk in segment_ids if pk in segments]
|
||||
self._segment_cache = retval
|
||||
if key == "segments":
|
||||
self._segment_cache = retval
|
||||
return retval
|
||||
|
||||
def set_segments(self, segments):
|
||||
def set_segments(self, segments, key="segments"):
|
||||
"""Set the currently active segments
|
||||
|
||||
:param segments: The segments to set for the current request
|
||||
:type segments: list of wagtail_personalisation.models.Segment
|
||||
:param key: The key under which to store the segments. Optional
|
||||
:type key: String
|
||||
|
||||
"""
|
||||
cache_segments = []
|
||||
@ -109,8 +115,9 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
serialized_segments.append(serialized)
|
||||
segment_ids.add(segment.pk)
|
||||
|
||||
self.request.session['segments'] = serialized_segments
|
||||
self._segment_cache = cache_segments
|
||||
self.request.session[key] = serialized_segments
|
||||
if key == "segments":
|
||||
self._segment_cache = cache_segments
|
||||
|
||||
def get_segment_by_id(self, segment_id):
|
||||
"""Find and return a single segment from the request session.
|
||||
@ -172,12 +179,16 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
rule_models = AbstractBaseRule.get_descendant_models()
|
||||
|
||||
current_segments = self.get_segments()
|
||||
excluded_segments = self.get_segments("excluded_segments")
|
||||
|
||||
# Run tests on all remaining enabled segments to verify applicability.
|
||||
additional_segments = []
|
||||
for segment in enabled_segments:
|
||||
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
|
||||
additional_segments.append(segment)
|
||||
elif (segment.excluded_users.filter(id=self.request.user.id).exists() or
|
||||
segment in excluded_segments):
|
||||
continue
|
||||
elif not segment.is_static or not segment.is_full:
|
||||
segment_rules = []
|
||||
for rule_model in rule_models:
|
||||
@ -186,14 +197,19 @@ 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:
|
||||
if self.request.user.is_authenticated():
|
||||
segment.static_users.add(self.request.user)
|
||||
|
||||
if result:
|
||||
if result and segment.randomise_into_segment():
|
||||
if segment.is_static and not segment.is_full:
|
||||
if self.request.user.is_authenticated():
|
||||
segment.static_users.add(self.request.user)
|
||||
additional_segments.append(segment)
|
||||
elif result:
|
||||
if segment.is_static and self.request.user.is_authenticated():
|
||||
segment.excluded_users.add(self.request.user)
|
||||
else:
|
||||
excluded_segments += [segment]
|
||||
|
||||
self.set_segments(current_segments + additional_segments)
|
||||
self.set_segments(excluded_segments, "excluded_segments")
|
||||
self.update_visit_count()
|
||||
|
||||
|
||||
|
@ -13,4 +13,6 @@ urlpatterns = [
|
||||
views.copy_page_view, name='copy_page'),
|
||||
url(r'^segment/toggle_segment_view/$',
|
||||
views.toggle_segment_view, name='toggle_segment_view'),
|
||||
url(r'^segment/users/(?P<segment_id>[0-9]+)$',
|
||||
views.segment_user_data, name='segment_user_data'),
|
||||
]
|
||||
|
@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
from itertools import takewhile
|
||||
|
||||
@ -8,15 +9,11 @@ from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils import timezone
|
||||
from django.utils.lru_cache import lru_cache
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailadmin.forms import WagtailAdminModelForm
|
||||
|
||||
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@ -29,8 +26,30 @@ def user_from_data(user_id):
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
|
||||
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.filter(is_active=True, is_staff=False)
|
||||
|
||||
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
|
||||
@ -62,13 +81,22 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
if field not in excluded:
|
||||
form.add_error(field, _('Cannot update a static segment'))
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
is_new = not self.instance.id
|
||||
|
||||
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:
|
||||
@ -79,6 +107,7 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
users_to_add = []
|
||||
users_to_exclude = []
|
||||
sessions = Session.objects.iterator()
|
||||
take_session = takewhile(
|
||||
lambda x: instance.count == 0 or len(users_to_add) <= instance.count,
|
||||
@ -91,10 +120,13 @@ class SegmentAdminForm(WagtailAdminModelForm):
|
||||
request.user = user
|
||||
request.session = SessionStore(session_key=session.session_key)
|
||||
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
|
||||
if passes:
|
||||
if passes and instance.randomise_into_segment():
|
||||
users_to_add.append(user)
|
||||
elif passes:
|
||||
users_to_exclude.append(user)
|
||||
|
||||
instance.static_users.add(*users_to_add)
|
||||
instance.excluded_users.add(*users_to_exclude)
|
||||
|
||||
return instance
|
||||
|
||||
|
@ -2,8 +2,8 @@
|
||||
# Generated by Django 1.11.1 on 2017-05-31 14:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.10.8 on 2018-01-31 16:12
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wagtail_personalisation', '0016_auto_20180125_0918'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='randomisation_percent',
|
||||
field=models.PositiveSmallIntegerField(blank=True, default=None, help_text='If this number is set each user matching the rules will have this percentage chance of being placed in the segment.', null=True, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]),
|
||||
),
|
||||
]
|
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.9 on 2018-02-09 08:28
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('wagtail_personalisation', '0017_segment_randomisation_percent'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='segment',
|
||||
name='excluded_users',
|
||||
field=models.ManyToManyField(help_text='Users that matched the rules but were excluded from the segment for some reason e.g. randomisation', related_name='excluded_segments', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
@ -1,8 +1,9 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import random
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models, transaction
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
@ -11,11 +12,7 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from modelcluster.models import ClusterableModel
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel,
|
||||
FieldRowPanel,
|
||||
InlinePanel,
|
||||
MultiFieldPanel,
|
||||
)
|
||||
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
|
||||
from wagtail.wagtailcore.models import Page
|
||||
|
||||
from wagtail_personalisation.rules import AbstractBaseRule
|
||||
@ -86,6 +83,25 @@ class Segment(ClusterableModel):
|
||||
static_users = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
)
|
||||
excluded_users = models.ManyToManyField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
help_text=_("Users that matched the rules but were excluded from the "
|
||||
"segment for some reason e.g. randomisation"),
|
||||
related_name="excluded_segments"
|
||||
)
|
||||
|
||||
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
|
||||
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
|
||||
|
||||
randomisation_percent = models.PositiveSmallIntegerField(
|
||||
null=True, blank=True, default=None,
|
||||
help_text=_(
|
||||
"If this number is set each user matching the rules will "
|
||||
"have this percentage chance of being placed in the segment."
|
||||
), validators=[
|
||||
MaxValueValidator(100),
|
||||
MinValueValidator(0)
|
||||
])
|
||||
|
||||
objects = SegmentQuerySet.as_manager()
|
||||
|
||||
@ -102,6 +118,7 @@ class Segment(ClusterableModel):
|
||||
FieldPanel('match_any'),
|
||||
FieldPanel('type', widget=forms.RadioSelect),
|
||||
FieldPanel('count', classname='count_field'),
|
||||
FieldPanel('randomisation_percent', classname='percent_field'),
|
||||
], heading="Segment"),
|
||||
MultiFieldPanel([
|
||||
InlinePanel(
|
||||
@ -125,7 +142,7 @@ class Segment(ClusterableModel):
|
||||
|
||||
@classmethod
|
||||
def all_static(cls, rules):
|
||||
return all(rule.static for rule in rules)
|
||||
return all(rule.static for rule in rules)
|
||||
|
||||
@property
|
||||
def all_rules_static(self):
|
||||
@ -172,6 +189,19 @@ class Segment(ClusterableModel):
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
def randomise_into_segment(self):
|
||||
""" Returns True if randomisation_percent is not set or it generates
|
||||
a random number less than the randomisation_percent
|
||||
This is so there is some randomisation in which users are added to the
|
||||
segment
|
||||
"""
|
||||
if self.randomisation_percent is None:
|
||||
return True
|
||||
|
||||
if random.randint(1, 100) <= self.randomisation_percent:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class PersonalisablePageMetadata(ClusterableModel):
|
||||
"""The personalisable page model. Allows creation of variants with linked
|
||||
|
@ -2,17 +2,23 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.test.client import RequestFactory
|
||||
from modelcluster.fields import ParentalKey
|
||||
from user_agents import parse
|
||||
from wagtail.wagtailadmin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel, PageChooserPanel)
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class AbstractBaseRule(models.Model):
|
||||
@ -220,13 +226,37 @@ class VisitCountRule(AbstractBaseRule):
|
||||
class Meta:
|
||||
verbose_name = _('Visit count Rule')
|
||||
|
||||
def test_user(self, request):
|
||||
def _get_user_session(self, user):
|
||||
sessions = Session.objects.iterator()
|
||||
for session in sessions:
|
||||
session_data = session.get_decoded()
|
||||
if session_data.get('_auth_user_id') == str(user.id):
|
||||
return SessionStore(session_key=session.session_key)
|
||||
return SessionStore()
|
||||
|
||||
def test_user(self, request, user=None):
|
||||
# Local import for cyclic import
|
||||
from wagtail_personalisation.adapters import (
|
||||
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
|
||||
|
||||
if user:
|
||||
# Create a fake request so we can use the adapter
|
||||
request = RequestFactory().get('/')
|
||||
request.user = user
|
||||
|
||||
# If we're using the session adapter check for an active session
|
||||
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
|
||||
request.session = self._get_user_session(user)
|
||||
else:
|
||||
request.session = SessionStore()
|
||||
|
||||
elif not request:
|
||||
# Return false if we don't have a user or a request
|
||||
return False
|
||||
|
||||
operator = self.operator
|
||||
segment_count = self.count
|
||||
|
||||
# Local import for cyclic import
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
|
||||
adapter = get_segment_adapter(request)
|
||||
|
||||
visit_count = adapter.get_visit_count(self.counted_page)
|
||||
@ -252,6 +282,28 @@ class VisitCountRule(AbstractBaseRule):
|
||||
),
|
||||
}
|
||||
|
||||
def get_column_header(self):
|
||||
return "Visit count - %s" % self.counted_page
|
||||
|
||||
def get_user_info_string(self, user):
|
||||
# Local import for cyclic import
|
||||
from wagtail_personalisation.adapters import (
|
||||
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
|
||||
|
||||
# Create a fake request so we can use the adapter
|
||||
request = RequestFactory().get('/')
|
||||
request.user = user
|
||||
|
||||
# If we're using the session adapter check for an active session
|
||||
if SEGMENT_ADAPTER_CLASS == SessionSegmentsAdapter:
|
||||
request.session = self._get_user_session(user)
|
||||
else:
|
||||
request.session = SessionStore()
|
||||
|
||||
adapter = get_segment_adapter(request)
|
||||
visit_count = adapter.get_visit_count(self.counted_page)
|
||||
return str(visit_count)
|
||||
|
||||
|
||||
class QueryRule(AbstractBaseRule):
|
||||
"""Query rule to segment users based on matching queries.
|
||||
|
@ -38,11 +38,11 @@
|
||||
<li class="stat_card">
|
||||
{% trans "This segment is Static" %}
|
||||
<span class="icon icon-fa-user">
|
||||
{{ segment.sessions.count|localize }}
|
||||
{% if segment.sessions.count < segment.count %}
|
||||
{{ segment.static_users.count|localize }}
|
||||
{% if segment.static_users.count < segment.count %}
|
||||
/ {{ segment.count }} {% trans "member" %}{{ segment.count|pluralize }}
|
||||
{% else %}
|
||||
{% trans "member" %}{{ segment.sessions.count|pluralize }}
|
||||
{% trans "member" %}{{ segment.count|pluralize }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</li>
|
||||
@ -70,6 +70,13 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
|
||||
{% if segment.randomisation_percent is not None %}
|
||||
<li class="stat_card">
|
||||
<span>{{ segment.randomisation_percent }} %</span>
|
||||
{% trans "Chance that visitors matching the rules are added to the segment" %}
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for rule in segment.get_rules %}
|
||||
<li class="stat_card {{ rule.encoded_name }}">
|
||||
{{ rule.description.title }}
|
||||
@ -80,6 +87,11 @@
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if segment.matched_users_count > 0 %}
|
||||
<li class="stat_card">
|
||||
<span class="icon icon-fa-user"> {{ segment.matched_users_count }} {% trans "user" %}{{ segment.matched_users_count|pluralize }}</span> {% trans "were possible matches for this segment at creation" %}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -91,6 +103,9 @@
|
||||
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
||||
{% if segment.is_static %}
|
||||
<li><a href="{% url 'segment:segment_user_data' segment.pk %}" title="{% trans "Download user info" %}">download users csv</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -103,9 +103,17 @@ def exclude_variants(pages):
|
||||
:return: List of pages that aren't variants
|
||||
:rtype: list
|
||||
"""
|
||||
return [page for page in pages
|
||||
if (hasattr(page, 'personalisation_metadata') is False)
|
||||
or (hasattr(page, 'personalisation_metadata')
|
||||
and page.personalisation_metadata is None)
|
||||
or (hasattr(page, 'personalisation_metadata')
|
||||
and page.personalisation_metadata.is_canonical)]
|
||||
return [
|
||||
page for page in pages
|
||||
if (
|
||||
(
|
||||
hasattr(page, 'personalisation_metadata') is False
|
||||
) or
|
||||
(
|
||||
hasattr(page, 'personalisation_metadata') and page.personalisation_metadata is None
|
||||
) or
|
||||
(
|
||||
hasattr(page, 'personalisation_metadata') and page.personalisation_metadata.is_canonical
|
||||
)
|
||||
)
|
||||
]
|
||||
|
@ -1,8 +1,9 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import csv
|
||||
|
||||
from django import forms
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
|
||||
@ -139,3 +140,32 @@ def copy_page_view(request, page_id, segment_id):
|
||||
return HttpResponseRedirect(edit_url)
|
||||
|
||||
return HttpResponseForbidden()
|
||||
|
||||
|
||||
# CSV download views
|
||||
def segment_user_data(request, segment_id):
|
||||
if request.user.has_perm('wagtailadmin.access_admin'):
|
||||
segment = get_object_or_404(Segment, pk=segment_id)
|
||||
|
||||
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||
response['Content-Disposition'] = \
|
||||
'attachment;filename=segment-%s-users.csv' % str(segment_id)
|
||||
|
||||
headers = ['Username']
|
||||
for rule in segment.get_rules():
|
||||
if rule.static:
|
||||
headers.append(rule.get_column_header())
|
||||
|
||||
writer = csv.writer(response)
|
||||
writer.writerow(headers)
|
||||
|
||||
for user in segment.static_users.all():
|
||||
row = [user.username]
|
||||
for rule in segment.get_rules():
|
||||
if rule.static:
|
||||
row.append(rule.get_user_info_string(user))
|
||||
writer.writerow(row)
|
||||
|
||||
return response
|
||||
|
||||
return HttpResponseForbidden()
|
||||
|
@ -7,7 +7,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.template.defaultfilters import pluralize
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.wagtailadmin.site_summary import SummaryItem, PagesSummaryItem
|
||||
from wagtail.wagtailadmin.site_summary import PagesSummaryItem, SummaryItem
|
||||
from wagtail.wagtailadmin.widgets import Button, ButtonWithDropdownFromHook
|
||||
from wagtail.wagtailcore import hooks
|
||||
from wagtail.wagtailcore.models import Page
|
||||
|
@ -1,10 +1,9 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import os
|
||||
from pkg_resources import parse_version as V
|
||||
|
||||
import django
|
||||
|
||||
from pkg_resources import parse_version as V
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
@ -56,6 +55,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_middleware_settings():
|
||||
return (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
@ -69,6 +69,7 @@ def get_middleware_settings():
|
||||
'wagtail.wagtailcore.middleware.SiteMiddleware',
|
||||
)
|
||||
|
||||
|
||||
# Django 1.10 started to use "MIDDLEWARE" instead of "MIDDLEWARE_CLASSES".
|
||||
if V(django.get_version()) < V('1.10'):
|
||||
MIDDLEWARE_CLASSES = get_middleware_settings()
|
||||
|
@ -21,7 +21,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='ContentPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
||||
],
|
||||
|
@ -2,9 +2,9 @@
|
||||
# Generated by Django 1.11.1 on 2017-06-02 04:26
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import wagtail.wagtailcore.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@ -18,7 +18,7 @@ class Migration(migrations.Migration):
|
||||
migrations.CreateModel(
|
||||
name='RegularPage',
|
||||
fields=[
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
|
||||
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
|
||||
('subtitle', models.CharField(blank=True, default='', max_length=255)),
|
||||
('body', wagtail.wagtailcore.fields.RichTextField(blank=True, default='')),
|
||||
],
|
||||
|
@ -4,16 +4,14 @@ import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.factories.page import ContentPageFactory
|
||||
from tests.factories.rule import (
|
||||
DayRuleFactory, DeviceRuleFactory, ReferralRuleFactory, TimeRuleFactory)
|
||||
from tests.factories.rule import ReferralRuleFactory, QueryRuleFactory
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from tests.factories.site import SiteFactory
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import TimeRule
|
||||
|
||||
# Factory tests
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_segment_create():
|
||||
factoried_segment = SegmentFactory()
|
||||
@ -27,8 +25,6 @@ def test_segment_create():
|
||||
assert factoried_segment.status == segment.status
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_referral_rule_create():
|
||||
segment = SegmentFactory(name='Referral')
|
||||
@ -37,3 +33,15 @@ def test_referral_rule_create():
|
||||
segment=segment)
|
||||
|
||||
assert referral_rule.regex_string == 'test.test'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_query_rule_create():
|
||||
segment = SegmentFactory(name='Query')
|
||||
query_rule = QueryRuleFactory(
|
||||
parameter="query",
|
||||
value="value",
|
||||
segment=segment)
|
||||
|
||||
assert query_rule.parameter == 'query'
|
||||
assert query_rule.value == 'value'
|
||||
|
@ -16,6 +16,8 @@ def test_time_rule_create():
|
||||
segment=segment)
|
||||
|
||||
assert time_rule.start_time == datetime.time(8, 0, 0)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@freeze_time("10:00:00")
|
||||
def test_requesttime_segment(client, site):
|
||||
|
@ -1,5 +1,8 @@
|
||||
import pytest
|
||||
|
||||
from tests.factories.rule import VisitCountRuleFactory
|
||||
from tests.factories.segment import SegmentFactory
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_visit_count(site, client):
|
||||
@ -20,3 +23,50 @@ def test_visit_count(site, client):
|
||||
visit_count = client.session['visit_count']
|
||||
assert visit_count[0]['count'] == 2
|
||||
assert visit_count[1]['count'] == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_visit_count_call_test_user_with_user(site, client, user):
|
||||
segment = SegmentFactory(name='VisitCount')
|
||||
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||
|
||||
session = client.session
|
||||
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
|
||||
assert rule.test_user(None, user)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_visit_count_call_test_user_with_user_or_request_fails(site, client, user):
|
||||
segment = SegmentFactory(name='VisitCount')
|
||||
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||
|
||||
session = client.session
|
||||
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
|
||||
assert not rule.test_user(None)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_column_header(site):
|
||||
segment = SegmentFactory(name='VisitCount')
|
||||
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||
|
||||
assert rule.get_column_header() == 'Visit count - Test page'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_user_info_string_returns_count(site, client, user):
|
||||
segment = SegmentFactory(name='VisitCount')
|
||||
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
|
||||
|
||||
session = client.session
|
||||
session['visit_count'] = [{'path': '/', 'count': 2}]
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
|
||||
assert rule.get_user_info_string(user) == '2'
|
||||
|
@ -2,18 +2,17 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
from django.forms.models import model_to_dict
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
import pytest
|
||||
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 tests.factories.segment import SegmentFactory
|
||||
|
||||
|
||||
def form_with_data(segment, *rules):
|
||||
model_fields = ['type', 'status', 'count', 'name', 'match_any']
|
||||
model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent']
|
||||
|
||||
class TestSegmentAdminForm(SegmentAdminForm):
|
||||
class Meta:
|
||||
@ -63,6 +62,7 @@ def test_anonymous_user_not_added_to_static_segment_at_creation(site, client):
|
||||
|
||||
assert not instance.static_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_match_any_correct_populates(site, client, django_user_model):
|
||||
user = django_user_model.objects.create(username='first')
|
||||
@ -246,3 +246,338 @@ 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_randomisation_percentage_added_to_segment_at_creation(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = 80
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert instance.randomisation_percent == 80
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_randomisation_percentage_min_zero(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = -1
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_randomisation_percentage_max_100(site, client, mocker, django_user_model):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
segment.randomisation_percent = 101
|
||||
rule = VisitCountRule()
|
||||
|
||||
form = form_with_data(segment, rule)
|
||||
assert not form.is_valid()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_in_static_segment_if_random_is_below_percentage(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=39)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert instance.id == client.session['segments'][0]['id']
|
||||
assert user in instance.static_users.all()
|
||||
assert user not in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_in_static_segment_if_random_is_above_percentage(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=41)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert len(client.session['segments']) == 0
|
||||
assert user not in instance.static_users.all()
|
||||
assert user in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_offered_dynamic_segment_if_random_is_below_percentage(site, client, mocker):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=39)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert len(client.session['excluded_segments']) == 0
|
||||
assert instance.id == client.session['segments'][0]['id']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_offered_dynamic_segment_if_random_is_above_percentage(site, client, mocker):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||
randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
mocker.patch('random.randint', return_value=41)
|
||||
session = client.session
|
||||
session.save()
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert len(client.session['segments']) == 0
|
||||
assert instance.id == client.session['excluded_segments'][0]['id']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_in_segment_if_percentage_is_0(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=0)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert len(client.session['segments']) == 0
|
||||
assert user not in instance.static_users.all()
|
||||
assert user in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_always_in_segment_if_percentage_is_100(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=100)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert instance.id == client.session['segments'][0]['id']
|
||||
assert user in instance.static_users.all()
|
||||
assert user not in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_not_added_to_static_segment_at_creation_if_random_above_percent(site, client, mocker, user):
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
mocker.patch('random.randint', return_value=41)
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert user not in instance.static_users.all()
|
||||
assert user in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_added_to_static_segment_at_creation_if_random_below_percent(site, client, mocker, user):
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
mocker.patch('random.randint', return_value=39)
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, randomisation_percent=40)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
|
||||
assert user in instance.static_users.all()
|
||||
assert user not in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rules_check_skipped_if_user_in_excluded(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
|
||||
randomisation_percent=100)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
instance.excluded_users.add(user)
|
||||
instance.save
|
||||
|
||||
mock_test_rule = mocker.patch(
|
||||
'wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
|
||||
session = client.session
|
||||
session.save()
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert mock_test_rule.call_count == 0
|
||||
assert len(client.session['segments']) == 0
|
||||
assert user not in instance.static_users.all()
|
||||
assert user in instance.excluded_users.all()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_rules_check_skipped_if_dynamic_segment_in_excluded(site, client, mocker, user):
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
|
||||
randomisation_percent=100)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
instance = form.save()
|
||||
instance.persistent = True
|
||||
instance.save()
|
||||
|
||||
session = client.session
|
||||
session['excluded_segments'] = [{'id': instance.pk}]
|
||||
session.save()
|
||||
|
||||
mock_test_rule = mocker.patch(
|
||||
'wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
|
||||
|
||||
client.force_login(user)
|
||||
client.get(site.root_page.url)
|
||||
|
||||
assert mock_test_rule.call_count == 0
|
||||
assert len(client.session['segments']) == 0
|
||||
|
||||
|
||||
@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, 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(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_excludes_staff(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second', is_staff=True)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 1
|
||||
assert mock_test_user.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_excludes_inactive(site, client, mocker, django_user_model):
|
||||
django_user_model.objects.create(username='first')
|
||||
django_user_model.objects.create(username='second', is_active=False)
|
||||
|
||||
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
|
||||
rule = VisitCountRule(counted_page=site.root_page)
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 1
|
||||
assert mock_test_user.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_only_counts_static_rules(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 = TimeRule(
|
||||
start_time=datetime.time(0, 0, 0),
|
||||
end_time=datetime.time(23, 59, 59),
|
||||
segment=segment,
|
||||
)
|
||||
form = form_with_data(segment, rule)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.TimeRule.test_user')
|
||||
|
||||
assert form.count_matching_users([rule], True) is 0
|
||||
assert mock_test_user.call_count == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_handles_match_any(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)
|
||||
first_rule = VisitCountRule(counted_page=site.root_page)
|
||||
other_page = site.root_page.get_last_child()
|
||||
second_rule = VisitCountRule(counted_page=other_page)
|
||||
form = form_with_data(segment, first_rule, second_rule)
|
||||
|
||||
mock_test_user = mocker.patch(
|
||||
'wagtail_personalisation.rules.VisitCountRule.test_user',
|
||||
side_effect=[True, False, True, False])
|
||||
|
||||
assert form.count_matching_users([first_rule, second_rule], True) is 2
|
||||
mock_test_user.call_count == 4
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_count_matching_users_handles_match_all(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)
|
||||
first_rule = VisitCountRule(counted_page=site.root_page)
|
||||
other_page = site.root_page.get_last_child()
|
||||
second_rule = VisitCountRule(counted_page=other_page)
|
||||
form = form_with_data(segment, first_rule, second_rule)
|
||||
|
||||
mock_test_user = mocker.patch(
|
||||
'wagtail_personalisation.rules.VisitCountRule.test_user',
|
||||
side_effect=[True, True, False, True])
|
||||
|
||||
assert form.count_matching_users([first_rule, second_rule], False) is 1
|
||||
mock_test_user.call_count == 4
|
||||
|
@ -1,4 +1,5 @@
|
||||
from wagtail_personalisation.utils import impersonate_other_page
|
||||
from wagtail_personalisation.utils import (
|
||||
exclude_variants, impersonate_other_page)
|
||||
|
||||
|
||||
class Page(object):
|
||||
@ -19,3 +20,40 @@ def test_impersonate_other_page():
|
||||
impersonate_other_page(page, other_page)
|
||||
|
||||
assert page == other_page
|
||||
|
||||
|
||||
class Metadata(object):
|
||||
def __init__(self, is_canonical=True):
|
||||
self.is_canonical = is_canonical
|
||||
|
||||
|
||||
class PersonalisationMetadataPage(object):
|
||||
def __init__(self):
|
||||
self.personalisation_metadata = Metadata()
|
||||
|
||||
|
||||
def test_exclude_variants_includes_pages_with_no_metadata_property():
|
||||
page = PersonalisationMetadataPage()
|
||||
del page.personalisation_metadata
|
||||
result = exclude_variants([page])
|
||||
assert result == [page]
|
||||
|
||||
|
||||
def test_exclude_variants_includes_pages_with_metadata_none():
|
||||
page = PersonalisationMetadataPage()
|
||||
page.personalisation_metadata = None
|
||||
result = exclude_variants([page])
|
||||
assert result == [page]
|
||||
|
||||
|
||||
def test_exclude_variants_includes_pages_with_metadata_canonical():
|
||||
page = PersonalisationMetadataPage()
|
||||
result = exclude_variants([page])
|
||||
assert result == [page]
|
||||
|
||||
|
||||
def test_exclude_variants_excludes_pages_with_metadata_not_canonical():
|
||||
page = PersonalisationMetadataPage()
|
||||
page.personalisation_metadata.is_canonical = False
|
||||
result = exclude_variants([page])
|
||||
assert result == []
|
||||
|
55
tests/unit/test_views.py
Normal file
55
tests/unit/test_views.py
Normal file
@ -0,0 +1,55 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.core.urlresolvers import reverse
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import VisitCountRule
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_segment_user_data_view_requires_admin_access(site, client, django_user_model):
|
||||
user = django_user_model.objects.create(username='first')
|
||||
|
||||
segment = Segment(type=Segment.TYPE_STATIC, count=1)
|
||||
segment.save()
|
||||
|
||||
client.force_login(user)
|
||||
url = reverse('segment:segment_user_data', args=(segment.id,))
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 302
|
||||
assert response.url == '/admin/login/?next=%s' % url
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_segment_user_data_view(site, client, mocker, django_user_model):
|
||||
user1 = django_user_model.objects.create(username='first')
|
||||
user2 = django_user_model.objects.create(username='second')
|
||||
admin_user = django_user_model.objects.create(username='admin')
|
||||
permission = Permission.objects.get(codename='access_admin')
|
||||
admin_user.user_permissions.add(permission)
|
||||
|
||||
segment = Segment(type=Segment.TYPE_STATIC, count=1)
|
||||
segment.save()
|
||||
segment.static_users.add(user1)
|
||||
segment.static_users.add(user2)
|
||||
|
||||
rule1 = VisitCountRule(counted_page=site.root_page, segment=segment)
|
||||
rule2 = VisitCountRule(counted_page=site.root_page.get_last_child(),
|
||||
segment=segment)
|
||||
rule1.save()
|
||||
rule2.save()
|
||||
|
||||
mocker.patch('wagtail_personalisation.rules.VisitCountRule.get_user_info_string',
|
||||
side_effect=[3, 9, 0, 1])
|
||||
|
||||
client.force_login(admin_user)
|
||||
response = client.get(
|
||||
reverse('segment:segment_user_data', args=(segment.id,)))
|
||||
|
||||
assert response.status_code == 200
|
||||
data_lines = response.content.decode().split("\n")
|
||||
|
||||
assert data_lines[0] == 'Username,Visit count - Test page,Visit count - Regular page\r'
|
||||
assert data_lines[1] == 'first,3,9\r'
|
||||
assert data_lines[2] == 'second,0,1\r'
|
Reference in New Issue
Block a user