6
0
Fork 0

Merge branch 'develop' into feature/djangoconf-sprint

This commit is contained in:
Paul J Stevens 2018-05-26 10:48:33 +02:00
commit 5536adc3ec
35 changed files with 1400 additions and 106 deletions

View File

@ -4,33 +4,11 @@ language: python
matrix:
include:
# Django 1.9, Wagtail 1.9
- python: 2.7
env: TOXENV=py27-django19-wagtail19
- python: 3.5
env: TOXENV=py35-django19-wagtail19
- python: 3.6
env: TOXENV=py36-django19-wagtail19
env: lint
# Django 1.10, Wagtail 1.10
- python: 2.7
env: TOXENV=py27-django110-wagtail110
- python: 3.5
env: TOXENV=py35-django110-wagtail110
- python: 3.6
env: TOXENV=py36-django110-wagtail110
# Django 1.11, Wagtail 1.10
- python: 2.7
env: TOXENV=py27-django111-wagtail110
- python: 3.5
env: TOXENV=py35-django111-wagtail110
- python: 3.6
env: TOXENV=py36-django111-wagtail110
allow_failures:
- python: 3.5
env: TOXENV=lint
env: TOXENV=py27-django111-wagtail113
install:
- pip install tox codecov
@ -41,3 +19,13 @@ script:
after_success:
- tox -e coverage-report
- codecov
deploy:
provider: pypi
distributions: sdist bdist_wheel
user: praekelt.org
password:
secure: IxPnc95OFNQsl7kFfLlLc38KfHh/W79VXnhEkdb2x1GZznOsG167QlhpAnyXIJysCQxgMMwLMuPOOdk1WIxOpGNM1/M80hNzPAfxMYWPuwposDdwoIc8SyREPJt16BXWAY+rAH8SHxld9p1YdRbOEPSSglODe4dCmQWsCbKjV3aKv+gZxBvf6pMxUglp2fBIlCwrE77MyMYh9iW0AGzD6atC2xN9NgAm4f+2WFlKCUL/MztLhNWdvWEiibQav6Tts7p8tWrsBVzywDW2IOy3O0ihPgRdISZ7QrxhiJTjlHYPAy4eRGOnYSQXvp6Dy8ONE5a6Uv5g3Xw2UmQo85sSMUs2VxR0G7d+PQgL0A7ENBQ5i7eSAFHYs8EswiGilW2A7mM4qRXwg9URLelYSdkM+aNXvR+25dCsXakiO4NjCz/BPgNzxPeQLlBdxR5vHugeM/XYuhy6CHlZrR/uueaO0X8RyhJcqeDjBy58IrwYS3Mpj7QCfBpQ9PqsqXEWV9BKwKiBXM2+3hkhawFDWa0GW2PDbORKtSLy/ORfGLx5Y9qxQYKEGvFQA3iqkTjrLj+KeUziKtuvEAcvsdBIJVIxeoHwdl+qqxEm8A7YuRBnWVxWc3jE6wBXboeFP92gVe/ueoXmY22riK9Ja0pli3TyNga8by9WM8Qp4D2ZqkVXHwg=
on:
tags: true
condition: $TOXENV = py27-django111-wagtail113

47
CHANGES
View File

@ -1,3 +1,50 @@
0.11.3
==================
- Bugfix: Handle errors when testing an invalid visit count rule
0.11.2
==================
- Bugfix: Stop populating static segments when the count is reached
0.11.1
==================
- Populate entirely static segments from registered Users not active Sessions
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
0.9.1 (tbd)
==================

View File

@ -1,3 +1,6 @@
include README.rst
recursive-include src
recursive-include src *
recursive-exclude src __pycache__
recursive-exclude src *.py[co]

View File

@ -55,10 +55,10 @@ author = 'Lab Digital BV'
# built documents.
#
# The short X.Y version.
version = '0.9.1'
version = '0.11.3'
# The full version, including alpha/beta/rc tags.
release = '0.9.1'
release = '0.11.3'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

@ -86,6 +86,11 @@
padding: 0;
margin: 0;
list-style: none;
.stat_card {
display: inline-block;
margin-bottom: 5px;
margin-right: 10px;
}
}
.block_container .block span.icon::before {
@ -93,11 +98,6 @@
vertical-align: bottom;
}
.block_container .block .inspect_container .inspect li {
display: inline-block;
margin-bottom: 5px;
}
.block_container .block .inspect_container .inspect li span {
display: block;
font-size: 20px;

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.9.1
current_version = 0.11.3
commit = true
tag = true
tag_name = {new_version}
@ -15,14 +15,14 @@ python_paths = .
[flake8]
ignore = E731
max-line-length = 120
exclude =
exclude =
src/**/migrations/*.py
[wheel]
universal = 1
[coverage:run]
omit =
omit =
src/**/migrations/*.py
[bumpversion:file:setup.py]

View File

@ -1,7 +1,6 @@
import re
from setuptools import find_packages, setup
install_requires = [
'wagtail>=2.0,<2.1',
'user-agents>=1.0.1',
@ -21,6 +20,7 @@ tests_require = [
'pytest-sugar==0.9.1',
'pytest==3.4.2',
'wagtail_factories==1.0.0',
'pytest-mock==1.6.3',
]
docs_require = [
@ -33,11 +33,11 @@ with open('README.rst') as fh:
setup(
name='wagtail-personalisation',
version='0.9.1',
version='0.12.0',
description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV',
author='Lab Digital BV and others',
author_email='opensource@labdigital.nl',
url='http://labdigital.nl',
url='https://labdigital.nl/',
install_requires=install_requires,
tests_require=tests_require,
extras_require={
@ -55,16 +55,10 @@ setup(
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
'Framework :: Django :: 1.9',
'Framework :: Django :: 1.10',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2',
'Topic :: Internet :: WWW/HTTP :: Site Management',
],
)

View File

@ -66,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 = (
@ -86,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 = []
@ -108,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.
@ -132,18 +140,19 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
if page_visits:
for page_visit in page_visits:
page_visit['count'] += 1
page_visit['path'] = page.url_path if page else self.request.path
self.request.session.modified = True
else:
visit_count.append({
'slug': page.slug,
'id': page.pk,
'path': self.request.path,
'path': page.url_path if page else self.request.path,
'count': 1,
})
def get_visit_count(self, page=None):
"""Return the number of visits on the current request or given page"""
path = page.path if page else self.request.path
path = page.url_path if page else self.request.path
visit_count = self.request.session.setdefault('visit_count', [])
for visit in visit_count:
if visit['path'] == path:
@ -170,21 +179,37 @@ 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:
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:
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:
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.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()

View File

@ -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'),
]

View File

@ -0,0 +1,138 @@
from __future__ import absolute_import, unicode_literals
from datetime import datetime
from importlib import import_module
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.test.client import RequestFactory
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
@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()
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
rules = [
form.instance for formset in self.formsets.values()
for form in formset
if form not in formset.deleted_forms
]
consistent = rules and Segment.all_static(rules)
if cleaned_data.get('type') == Segment.TYPE_STATIC and not cleaned_data.get('count') and not consistent:
self.add_error('count', _('Static segments with non-static compatible rules must include a count.'))
if self.instance.id and self.instance.is_static:
if self.has_changed():
self.add_error_to_fields(self, excluded=['name', 'enabled'])
for formset in self.formsets.values():
if formset.has_changed():
for form in formset:
if form not in formset.deleted_forms:
self.add_error_to_fields(form)
return cleaned_data
def add_error_to_fields(self, form, excluded=list()):
for field in form.changed_data:
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 and self.instance.is_static and not self.instance.all_rules_static:
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:
from .adapters import get_segment_adapter
request = RequestFactory().get('/')
request.session = SessionStore()
adapter = get_segment_adapter(request)
users_to_add = []
users_to_exclude = []
User = get_user_model()
users = User.objects.filter(is_active=True, is_staff=False)
matched_count = 0
for user in users.iterator():
request.user = user
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
if passes:
matched_count += 1
if instance.count == 0 or len(users_to_add) < instance.count:
if instance.randomise_into_segment():
users_to_add.append(user)
else:
users_to_exclude.append(user)
instance.matched_users_count = matched_count
instance.matched_count_updated_at = datetime.now()
instance.static_users.add(*users_to_add)
instance.excluded_users.add(*users_to_exclude)
return instance
@property
def media(self):
media = super(SegmentAdminForm, self).media
media.add_js(
[static('js/segment_form_control.js')]
)
return media

View File

@ -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):

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='\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ', max_length=20),
),
]

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-01 15:58
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', '0013_add_dynamic_static_to_segment'),
]
operations = [
migrations.RemoveField(
model_name='segment',
name='sessions',
),
migrations.AddField(
model_name='segment',
name='static_users',
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
]

View File

@ -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),
),
]

View File

@ -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)]),
),
]

View File

@ -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),
),
]

View File

@ -1,9 +1,14 @@
from __future__ import absolute_import, unicode_literals
import random
from django import forms
from django.conf import settings
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
from django.utils.functional import cached_property
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel
from wagtail.admin.edit_handlers import (
@ -13,6 +18,8 @@ from wagtail.core.models import Page
from wagtail_personalisation.rules import AbstractBaseRule
from wagtail_personalisation.utils import count_active_days
from .forms import SegmentAdminForm
class SegmentQuerySet(models.QuerySet):
def enabled(self):
@ -30,6 +37,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,9 +59,54 @@ 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=mark_safe(_("""
</br></br><strong>Dynamic:</strong> Users in this segment will change
as more or less meet the rules specified in the segment.
</br><strong>Static:</strong> If the segment contains only static
compatible rules the segment will contain the members that pass
those rules when the segment is created. Mixed static segments or
those containing entirely non static compatible rules will be
populated using the count variable.
"""))
)
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."
)
)
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()
base_form_class = SegmentAdminForm
def __init__(self, *args, **kwargs):
Segment.panels = [
MultiFieldPanel([
@ -56,11 +116,17 @@ class Segment(ClusterableModel):
FieldPanel('persistent'),
]),
FieldPanel('match_any'),
FieldPanel('type', widget=forms.RadioSelect),
FieldPanel('count', classname='count_field'),
FieldPanel('randomisation_percent', classname='percent_field'),
], heading="Segment"),
MultiFieldPanel([
InlinePanel(
"{}_related".format(rule_model._meta.db_table),
label=rule_model._meta.verbose_name,
label='{}{}'.format(
rule_model._meta.verbose_name,
' ({})'.format(_('Static compatible')) if rule_model.static else ''
),
) for rule_model in AbstractBaseRule.__subclasses__()
], heading=_("Rules")),
]
@ -70,6 +136,23 @@ class Segment(ClusterableModel):
def __str__(self):
return self.name
@property
def is_static(self):
return self.type == self.TYPE_STATIC
@classmethod
def all_static(cls, rules):
return all(rule.static for rule in rules)
@property
def all_rules_static(self):
rules = self.get_rules()
return rules and self.all_static(rules)
@property
def is_full(self):
return self.static_users.count() >= self.count
def encoded_name(self):
"""Return a string with a slug for the segment."""
return slugify(self.name.lower())
@ -102,6 +185,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

View File

@ -2,22 +2,30 @@ 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.core.exceptions import ObjectDoesNotExist
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.admin.edit_handlers import (
FieldPanel, FieldRowPanel, PageChooserPanel)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@python_2_unicode_compatible
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 +198,7 @@ class VisitCountRule(AbstractBaseRule):
"""
icon = 'fa-calculator'
static = True
OPERATOR_CHOICES = (
('more_than', _("More than")),
@ -218,16 +227,46 @@ 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)
# Django formsets don't honour 'required' fields so check rule is valid
try:
self.counted_page
except ObjectDoesNotExist:
return False
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()
visit_count = adapter.get_visit_count(self.counted_page)
if visit_count and operator == "more_than":
if visit_count > segment_count:
return True
@ -250,6 +289,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.

View File

@ -1,2 +1,2 @@
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li{display:inline-block;margin-bottom:5px}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
.nice-padding{padding-left:50px;padding-right:50px}.block_container{display:block;margin-top:30px}.block_container .block{display:block;float:left;-webkit-box-sizing:border-box;box-sizing:border-box;position:relative;width:calc(50% - 10px);min-height:216px;padding:10px 20px;margin-bottom:20px;border:1px solid #d9d9d9;border-radius:3px;background-color:#fff;-webkit-box-shadow:0 1px 3px transparent,0 1px 2px transparent;box-shadow:0 1px 3px transparent,0 1px 2px transparent;-webkit-transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1);transition:box-shadow .3s cubic-bezier(.25,.8,.25,1),border .3s cubic-bezier(.25,.8,.25,1),-webkit-box-shadow .3s cubic-bezier(.25,.8,.25,1);cursor:pointer}.block_container .block--disabled .inspect_container,.block_container .block--disabled h2{opacity:.5}.block_container .block h2{display:inline-block;width:auto}.block_container .block:nth-child(odd){margin-right:20px}.block_container .block .block_actions{list-style:none;margin:0;padding:0}.block_container .block .block_actions li{float:left;margin-right:10px}.block_container .block .block_actions li:last-child{margin-right:0}.block_container .block.suggestion{border:1px dashed #d9d9d9}.block_container .block:hover{border:1px solid #fff;-webkit-box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}@media (max-width:699px){.block_container .block{width:100%;margin-right:0}}.block_container .block .inspect_container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;margin-bottom:10px}.block_container .block .inspect_container .inspect{display:block;float:left;width:calc(50% - 10px);padding:0;margin:0;list-style:none}.block_container .block .inspect_container .inspect .stat_card{display:inline-block;margin-bottom:5px;margin-right:10px}.block_container .block span.icon:before{margin-right:.3em;vertical-align:bottom}.block_container .block .inspect_container .inspect li span{display:block;font-size:20px;font-weight:700;margin:5px 0;overflow-wrap:break-word}.block_container .block .inspect_container .inspect li pre{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;width:auto;background-color:#eee;border:1px solid #ccc;margin:5px 0 5px 21px;padding:2px 5px;word-wrap:break-word;word-break:break-all;border-radius:3px}.block_container .block.suggestion .suggestive_text{display:block;position:absolute;width:calc(100% - 40px);text-align:center;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);color:#d9d9d9;font-size:20px;font-weight:100}
/*# sourceMappingURL=dashboard.css.map*/

View File

@ -1 +1 @@
{"version":3,"sources":["webpack:///./scss/dashboard.scss"],"names":[],"mappings":"AAAA,cACI,kBACA,kBAAmB,CAGvB,iBACI,cACA,eAAgB,CAGpB,wBACI,cACA,WACA,8BAAsB,sBACtB,kBACA,uBACA,iBACA,kBACA,mBACA,yBACA,kBACA,sBACA,+DAAkE,uDAClE,2GAA8F,2UAC9F,cAAe,CAGnB,0FAEI,UAAY,CAGZ,2BACI,qBACA,UAAW,CAGnB,uCACI,iBAAkB,CAGlB,uCACI,gBACA,SACA,SAAU,CAGV,0CACI,WACA,iBAAkB,CAGtB,qDACI,cAAe,CAG3B,mCACI,yBAA0B,CAG9B,8BACI,sBACA,uEAAkE,+DAGtE,yBACI,wBACI,WACA,cAAe,CAClB,CAGL,2CACI,oBAAa,iCACb,8BAAmB,uEACnB,qBAAiB,iBACjB,yBAA8B,oDAC9B,0BAAoB,2CACpB,kBAAmB,CAGvB,oDACI,cACA,WACA,uBACA,UACA,SACA,eAAgB,CAGhB,yCACI,kBACA,qBAAsB,CAG1B,uDACI,qBACA,iBAAkB,CAGtB,4DACI,cACA,eACA,gBACA,aACA,wBAAyB,CAG7B,2DACI,kBACA,8BAAsB,sBACtB,WACA,sBACA,sBACA,sBACA,gBACA,qBACA,qBACA,iBAAkB,CAG1B,oDACI,cACA,kBACA,wBACA,kBACA,QACA,mCAA2B,2BAC3B,cACA,eACA,eAAgB","file":"../css/dashboard.css","sourcesContent":[".nice-padding {\n padding-left: 50px;\n padding-right: 50px;\n}\n\n.block_container {\n display: block;\n margin-top: 30px;\n}\n\n.block_container .block {\n display: block;\n float: left;\n box-sizing: border-box;\n position: relative;\n width: calc(50% - 10px);\n min-height: 216px;\n padding: 10px 20px;\n margin-bottom: 20px;\n border: 1px solid #d9d9d9;\n border-radius: 3px;\n background-color: #fff;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);\n cursor: pointer;\n}\n\n.block_container .block--disabled h2,\n.block_container .block--disabled .inspect_container {\n opacity: 0.5;\n}\n\n .block_container .block h2 {\n display: inline-block;\n width: auto;\n }\n\n.block_container .block:nth-child(odd) {\n margin-right: 20px;\n}\n\n .block_container .block .block_actions {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n .block_container .block .block_actions li {\n float: left;\n margin-right: 10px;\n }\n\n .block_container .block .block_actions li:last-child {\n margin-right: 0;\n }\n\n.block_container .block.suggestion {\n border: 1px dashed #d9d9d9;\n}\n\n.block_container .block:hover {\n border: 1px solid #fff;\n box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);\n}\n\n@media (max-width: 699px) {\n .block_container .block {\n width: 100%;\n margin-right: 0;\n }\n}\n\n.block_container .block .inspect_container {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: stretch;\n margin-bottom: 10px;\n}\n\n.block_container .block .inspect_container .inspect {\n display: block;\n float: left;\n width: calc(50% - 10px);\n padding: 0;\n margin: 0;\n list-style: none;\n}\n\n .block_container .block span.icon::before {\n margin-right: 0.3em;\n vertical-align: bottom;\n }\n\n .block_container .block .inspect_container .inspect li {\n display: inline-block;\n margin-bottom: 5px;\n }\n\n .block_container .block .inspect_container .inspect li span {\n display: block;\n font-size: 20px;\n font-weight: bold;\n margin: 5px 0;\n overflow-wrap: break-word;\n }\n\n .block_container .block .inspect_container .inspect li pre {\n position: relative;\n box-sizing: border-box;\n width: auto;\n background-color: #eee;\n border: 1px solid #ccc;\n margin: 5px 0 5px 21px;\n padding: 2px 5px;\n word-wrap: break-word;\n word-break: break-all;\n border-radius: 3px;\n }\n\n.block_container .block.suggestion .suggestive_text {\n display: block;\n position: absolute;\n width: calc(100% - 40px);\n text-align: center;\n top: 50%;\n transform: translateY(-50%);\n color: #d9d9d9;\n font-size: 20px;\n font-weight: 100;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./scss/dashboard.scss"],"sourceRoot":""}
{"version":3,"sources":["webpack:///./scss/dashboard.scss"],"names":[],"mappings":"AAAA,cACI,kBACA,kBAAmB,CAGvB,iBACI,cACA,eAAgB,CAGpB,wBACI,cACA,WACA,8BAAsB,sBACtB,kBACA,uBACA,iBACA,kBACA,mBACA,yBACA,kBACA,sBACA,+DAAkE,uDAClE,2GAA8F,2UAC9F,cAAe,CAGnB,0FAEI,UAAY,CAGZ,2BACI,qBACA,UAAW,CAGnB,uCACI,iBAAkB,CAGlB,uCACI,gBACA,SACA,SAAU,CAGV,0CACI,WACA,iBAAkB,CAGtB,qDACI,cAAe,CAG3B,mCACI,yBAA0B,CAG9B,8BACI,sBACA,uEAAkE,+DAGtE,yBACI,wBACI,WACA,cAAe,CAClB,CAGL,2CACI,oBAAa,iCACb,8BAAmB,uEACnB,qBAAiB,iBACjB,yBAA8B,oDAC9B,0BAAoB,2CACpB,kBAAmB,CAGvB,oDACI,cACA,WACA,uBACA,UACA,SACA,eAAgB,CAMnB,+DAJO,qBACA,kBACA,iBAAkB,CAItB,yCACI,kBACA,qBAAsB,CAG1B,4DACI,cACA,eACA,gBACA,aACA,wBAAyB,CAG7B,2DACI,kBACA,8BAAsB,sBACtB,WACA,sBACA,sBACA,sBACA,gBACA,qBACA,qBACA,iBAAkB,CAG1B,oDACI,cACA,kBACA,wBACA,kBACA,QACA,mCAA2B,2BAC3B,cACA,eACA,eAAgB","file":"../css/dashboard.css","sourcesContent":[".nice-padding {\n padding-left: 50px;\n padding-right: 50px;\n}\n\n.block_container {\n display: block;\n margin-top: 30px;\n}\n\n.block_container .block {\n display: block;\n float: left;\n box-sizing: border-box;\n position: relative;\n width: calc(50% - 10px);\n min-height: 216px;\n padding: 10px 20px;\n margin-bottom: 20px;\n border: 1px solid #d9d9d9;\n border-radius: 3px;\n background-color: #fff;\n box-shadow: 0 1px 3px rgba(0,0,0,0.00), 0 1px 2px rgba(0,0,0,0.00);\n transition: box-shadow 0.3s cubic-bezier(.25,.8,.25,1), border 0.3s cubic-bezier(.25,.8,.25,1);\n cursor: pointer;\n}\n\n.block_container .block--disabled h2,\n.block_container .block--disabled .inspect_container {\n opacity: 0.5;\n}\n\n .block_container .block h2 {\n display: inline-block;\n width: auto;\n }\n\n.block_container .block:nth-child(odd) {\n margin-right: 20px;\n}\n\n .block_container .block .block_actions {\n list-style: none;\n margin: 0;\n padding: 0;\n }\n\n .block_container .block .block_actions li {\n float: left;\n margin-right: 10px;\n }\n\n .block_container .block .block_actions li:last-child {\n margin-right: 0;\n }\n\n.block_container .block.suggestion {\n border: 1px dashed #d9d9d9;\n}\n\n.block_container .block:hover {\n border: 1px solid #fff;\n box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);\n}\n\n@media (max-width: 699px) {\n .block_container .block {\n width: 100%;\n margin-right: 0;\n }\n}\n\n.block_container .block .inspect_container {\n display: flex;\n flex-direction: row;\n flex-wrap: nowrap;\n justify-content: space-between;\n align-items: stretch;\n margin-bottom: 10px;\n}\n\n.block_container .block .inspect_container .inspect {\n display: block;\n float: left;\n width: calc(50% - 10px);\n padding: 0;\n margin: 0;\n list-style: none;\n .stat_card {\n display: inline-block;\n margin-bottom: 5px;\n margin-right: 10px;\n }\n}\n\n .block_container .block span.icon::before {\n margin-right: 0.3em;\n vertical-align: bottom;\n }\n\n .block_container .block .inspect_container .inspect li span {\n display: block;\n font-size: 20px;\n font-weight: bold;\n margin: 5px 0;\n overflow-wrap: break-word;\n }\n\n .block_container .block .inspect_container .inspect li pre {\n position: relative;\n box-sizing: border-box;\n width: auto;\n background-color: #eee;\n border: 1px solid #ccc;\n margin: 5px 0 5px 21px;\n padding: 2px 5px;\n word-wrap: break-word;\n word-break: break-all;\n border-radius: 3px;\n }\n\n.block_container .block.suggestion .suggestive_text {\n display: block;\n position: absolute;\n width: calc(100% - 40px);\n text-align: center;\n top: 50%;\n transform: translateY(-50%);\n color: #d9d9d9;\n font-size: 20px;\n font-weight: 100;\n}\n\n\n\n// WEBPACK FOOTER //\n// ./scss/dashboard.scss"],"sourceRoot":""}

View File

@ -0,0 +1,20 @@
(function($) {
$(document).ready( () => {
var count = $('.count_field');
var typeRadio = $('input:radio[name="type"]');
var updateCountDispay = function(value) {
if (value == 'dynamic') {
count.slideUp(250);
} else {
count.slideDown(250);
}
};
updateCountDispay(typeRadio.filter(':checked').val());
typeRadio.change( event => {
updateCountDispay(event.target.value);
});
});
})(jQuery);

View File

@ -22,25 +22,38 @@
<div class="nice-padding block_container">
{% if all_count %}
{% for segment in object_list %}
<div class="block block--{{ segment.status }}" onclick="location.href = 'edit/{{ segment.pk }}/'">
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
<h2>{{ segment }}</h2>
<div class="inspect_container">
<ul class="inspect segment_stats">
<li class="visit_stat">
<li class="stat_card">
{% trans "This segment has been visited" %}
<span class="icon icon-fa-rocket">{{ segment.visit_count|localize }} {% trans "time" %}{{ segment.visit_count|pluralize }}</span>
</li>
<li class="days_stat">
<li class="stat_card">
{% trans "This segment has been active for" %}
<span class="icon icon-fa-calendar">{{ segment.enable_date|days_since:segment.disable_date }} {% trans "day" %}{{ segment.enable_date|days_since:segment.disable_date|pluralize }}</span>
</li>
{% if segment.is_static %}
<li class="stat_card">
{% trans "This segment is Static" %}
<span class="icon icon-fa-user">
{{ segment.static_users.count|localize }}
{% if segment.static_users.count < segment.count %}
/ {{ segment.count }} {% trans "member" %}{{ segment.count|pluralize }}
{% else %}
{% trans "member" %}{{ segment.count|pluralize }}
{% endif %}
</span>
</li>
{% endif %}
</ul>
<hr />
<ul class="inspect segment_rules">
<li class="match_state {{ segment.match_any|yesno:"any,all" }}">
<li class="stat_card {{ segment.match_any|yesno:"any,all" }}">
{% trans "The visitor must match" %}
{% if segment.match_any %}
<span class="icon icon-fa-cube">{% trans "Any rule" %}</span>
@ -49,7 +62,7 @@
{% endif %}
</li>
<li class="persistent_state {{ segment.persistent|yesno:"persistent,fleeting" }}">
<li class="stat_card {{ segment.persistent|yesno:"persistent,fleeting" }}">
{% trans "The persistence of this segment is" %}
{% if segment.persistent %}
<span class="icon icon-fa-bookmark" title="{% trans "This segment persists in between visits" %}">{% trans "Persistent" %}</span>
@ -58,8 +71,15 @@
{% 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="{{ rule.encoded_name }}">
<li class="stat_card {{ rule.encoded_name }}">
{{ rule.description.title }}
{% if rule.description.code %}
<pre>{{ rule.description.value }}</pre>
@ -68,6 +88,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>
@ -78,7 +103,10 @@
{% elif segment.status == segment.STATUS_ENABLED %}
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
{% endif %}
<li><a href="edit/{{ segment.pk }}/" title="{% trans "Configure this segment" %}">configure this</a></li>
<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>

View File

@ -1,9 +1,12 @@
from __future__ import absolute_import, unicode_literals
import csv
from django import forms
from django.urls 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.urls import reverse
from django.utils.translation import ugettext_lazy as _
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.contrib.modeladmin.views import IndexView
@ -139,3 +142,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()

View File

@ -3,11 +3,11 @@ from __future__ import absolute_import, unicode_literals
import logging
from django.conf.urls import include, url
from django.urls import reverse
from django.template.defaultfilters import pluralize
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from wagtail.admin.site_summary import SummaryItem, PagesSummaryItem
from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem
from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook
from wagtail.core import hooks
from wagtail.core.models import Page

View File

@ -44,3 +44,8 @@ class RequestFactory(BaseRequestFactory):
request.session = SessionStore()
request._messages = FallbackStorage(request)
return request
@pytest.fixture
def user(django_user_model):
return django_user_model.objects.create(username='user')

View File

@ -1,9 +1,6 @@
from __future__ import absolute_import, unicode_literals
import os
from pkg_resources import parse_version as V
import django
DATABASES = {
@ -57,15 +54,15 @@ TEMPLATES = [
]
MIDDLEWARE = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.core.middleware.SiteMiddleware',
)
'wagtail.core.middleware.SiteMiddleware',
)
INSTALLED_APPS = (
'wagtail_personalisation',

View File

@ -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.core.fields.RichTextField(blank=True, default='')),
],

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# 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.core.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.core.fields.RichTextField(blank=True, default='')),
],

View File

@ -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'

View File

@ -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):

View File

@ -1,5 +1,9 @@
import pytest
from tests.factories.rule import VisitCountRuleFactory
from tests.factories.segment import SegmentFactory
from wagtail_personalisation.rules import VisitCountRule
@pytest.mark.django_db
def test_visit_count(site, client):
@ -20,3 +24,56 @@ 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_call_test_user_on_invalid_rule_fails(site, user, mocker):
rule = VisitCountRule()
assert not (rule.test_user(None, user))
@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'

View File

@ -0,0 +1,574 @@
from __future__ import absolute_import, unicode_literals
import datetime
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
def form_with_data(segment, *rules):
model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent']
class TestSegmentAdminForm(SegmentAdminForm):
class Meta:
model = Segment
fields = model_fields
data = model_to_dict(segment, model_fields)
for formset in TestSegmentAdminForm().formsets.values():
rule_data = {}
count = 0
for rule in rules:
if isinstance(rule, formset.model):
rule_data = model_to_dict(rule)
for key, value in rule_data.items():
data['{}-{}-{}'.format(formset.prefix, count, key)] = value
count += 1
data['{}-INITIAL_FORMS'.format(formset.prefix)] = 0
data['{}-TOTAL_FORMS'.format(formset.prefix)] = count
return TestSegmentAdminForm(data)
@pytest.mark.django_db
def test_user_added_to_static_segment_at_creation(site, user, mocker):
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)
instance = form.save()
assert user in instance.static_users.all()
@pytest.mark.django_db
def test_user_not_added_to_full_static_segment_at_creation(site, django_user_model, mocker):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second')
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user',
side_effect=[True, True])
instance = form.save()
assert len(instance.static_users.all()) == 1
@pytest.mark.django_db
def test_anonymous_user_not_added_to_static_segment_at_creation(site, client, mocker):
session = client.session
session.save()
client.get(site.root_page.url)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
instance = form.save()
assert not instance.static_users.all()
assert mock_test_rule.call_count == 0
@pytest.mark.django_db
def test_match_any_correct_populates(site, django_user_model, mocker):
user = django_user_model.objects.create(username='first')
other_user = django_user_model.objects.create(username='second')
other_page = site.root_page.get_last_child()
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, match_any=True)
rule_1 = VisitCountRule(counted_page=site.root_page)
rule_2 = VisitCountRule(counted_page=other_page)
form = form_with_data(segment, rule_1, rule_2)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', side_effect=[True, False, True, False])
instance = form.save()
assert user in instance.static_users.all()
assert other_user in instance.static_users.all()
@pytest.mark.django_db
def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, mocker):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
static_rule = VisitCountRule(counted_page=site.root_page)
non_static_rule = TimeRule(
start_time=datetime.time(0, 0, 0),
end_time=datetime.time(23, 59, 59),
)
form = form_with_data(segment, static_rule, non_static_rule)
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
instance = form.save()
assert not instance.static_users.all()
assert mock_test_rule.call_count == 0
@pytest.mark.django_db
def test_session_not_added_to_static_segment_after_creation(site, client, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=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 not instance.static_users.all()
@pytest.mark.django_db
def test_session_added_to_static_segment_after_creation(site, client, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
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 user in instance.static_users.all()
@pytest.mark.django_db
def test_anonymou_user_not_added_to_static_segment_after_creation(site, client):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
session = client.session
session.save()
client.get(site.root_page.url)
assert not instance.static_users.all()
@pytest.mark.django_db
def test_session_not_added_to_static_segment_after_full(site, client, django_user_model):
user = django_user_model.objects.create(username='first')
other_user = django_user_model.objects.create(username='second')
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
assert not instance.static_users.all()
session = client.session
client.force_login(user)
client.get(site.root_page.url)
assert instance.static_users.count() == 1
client.cookies.clear()
second_session = client.session
client.force_login(other_user)
client.get(site.root_page.url)
assert session.session_key != second_session.session_key
assert instance.static_users.count() == 1
assert user in instance.static_users.all()
assert other_user not in instance.static_users.all()
@pytest.mark.django_db
def test_sessions_not_added_to_static_segment_if_rule_not_static(mocker):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
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_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
instance = form.save()
assert not instance.static_users.all()
assert mock_test_rule.call_count == 0
@pytest.mark.django_db
def test_does_not_calculate_the_segment_again(site, client, mocker, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=2)
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
instance = form.save()
assert user in instance.static_users.all()
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
@pytest.mark.django_db
def test_non_static_rules_have_a_count():
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
rule = TimeRule(
start_time=datetime.time(0, 0, 0),
end_time=datetime.time(23, 59, 59),
segment=segment,
)
form = form_with_data(segment, rule)
assert not form.is_valid()
@pytest.mark.django_db
def test_static_segment_with_static_rules_needs_no_count(site):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=0)
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
form = form_with_data(segment, rule)
assert form.is_valid()
@pytest.mark.django_db
def test_dynamic_segment_with_non_static_rules_have_a_count():
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC, count=0)
rule = TimeRule(
start_time=datetime.time(0, 0, 0),
end_time=datetime.time(23, 59, 59),
)
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, mocker, user):
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)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
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, mocker, user):
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)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
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, 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)
form.instance.type = Segment.TYPE_STATIC
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

View File

@ -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
View 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'

View File

@ -17,7 +17,6 @@ commands =
coverage combine
coverage report
[testenv:lint]
basepython = python3.6
deps = flake8