7

Compare commits

..

9 Commits

50 changed files with 1434 additions and 4642 deletions

View File

@ -1,89 +0,0 @@
---
name: Python Tests
on: [push, pull_request]
jobs:
format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: pip install tox
- name: Validate formatting
run: tox -e format
test:
runs-on: ubuntu-latest
strategy:
max-parallel: 4
matrix:
tox_env:
- py36-dj22-wt211
- py36-dj22-wt212
- py36-dj22-wt213
- py37-dj22-wt211
- py37-dj22-wt212
- py37-dj22-wt213
- py38-dj22-wt211
- py38-dj22-wt212
- py38-dj22-wt213
- py37-dj30-wt211
- py37-dj30-wt212
- py37-dj30-wt213
- py38-dj30-wt211
- py38-dj30-wt212
- py38-dj30-wt213
include:
- python-version: 3.6
tox_env: py36-dj22-wt211
- python-version: 3.6
tox_env: py36-dj22-wt212
- python-version: 3.6
tox_env: py36-dj22-wt213
- python-version: 3.7
tox_env: py37-dj22-wt211
- python-version: 3.7
tox_env: py37-dj22-wt212
- python-version: 3.7
tox_env: py37-dj22-wt213
- python-version: 3.8
tox_env: py38-dj22-wt211
- python-version: 3.8
tox_env: py38-dj22-wt212
- python-version: 3.8
tox_env: py38-dj22-wt213
- python-version: 3.7
tox_env: py37-dj30-wt211
- python-version: 3.7
tox_env: py37-dj30-wt212
- python-version: 3.7
tox_env: py37-dj30-wt213
- python-version: 3.8
tox_env: py38-dj30-wt211
- python-version: 3.8
tox_env: py38-dj30-wt212
- python-version: 3.8
tox_env: py38-dj30-wt213
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v1
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install tox tox-gh-actions
- name: Test with tox
run: tox -e ${{ matrix.tox_env }} --index-url=https://pypi.python.org/simple/
- name: Prepare artifacts
run: mkdir -p .coverage-data && mv .coverage.* .coverage-data/
- uses: actions/upload-artifact@master
with:
name: coverage-data
path: .coverage-data/

1
.gitignore vendored
View File

@ -13,7 +13,6 @@
.vscode/ .vscode/
build/ build/
ve/
dist/ dist/
htmlcov/ htmlcov/
docs/_build docs/_build

22
.travis.yml Normal file
View File

@ -0,0 +1,22 @@
---
sudo: false
language: python
matrix:
include:
- python: 3.6
env: lint
- python: 3.6
env: TOXENV=py36-django20-wagtail20
- python: 3.6
env: TOXENV=py36-django20-wagtail21
install:
- pip install tox codecov
script:
- tox
after_success:
- tox -e coverage-report
- codecov

30
CHANGES
View File

@ -1,33 +1,3 @@
0.13.0
=================
- Merged Praekelt fork
- Add custom javascript to segment forms
- bugfix:exclude variant returns queryset when params is queryset
- Added RulePanel, a subclass of InlinePanel, for Rules
- Upgrade to Wagtail > 2.0, drop support for Wagtail < 2
0.12.0
==================
- Fix Django version classifier in setup.py
0.12.0
==================
- Merged forks of Torchbox and Praekelt
- Wagtail 2 compatibility
- Makefile adjustments for portability
- Adds simple segment forcing for superusers
- Fix excluding pages without variant
- Fix bug on visiting a segment page in the admin
- Use Wagtail's logic in the page count in the dash
- Prevent corrected summary item from counting the root page
- Delete variants of a page that is being deleted
- Add end user and developer documentation
- Add an option to show a personalised block to everyone
- Add origin country rule (#190)
- Return 404 if variant page is accessed directly (#188)
- Do not generate sitemap entries for variants (#187)
- Remove restrictive wagtail dependency version constraint (#192)
0.11.3 0.11.3
================== ==================
- Bugfix: Handle errors when testing an invalid visit count rule - Bugfix: Handle errors when testing an invalid visit count rule

View File

@ -54,7 +54,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = 'wagtail-personalisation' project = 'wagtail-personalisation'
copyright = '2019, Lab Digital BV' copyright = '2018, Lab Digital BV'
author = 'Lab Digital BV' author = 'Lab Digital BV'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
@ -62,10 +62,10 @@ author = 'Lab Digital BV'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = '0.15.1' version = '0.12.0'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = '0.15.1' release = '0.12.0'
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

View File

@ -131,47 +131,3 @@ Is logged in Whether the user is logged in or logged out.
================== ========================================================== ================== ==========================================================
``wagtail_personalisation.rules.UserIsLoggedInRule`` ``wagtail_personalisation.rules.UserIsLoggedInRule``
Origin country rule
-------------------
The origin country rule allows you to match visitors based on the origin
country of their request. This rule requires to have set up a way to detect
countries beforehand.
================== ==========================================================
Option Description
================== ==========================================================
Country What country user's request comes from.
================== ==========================================================
You must have one of the following configurations set up in order to
make it work.
- Cloudflare IP Geolocation - ``cf-ipcountry`` HTTP header set with a value of
the alpha-2 country format.
- CloudFront Geo-Targeting - ``cloudfront-viewer-country`` header set with a
value of the alpha-2 country format.
- The last fallback is to use GeoIP2 module that is included with Django. This
requires setting up an IP database beforehand, see the Django's
`GeoIP2 instructions <https://docs.djangoproject.com/en/stable/ref/contrib/gis/geoip2/>`_
for more information. It will use IP of the request, using HTTP header
the ``x-forwarded-for`` HTTP header and ``REMOTE_ADDR`` server value as a
fallback. If you want to use a custom logic when obtaining IP address, please
set the ``WAGTAIL_PERSONALISATION_IP_FUNCTION`` setting to the function that takes a
request as an argument, e.g.
.. code-block:: python
# settings.py
WAGTAIL_PERSONALISATION_IP_FUNCTION = 'yourproject.utils.get_client_ip'
# yourproject/utils.py
def get_client_ip(request):
return request['HTTP_CF_CONNECTING_IP']
``wagtail_personalisation.rules.OriginCountryRule``

View File

@ -25,7 +25,7 @@
"enable_date": "2017-06-02T10:58:39.389Z", "enable_date": "2017-06-02T10:58:39.389Z",
"disable_date": "2017-06-02T10:34:51.722Z", "disable_date": "2017-06-02T10:34:51.722Z",
"visit_count": 0, "visit_count": 0,
"status": "enabled", "enabled": true,
"persistent": false, "persistent": false,
"match_any": false "match_any": false
} }
@ -39,7 +39,7 @@
"enable_date": "2017-06-02T10:57:44.497Z", "enable_date": "2017-06-02T10:57:44.497Z",
"disable_date": "2017-06-02T10:57:39.984Z", "disable_date": "2017-06-02T10:57:39.984Z",
"visit_count": 1, "visit_count": 1,
"status": "enabled", "enabled": true,
"persistent": false, "persistent": false,
"match_any": false "match_any": false
} }

View File

@ -1,4 +1,4 @@
Django>=2.2,<2.3 Django>=2.0,<2.1
wagtail>=2.6,<2.7 wagtail>=2.1,<2.2
django-debug-toolbar==2.0 django-debug-toolbar==1.9.1
-e .[docs,test] -e .[docs,test]

View File

@ -1,20 +0,0 @@
# Generated by Django 2.1.7 on 2019-03-15 12:54
from django.db import migrations
import sandbox.apps.user.models
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AlterModelManagers(
name='user',
managers=[
('objects', sandbox.apps.user.models.UserManager()),
],
),
]

View File

@ -1,44 +1,14 @@
from django.contrib.auth.models import ( from django.contrib.auth.models import (
AbstractBaseUser, PermissionsMixin, BaseUserManager) AbstractBaseUser, PermissionsMixin, UserManager)
from django.core.mail import send_mail from django.core.mail import send_mail
from django.db import models from django.db import connections, models
from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
class UserManager(BaseUserManager):
use_in_migrations = True
def _create_user(self, email, password, **extra_fields):
"""
Create and save a user with the given username, email, and password.
"""
if not email:
raise ValueError('The given email address must be set')
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
user.save(using=self._db)
return user
def create_user(self, email, password=None, **extra_fields):
extra_fields.setdefault('is_staff', False)
extra_fields.setdefault('is_superuser', False)
return self._create_user(email, password, **extra_fields)
def create_superuser(self, email, password, **extra_fields):
extra_fields.setdefault('is_staff', True)
extra_fields.setdefault('is_superuser', True)
if extra_fields.get('is_staff') is not True:
raise ValueError('Superuser must have is_staff=True.')
if extra_fields.get('is_superuser') is not True:
raise ValueError('Superuser must have is_superuser=True.')
return self._create_user(email, password, **extra_fields)
class User(AbstractBaseUser, PermissionsMixin): class User(AbstractBaseUser, PermissionsMixin):
"""Customized version of the default `AbstractUser` from Django. """Cusomtized version of the default `AbstractUser` from Django.
""" """
first_name = models.CharField(_('first name'), max_length=100, blank=True) first_name = models.CharField(_('first name'), max_length=100, blank=True)

View File

@ -14,7 +14,6 @@ from __future__ import absolute_import, unicode_literals
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
from importlib.util import find_spec
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
BASE_DIR = os.path.dirname(PROJECT_DIR) BASE_DIR = os.path.dirname(PROJECT_DIR)
@ -79,14 +78,11 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.core.middleware.SiteMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware', 'wagtail.contrib.redirects.middleware.RedirectMiddleware',
] ]
if find_spec('wagtail.contrib.legacy'):
MIDDLEWARE += ('wagtail.contrib.legacy.sitemiddleware.SiteMiddleware',)
else:
MIDDLEWARE += ('wagtail.core.middleware.SiteMiddleware', )
ROOT_URLCONF = 'sandbox.urls' ROOT_URLCONF = 'sandbox.urls'
TEMPLATES = [ TEMPLATES = [

View File

@ -11,9 +11,9 @@ from wagtail.documents import urls as wagtaildocs_urls
from sandbox.apps.search import views as search_views from sandbox.apps.search import views as search_views
urlpatterns = [ urlpatterns = [
url(r'^django-admin/', admin.site.urls), url(r'^admin/', admin.site.urls),
url(r'^admin/', include(wagtailadmin_urls)), url(r'^cms/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)), url(r'^documents/', include(wagtaildocs_urls)),
url(r'^search/$', search_views.search, name='search'), url(r'^search/$', search_views.search, name='search'),

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.15.1 current_version = 0.12.0
commit = true commit = true
tag = true tag = true
tag_name = {new_version} tag_name = {new_version}
@ -28,3 +28,4 @@ omit = src/**/migrations/*.py
[bumpversion:file:setup.py] [bumpversion:file:setup.py]
[bumpversion:file:docs/conf.py] [bumpversion:file:docs/conf.py]

View File

@ -2,25 +2,24 @@ import re
from setuptools import find_packages, setup from setuptools import find_packages, setup
install_requires = [ install_requires = [
'wagtail>=2.0', 'wagtail>=2.0,<2.2',
'user-agents>=1.1.0', 'user-agents>=1.1.0',
'wagtailfontawesome>=1.1.3', 'wagtailfontawesome>=1.1.3',
'pycountry',
] ]
tests_require = [ tests_require = [
'factory_boy==2.8.1', 'factory_boy==2.8.1',
'flake8-blind-except', 'flake8-blind-except',
'flake8-debugger', 'flake8-debugger',
'flake8-isort', 'flake8-imports',
'flake8', 'flake8',
'freezegun==0.3.8', 'freezegun==0.3.8',
'pytest-cov==2.5.1', 'pytest-cov==2.5.1',
'pytest-django==4.1.0', 'pytest-django==3.1.2',
'pytest-pythonpath==0.7.2', 'pytest-pythonpath==0.7.2',
'pytest-sugar==0.9.1', 'pytest-sugar==0.9.1',
'pytest==6.1.2', 'pytest==3.4.2',
'wagtail_factories==1.1.0', 'wagtail_factories==1.0.0',
'pytest-mock==1.6.3', 'pytest-mock==1.6.3',
] ]
@ -35,7 +34,7 @@ with open('README.rst') as fh:
setup( setup(
name='wagtail-personalisation', name='wagtail-personalisation',
version='0.15.1', version='0.12.0',
description='A Wagtail add-on for showing personalized content', description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV and others', author='Lab Digital BV and others',
author_email='opensource@labdigital.nl', author_email='opensource@labdigital.nl',
@ -52,7 +51,7 @@ setup(
license='MIT', license='MIT',
long_description=long_description, long_description=long_description,
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment', 'Environment :: Web Environment',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
@ -60,7 +59,7 @@ setup(
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.6',
'Framework :: Django', 'Framework :: Django',
'Framework :: Django :: 2.0', 'Framework :: Django :: 2',
'Topic :: Internet :: WWW/HTTP :: Site Management', 'Topic :: Internet :: WWW/HTTP :: Site Management',
], ],
) )

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, unicode_literals
from django.conf import settings from django.conf import settings
from django.db.models import F from django.db.models import F
from django.utils.module_loading import import_string from django.utils.module_loading import import_string
@ -194,10 +196,8 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
for segment in enabled_segments: for segment in enabled_segments:
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists(): if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
additional_segments.append(segment) additional_segments.append(segment)
elif any(( elif (segment.excluded_users.filter(id=self.request.user.id).exists() or
segment.excluded_users.filter(id=self.request.user.id).exists(), segment in excluded_segments):
segment in excluded_segments
)):
continue continue
elif not segment.is_static or not segment.is_full: elif not segment.is_static or not segment.is_full:
segment_rules = [] segment_rules = []

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import admin from django.contrib import admin
from wagtail_personalisation import models, rules from wagtail_personalisation import models, rules

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url from django.conf.urls import url
from wagtail_personalisation import views from wagtail_personalisation import views

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from wagtail.core import blocks from wagtail.core import blocks
@ -6,7 +8,6 @@ from wagtail_personalisation.models import Segment
def list_segment_choices(): def list_segment_choices():
yield -1, ("Show to everyone")
for pk, name in Segment.objects.values_list('pk', 'name'): for pk, name in Segment.objects.values_list('pk', 'name'):
yield pk, name yield pk, name
@ -34,19 +35,10 @@ class PersonalisedStructBlock(blocks.StructBlock):
adapter = get_segment_adapter(request) adapter = get_segment_adapter(request)
user_segments = adapter.get_segments() user_segments = adapter.get_segments()
try: if value['segment']:
segment_id = int(value['segment'])
except (ValueError, TypeError):
return ''
if segment_id > 0:
for segment in user_segments: for segment in user_segments:
if segment.id == segment_id: if segment.id == int(value['segment']):
return super(PersonalisedStructBlock, self).render( return super(PersonalisedStructBlock, self).render(
value, context) value, context)
if segment_id == -1: return ""
return super(PersonalisedStructBlock, self).render(
value, context)
return ''

View File

@ -1,19 +1,21 @@
from __future__ import absolute_import, unicode_literals
from datetime import datetime from datetime import datetime
import functools
from importlib import import_module from importlib import import_module
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.templatetags.static import static from django.contrib.staticfiles.templatetags.staticfiles import static
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils.lru_cache import lru_cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from wagtail.admin.forms import WagtailAdminModelForm from wagtail.admin.forms import WagtailAdminModelForm
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
@functools.lru_cache(maxsize=1000) @lru_cache(maxsize=1000)
def user_from_data(user_id): def user_from_data(user_id):
User = get_user_model() User = get_user_model()
try: try:

View File

@ -1,7 +1,7 @@
# Generated by Django 2.0.7 on 2018-07-04 15:26 # Generated by Django 2.0.7 on 2018-07-04 15:26
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):

View File

@ -1,7 +1,7 @@
# Generated by Django 2.0.5 on 2018-07-19 09:57 # Generated by Django 2.0.5 on 2018-07-19 09:57
import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,45 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-08-10 13:48
from __future__ import unicode_literals
from django.db import migrations, models
def forward(apps, schema_editor):
Segment = apps.get_model('wagtail_personalisation', 'Segment')
for segment in Segment.objects.all():
segment.enabled = segment.status == 'enabled'
segment.save()
def backward(apps, schema_editor):
Segment = apps.get_model('wagtail_personalisation', 'Segment')
for segment in Segment.objects.all():
if segment.enabled:
segment.status = 'enabled'
else:
segment.status = 'disabled'
segment.save()
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0023_personalisablepagemetadata_variant_cascade'),
]
operations = [
migrations.AddField(
model_name='segment',
name='enabled',
field=models.BooleanField(default=True, help_text='Should the segment be active?'),
),
migrations.RunPython(forward, reverse_code=backward),
migrations.RemoveField(
model_name='segment',
name='status',
),
]

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
import random import random
import wagtail
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models, transaction from django.db import models, transaction
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -20,29 +20,14 @@ from wagtail_personalisation.utils import count_active_days
from .forms import SegmentAdminForm from .forms import SegmentAdminForm
class RulePanel(InlinePanel):
def on_model_bound(self):
self.relation_name = self.relation_name.replace('_related', 's')
self.db_field = self.model._meta.get_field(self.relation_name)
manager = getattr(self.model, self.relation_name)
self.related = manager.rel
class SegmentQuerySet(models.QuerySet): class SegmentQuerySet(models.QuerySet):
def enabled(self): def enabled(self):
return self.filter(status=self.model.STATUS_ENABLED) return self.filter(enabled=True)
@python_2_unicode_compatible
class Segment(ClusterableModel): class Segment(ClusterableModel):
"""The segment model.""" """The segment model."""
STATUS_ENABLED = 'enabled'
STATUS_DISABLED = 'disabled'
STATUS_CHOICES = (
(STATUS_ENABLED, _('Enabled')),
(STATUS_DISABLED, _('Disabled')),
)
TYPE_DYNAMIC = 'dynamic' TYPE_DYNAMIC = 'dynamic'
TYPE_STATIC = 'static' TYPE_STATIC = 'static'
@ -57,8 +42,8 @@ class Segment(ClusterableModel):
enable_date = models.DateTimeField(null=True, editable=False) enable_date = models.DateTimeField(null=True, editable=False)
disable_date = models.DateTimeField(null=True, editable=False) disable_date = models.DateTimeField(null=True, editable=False)
visit_count = models.PositiveIntegerField(default=0, editable=False) visit_count = models.PositiveIntegerField(default=0, editable=False)
status = models.CharField( enabled = models.BooleanField(
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED) default=True, help_text=_("Should the segment be active?"))
persistent = models.BooleanField( persistent = models.BooleanField(
default=False, help_text=_("Should the segment persist between visits?")) default=False, help_text=_("Should the segment persist between visits?"))
match_any = models.BooleanField( match_any = models.BooleanField(
@ -118,7 +103,7 @@ class Segment(ClusterableModel):
MultiFieldPanel([ MultiFieldPanel([
FieldPanel('name', classname="title"), FieldPanel('name', classname="title"),
FieldRowPanel([ FieldRowPanel([
FieldPanel('status'), FieldPanel('enabled'),
FieldPanel('persistent'), FieldPanel('persistent'),
]), ]),
FieldPanel('match_any'), FieldPanel('match_any'),
@ -127,8 +112,8 @@ class Segment(ClusterableModel):
FieldPanel('randomisation_percent', classname='percent_field'), FieldPanel('randomisation_percent', classname='percent_field'),
], heading="Segment"), ], heading="Segment"),
MultiFieldPanel([ MultiFieldPanel([
RulePanel( InlinePanel(
"{}_related".format(rule_model._meta.db_table), "{}s".format(rule_model._meta.db_table),
label='{}{}'.format( label='{}{}'.format(
rule_model._meta.verbose_name, rule_model._meta.verbose_name,
' ({})'.format(_('Static compatible')) if rule_model.static else '' ' ({})'.format(_('Static compatible')) if rule_model.static else ''
@ -185,9 +170,7 @@ class Segment(ClusterableModel):
return segment_rules return segment_rules
def toggle(self, save=True): def toggle(self, save=True):
self.status = ( self.enabled = not self.enabled
self.STATUS_ENABLED if self.status == self.STATUS_DISABLED
else self.STATUS_DISABLED)
if save: if save:
self.save() self.save()
@ -309,15 +292,3 @@ class PersonalisablePageMixin:
metadata = PersonalisablePageMetadata.objects.create( metadata = PersonalisablePageMetadata.objects.create(
canonical_page=self, variant=self) canonical_page=self, variant=self)
return metadata return metadata
def get_sitemap_urls(self, request=None):
# Do not generate sitemap entries for variants.
if not self.personalisation_metadata.is_canonical:
return []
if wagtail.VERSION >= (2, 2):
# Since Wagtail 2.2 you can pass request to the get_sitemap_urls
# method.
return super(PersonalisablePageMixin, self).get_sitemap_urls(
request=request
)
return super(PersonalisablePageMixin, self).get_sitemap_urls()

View File

@ -7,16 +7,16 @@ from wagtail_personalisation.models import Segment
def check_status_change(sender, instance, *args, **kwargs): def check_status_change(sender, instance, *args, **kwargs):
"""Check if the status has changed. Alter dates accordingly.""" """Check if the status has changed. Alter dates accordingly."""
try: try:
original_status = sender.objects.get(pk=instance.id).status original_status = sender.objects.get(pk=instance.id).enabled
except sender.DoesNotExist: except sender.DoesNotExist:
original_status = "" original_status = None
if original_status != instance.status: if original_status != instance.enabled:
if instance.status == instance.STATUS_ENABLED: if instance.enabled is True:
instance.enable_date = timezone.now() instance.enable_date = timezone.now()
instance.visit_count = 0 instance.visit_count = 0
return instance return instance
if instance.status == instance.STATUS_DISABLED: if instance.enabled is False:
instance.disable_date = timezone.now() instance.disable_date = timezone.now()

View File

@ -1,8 +1,9 @@
import logging from __future__ import absolute_import, unicode_literals
import re import re
from datetime import datetime
from importlib import import_module from importlib import import_module
import pycountry
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
@ -10,36 +11,17 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils import timezone from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from user_agents import parse from user_agents import parse
from wagtail.admin.edit_handlers import ( from wagtail.admin.edit_handlers import (
FieldPanel, FieldRowPanel, PageChooserPanel) FieldPanel, FieldRowPanel, PageChooserPanel)
from wagtail_personalisation.utils import get_client_ip
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
logger = logging.getLogger(__name__)
def get_geoip_module():
try:
from django.contrib.gis.geoip2 import GeoIP2
return GeoIP2
except ImportError:
logger.exception(
'GeoIP module is disabled. To use GeoIP for the origin\n'
'country personaliastion rule please set it up as per '
'documentation:\n'
'https://docs.djangoproject.com/en/stable/ref/contrib/gis/'
'geoip2/.\n'
'Wagtail-personalisation also works with Cloudflare and\n'
'CloudFront country detection, so you should not see this\n'
'warning if you use one of those.')
@python_2_unicode_compatible
class AbstractBaseRule(models.Model): class AbstractBaseRule(models.Model):
"""Base for creating rules to segment users with.""" """Base for creating rules to segment users with."""
icon = 'fa-circle-o' icon = 'fa-circle-o'
@ -55,7 +37,7 @@ class AbstractBaseRule(models.Model):
verbose_name = 'Abstract segmentation rule' verbose_name = 'Abstract segmentation rule'
def __str__(self): def __str__(self):
return str(self._meta.verbose_name) return force_text(self._meta.verbose_name)
def test_user(self): def test_user(self):
"""Test if the user matches this rule.""" """Test if the user matches this rule."""
@ -63,7 +45,7 @@ class AbstractBaseRule(models.Model):
def encoded_name(self): def encoded_name(self):
"""Return a string with a slug for the rule.""" """Return a string with a slug for the rule."""
return slugify(str(self).lower()) return slugify(force_text(self).lower())
def description(self): def description(self):
"""Return a description explaining the functionality of the rule. """Return a description explaining the functionality of the rule.
@ -109,7 +91,7 @@ class TimeRule(AbstractBaseRule):
verbose_name = _('Time Rule') verbose_name = _('Time Rule')
def test_user(self, request=None): def test_user(self, request=None):
return self.start_time <= timezone.now().time() <= self.end_time return self.start_time <= datetime.now().time() <= self.end_time
def description(self): def description(self):
return { return {
@ -153,7 +135,7 @@ class DayRule(AbstractBaseRule):
def test_user(self, request=None): def test_user(self, request=None):
return [self.mon, self.tue, self.wed, self.thu, return [self.mon, self.tue, self.wed, self.thu,
self.fri, self.sat, self.sun][timezone.now().date().weekday()] self.fri, self.sat, self.sun][datetime.today().weekday()]
def description(self): def description(self):
days = ( days = (
@ -419,72 +401,10 @@ class UserIsLoggedInRule(AbstractBaseRule):
verbose_name = _('Logged in Rule') verbose_name = _('Logged in Rule')
def test_user(self, request=None): def test_user(self, request=None):
return request.user.is_authenticated == self.is_logged_in return request.user.is_authenticated() == self.is_logged_in
def description(self): def description(self):
return { return {
'title': _('These visitors are'), 'title': _('These visitors are'),
'value': _('Logged in') if self.is_logged_in else _('Not logged in'), 'value': _('Logged in') if self.is_logged_in else _('Not logged in'),
} }
COUNTRY_CHOICES = [(country.alpha_2.lower(), country.name)
for country in pycountry.countries]
class OriginCountryRule(AbstractBaseRule):
"""
Test user against the country or origin of their request.
Using this rule requires setting up GeoIP2 on Django or using
CloudFlare or CloudFront geolocation detection.
"""
country = models.CharField(
max_length=2, choices=COUNTRY_CHOICES,
help_text=_("Select origin country of the request that this rule will "
"match against. This rule will only work if you use "
"Cloudflare or CloudFront IP geolocation or if GeoIP2 "
"module is configured.")
)
class Meta:
verbose_name = _("origin country rule")
def get_cloudflare_country(self, request):
"""
Get country code that has been detected by Cloudflare.
Guide to the functionality:
https://support.cloudflare.com/hc/en-us/articles/200168236-What-does-Cloudflare-IP-Geolocation-do-
"""
try:
return request.META['HTTP_CF_IPCOUNTRY'].lower()
except KeyError:
pass
def get_cloudfront_country(self, request):
try:
return request.META['HTTP_CLOUDFRONT_VIEWER_COUNTRY'].lower()
except KeyError:
pass
def get_geoip_country(self, request):
GeoIP2 = get_geoip_module()
if GeoIP2 is None:
return False
return GeoIP2().country_code(get_client_ip(request)).lower()
def get_country(self, request):
# Prioritise CloudFlare and CloudFront country detection over GeoIP.
functions = (
self.get_cloudflare_country,
self.get_cloudfront_country,
self.get_geoip_country,
)
for function in functions:
result = function(request)
if result:
return result
def test_user(self, request=None):
return (self.get_country(request) or '') == self.country.lower()

View File

@ -37,6 +37,8 @@
/******/ 3: 0 /******/ 3: 0
/******/ }; /******/ };
/******/ /******/
/******/ var resolvedPromise = new Promise(function(resolve) { resolve(); });
/******/
/******/ // The require function /******/ // The require function
/******/ function __webpack_require__(moduleId) { /******/ function __webpack_require__(moduleId) {
/******/ /******/
@ -64,21 +66,20 @@
/******/ // This file contains only the entry chunk. /******/ // This file contains only the entry chunk.
/******/ // The chunk loading function for additional chunks /******/ // The chunk loading function for additional chunks
/******/ __webpack_require__.e = function requireEnsure(chunkId) { /******/ __webpack_require__.e = function requireEnsure(chunkId) {
/******/ var installedChunkData = installedChunks[chunkId]; /******/ if(installedChunks[chunkId] === 0) {
/******/ if(installedChunkData === 0) { /******/ return resolvedPromise;
/******/ return new Promise(function(resolve) { resolve(); });
/******/ } /******/ }
/******/ /******/
/******/ // a Promise means "currently loading". /******/ // a Promise means "currently loading".
/******/ if(installedChunkData) { /******/ if(installedChunks[chunkId]) {
/******/ return installedChunkData[2]; /******/ return installedChunks[chunkId][2];
/******/ } /******/ }
/******/ /******/
/******/ // setup Promise in chunk cache /******/ // setup Promise in chunk cache
/******/ var promise = new Promise(function(resolve, reject) { /******/ var promise = new Promise(function(resolve, reject) {
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject]; /******/ installedChunks[chunkId] = [resolve, reject];
/******/ }); /******/ });
/******/ installedChunkData[2] = promise; /******/ installedChunks[chunkId][2] = promise;
/******/ /******/
/******/ // start chunk loading /******/ // start chunk loading
/******/ var head = document.getElementsByTagName('head')[0]; /******/ var head = document.getElementsByTagName('head')[0];

File diff suppressed because one or more lines are too long

View File

@ -22,7 +22,7 @@
<div class="nice-padding block_container"> <div class="nice-padding block_container">
{% if all_count %} {% if all_count %}
{% for segment in object_list %} {% for segment in object_list %}
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'"> <div class="block block--{{ segment.enabled|yesno:"enabled,disabled" }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
<h2>{{ segment }}</h2> <h2>{{ segment }}</h2>
<div class="inspect_container"> <div class="inspect_container">
@ -98,10 +98,10 @@
{% if user_can_create %} {% if user_can_create %}
<ul class="block_actions"> <ul class="block_actions">
{% if segment.status == segment.STATUS_DISABLED %} {% if segment.enabled %}
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
{% elif segment.status == segment.STATUS_ENABLED %}
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li> <li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
{% else %}
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
{% endif %} {% endif %}
<li><a href="{% url 'wagtail_personalisation_segment_modeladmin_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 %} {% if segment.is_static %}

View File

@ -1,10 +1,8 @@
import time import time
from django.conf import settings
from django.db.models import F from django.db.models import F
from django.template.base import FilterExpression, kwarg_re from django.template.base import FilterExpression, kwarg_re
from django.utils import timezone from django.utils import timezone
from django.utils.module_loading import import_string
def impersonate_other_page(page, other_page): def impersonate_other_page(page, other_page):
@ -118,17 +116,3 @@ def can_delete_pages(pages, user):
if not variant.permissions_for_user(user).can_delete(): if not variant.permissions_for_user(user).can_delete():
return False return False
return True return True
def get_client_ip(request):
try:
func = import_string(settings.WAGTAIL_PERSONALISATION_IP_FUNCTION)
except AttributeError:
pass
else:
return func(request)
try:
x_forwarded_for = request.META['HTTP_X_FORWARDED_FOR']
return x_forwarded_for.split(',')[-1].strip()
except KeyError:
return request.META['REMOTE_ADDR']

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, unicode_literals
import csv import csv
from django import forms from django import forms
@ -81,14 +83,11 @@ class SegmentModelAdmin(ModelAdmin):
delete_view_class = SegmentModelDeleteView delete_view_class = SegmentModelDeleteView
menu_icon = 'fa-snowflake-o' menu_icon = 'fa-snowflake-o'
add_to_settings_menu = False add_to_settings_menu = False
list_display = ('name', 'persistent', 'match_any', 'status', list_display = ('name', 'persistent', 'match_any', 'enabled',
'page_count', 'variant_count', 'statistics') 'page_count', 'variant_count', 'statistics')
index_view_extra_js = ['js/commons.js', 'js/index.js'] index_view_extra_js = ['js/commons.js', 'js/index.js']
index_view_extra_css = ['css/index.css'] index_view_extra_css = ['css/index.css']
form_view_extra_js = ['js/commons.js', 'js/form.js', form_view_extra_js = ['js/commons.js', 'js/form.js']
'js/segment_form_control.js',
'wagtailadmin/js/page-chooser-modal.js',
'wagtailadmin/js/page-chooser.js']
form_view_extra_css = ['css/form.css'] form_view_extra_css = ['css/form.css']
def index_view(self, request): def index_view(self, request):

View File

@ -1,9 +1,9 @@
from __future__ import absolute_import, unicode_literals
import logging import logging
from django.conf.urls import include, url from django.conf.urls import include, url
from django.db import transaction from django.db import transaction
from django.db.models import F
from django.http import Http404
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize
from django.urls import reverse from django.urls import reverse
@ -11,35 +11,27 @@ from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from wagtail.admin import messages from wagtail.admin import messages
from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem from wagtail.admin.site_summary import PagesSummaryItem, SummaryItem
from wagtail.admin.views.pages import get_valid_next_url_from_request
try:
from wagtail.admin.views.pages.utils import get_valid_next_url_from_request
except ModuleNotFoundError:
from wagtail.admin.views.pages import get_valid_next_url_from_request # noqa
from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook from wagtail.admin.widgets import Button, ButtonWithDropdownFromHook
from wagtail.core import hooks from wagtail.core import hooks
from wagtail.core.models import Page from wagtail.core.models import Page
from wagtail_personalisation import admin_urls, models, utils from wagtail_personalisation import admin_urls, models, utils
from wagtail_personalisation.adapters import get_segment_adapter from wagtail_personalisation.adapters import get_segment_adapter
from wagtail_personalisation.models import PersonalisablePageMetadata
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@hooks.register("register_admin_urls") @hooks.register('register_admin_urls')
def register_admin_urls(): def register_admin_urls():
"""Adds the administration urls for the personalisation apps.""" """Adds the administration urls for the personalisation apps."""
return [ return [
url( url(r'^personalisation/', include(
r"^personalisation/", admin_urls, namespace='wagtail_personalisation')),
include(admin_urls, namespace="wagtail_personalisation"),
)
] ]
@hooks.register("before_serve_page") @hooks.register('before_serve_page')
def set_visit_count(page, request, serve_args, serve_kwargs): def set_visit_count(page, request, serve_args, serve_kwargs):
"""Tests the provided rules to see if the request still belongs """Tests the provided rules to see if the request still belongs
to a segment. to a segment.
@ -54,7 +46,7 @@ def set_visit_count(page, request, serve_args, serve_kwargs):
adapter.add_page_visit(page) adapter.add_page_visit(page)
@hooks.register("before_serve_page") @hooks.register('before_serve_page')
def segment_user(page, request, serve_args, serve_kwargs): def segment_user(page, request, serve_args, serve_kwargs):
"""Apply a segment to a visitor before serving the page. """Apply a segment to a visitor before serving the page.
@ -67,7 +59,7 @@ def segment_user(page, request, serve_args, serve_kwargs):
adapter = get_segment_adapter(request) adapter = get_segment_adapter(request)
adapter.refresh() adapter.refresh()
forced_segment = request.GET.get("segment", None) forced_segment = request.GET.get('segment', None)
if request.user.is_superuser and forced_segment is not None: if request.user.is_superuser and forced_segment is not None:
segment = models.Segment.objects.filter(pk=forced_segment).first() segment = models.Segment.objects.filter(pk=forced_segment).first()
if segment: if segment:
@ -85,14 +77,14 @@ class UserbarSegmentedLinkItem:
Show as segment: {self.segment.name}</a></div>""" Show as segment: {self.segment.name}</a></div>"""
@hooks.register("construct_wagtail_userbar") @hooks.register('construct_wagtail_userbar')
def add_segment_link_items(request, items): def add_segment_link_items(request, items):
for item in models.Segment.objects.enabled(): for item in models.Segment.objects.enabled():
items.append(UserbarSegmentedLinkItem(item)) items.append(UserbarSegmentedLinkItem(item))
return items return items
@hooks.register("before_serve_page") @hooks.register('before_serve_page')
def serve_variant(page, request, serve_args, serve_kwargs): def serve_variant(page, request, serve_args, serve_kwargs):
"""Apply a segment to a visitor before serving the page. """Apply a segment to a visitor before serving the page.
@ -112,13 +104,9 @@ def serve_variant(page, request, serve_args, serve_kwargs):
adapter = get_segment_adapter(request) adapter = get_segment_adapter(request)
user_segments = adapter.get_segments() user_segments = adapter.get_segments()
metadata = page.personalisation_metadata
# If page is not canonical, don't serve it.
if not metadata.is_canonical:
raise Http404
if user_segments: if user_segments:
metadata = page.personalisation_metadata
# TODO: This is never more then one page? (fix query count) # TODO: This is never more then one page? (fix query count)
metadata = metadata.metadata_for_segments(user_segments) metadata = metadata.metadata_for_segments(user_segments)
if metadata: if metadata:
@ -126,13 +114,13 @@ def serve_variant(page, request, serve_args, serve_kwargs):
return variant.serve(request, *serve_args, **serve_kwargs) return variant.serve(request, *serve_args, **serve_kwargs)
@hooks.register("construct_explorer_page_queryset") @hooks.register('construct_explorer_page_queryset')
def dont_show_variant(parent_page, pages, request): def dont_show_variant(parent_page, pages, request):
return utils.exclude_variants(pages) return utils.exclude_variants(pages)
@hooks.register("register_page_listing_buttons") @hooks.register('register_page_listing_buttons')
def page_listing_variant_buttons(page, page_perms, is_parent=False, *args): def page_listing_variant_buttons(page, page_perms, is_parent=False):
"""Adds page listing buttons to personalisable pages. Shows variants for """Adds page listing buttons to personalisable pages. Shows variants for
the page (if any) and a 'Create a new variant' button. the page (if any) and a 'Create a new variant' button.
@ -143,18 +131,17 @@ def page_listing_variant_buttons(page, page_perms, is_parent=False, *args):
metadata = page.personalisation_metadata metadata = page.personalisation_metadata
if metadata.is_canonical: if metadata.is_canonical:
yield ButtonWithDropdownFromHook( yield ButtonWithDropdownFromHook(
_("Variants"), _('Variants'),
hook_name="register_page_listing_variant_buttons", hook_name='register_page_listing_variant_buttons',
page=page, page=page,
page_perms=page_perms, page_perms=page_perms,
is_parent=is_parent, is_parent=is_parent,
attrs={"target": "_blank", "title": _("Create or edit a variant")}, attrs={'target': '_blank', 'title': _('Create or edit a variant')},
priority=100, priority=100)
)
@hooks.register("register_page_listing_variant_buttons") @hooks.register('register_page_listing_variant_buttons')
def page_listing_more_buttons(page, page_perms, is_parent=False, *args): def page_listing_more_buttons(page, page_perms, is_parent=False):
"""Adds a 'more' button to personalisable pages allowing users to quickly """Adds a 'more' button to personalisable pages allowing users to quickly
create a new variant for the selected segment. create a new variant for the selected segment.
@ -165,30 +152,24 @@ def page_listing_more_buttons(page, page_perms, is_parent=False, *args):
metadata = page.personalisation_metadata metadata = page.personalisation_metadata
for vm in metadata.variants_metadata: for vm in metadata.variants_metadata:
yield Button( yield Button('%s variant' % (vm.segment.name),
"%s variant" % (vm.segment.name), reverse('wagtailadmin_pages:edit', args=[vm.variant_id]),
reverse("wagtailadmin_pages:edit", args=[vm.variant_id]), attrs={"title": _('Edit this variant')},
attrs={"title": _("Edit this variant")}, classes=("icon", "icon-fa-pencil"),
classes=("icon", "icon-fa-pencil"), priority=0)
priority=0,
)
for segment in metadata.get_unused_segments(): for segment in metadata.get_unused_segments():
yield Button( yield Button('%s variant' % (segment.name),
"%s variant" % (segment.name), reverse('segment:copy_page', args=[page.pk, segment.pk]),
reverse("segment:copy_page", args=[page.pk, segment.pk]), attrs={"title": _('Create this variant')},
attrs={"title": _("Create this variant")}, classes=("icon", "icon-fa-plus"),
classes=("icon", "icon-fa-plus"), priority=100)
priority=100,
)
yield Button( yield Button(_('Create a new segment'),
_("Create a new segment"), reverse('wagtail_personalisation_segment_modeladmin_create'),
reverse("wagtail_personalisation_segment_modeladmin_create"), attrs={"title": _('Create a new segment')},
attrs={"title": _("Create a new segment")}, classes=("icon", "icon-fa-snowflake-o"),
classes=("icon", "icon-fa-snowflake-o"), priority=200)
priority=200,
)
class CorrectedPagesSummaryItem(PagesSummaryItem): class CorrectedPagesSummaryItem(PagesSummaryItem):
@ -198,22 +179,21 @@ class CorrectedPagesSummaryItem(PagesSummaryItem):
# The `PagesSummaryItem` will return a page count of 0 otherwise. # The `PagesSummaryItem` will return a page count of 0 otherwise.
# https://github.com/wagtail/wagtail/blob/5c9ff23e229acabad406c42c4e13cbaea32e6c15/wagtail/admin/site_summary.py#L38 # https://github.com/wagtail/wagtail/blob/5c9ff23e229acabad406c42c4e13cbaea32e6c15/wagtail/admin/site_summary.py#L38
context = super().get_context() context = super().get_context()
root_page = context.get("root_page", None) root_page = context.get('root_page', None)
if root_page: if root_page:
pages = utils.exclude_variants( pages = utils.exclude_variants(
Page.objects.descendant_of(root_page, inclusive=True) Page.objects.descendant_of(root_page, inclusive=True))
)
page_count = pages.count() page_count = pages.count()
if root_page.is_root(): if root_page.is_root():
page_count -= 1 page_count -= 1
context["total_pages"] = page_count context['total_pages'] = page_count
return context return context
@hooks.register("construct_homepage_summary_items") @hooks.register('construct_homepage_summary_items')
def add_corrected_pages_summary_panel(request, items): def add_corrected_pages_summary_panel(request, items):
"""Replaces the Pages summary panel to hide variants.""" """Replaces the Pages summary panel to hide variants."""
for index, item in enumerate(items): for index, item in enumerate(items):
@ -226,21 +206,16 @@ class SegmentSummaryPanel(SummaryItem):
site and allowing quick access to the Segment dashboard. site and allowing quick access to the Segment dashboard.
""" """
order = 2000 order = 2000
def render(self): def render(self):
segment_count = models.Segment.objects.count() segment_count = models.Segment.objects.count()
target_url = reverse("wagtail_personalisation_segment_modeladmin_index") target_url = reverse('wagtail_personalisation_segment_modeladmin_index')
title = _("Segments") title = _("Segments")
return mark_safe( return mark_safe("""
"""
<li class="icon icon-fa-snowflake-o"> <li class="icon icon-fa-snowflake-o">
<a href="{}"><span>{}</span>{}</a> <a href="{}"><span>{}</span>{}</a>
</li>""".format( </li>""".format(target_url, segment_count, title))
target_url, segment_count, title
)
)
class PersonalisedPagesSummaryPanel(PagesSummaryItem): class PersonalisedPagesSummaryPanel(PagesSummaryItem):
@ -248,17 +223,12 @@ class PersonalisedPagesSummaryPanel(PagesSummaryItem):
def render(self): def render(self):
page_count = models.PersonalisablePageMetadata.objects.filter( page_count = models.PersonalisablePageMetadata.objects.filter(
segment__isnull=True segment__isnull=True).count()
).count()
title = _("Personalised Page") title = _("Personalised Page")
return mark_safe( return mark_safe("""
"""
<li class="icon icon-fa-file-o"> <li class="icon icon-fa-file-o">
<span>{}</span>{}{} <span>{}</span>{}{}
</li>""".format( </li>""".format(page_count, title, pluralize(page_count)))
page_count, title, pluralize(page_count)
)
)
class VariantPagesSummaryPanel(PagesSummaryItem): class VariantPagesSummaryPanel(PagesSummaryItem):
@ -266,20 +236,15 @@ class VariantPagesSummaryPanel(PagesSummaryItem):
def render(self): def render(self):
page_count = models.PersonalisablePageMetadata.objects.filter( page_count = models.PersonalisablePageMetadata.objects.filter(
segment__isnull=False segment__isnull=False).count()
).count()
title = _("Variant") title = _("Variant")
return mark_safe( return mark_safe("""
"""
<li class="icon icon-fa-files-o"> <li class="icon icon-fa-files-o">
<span>{}</span>{}{} <span>{}</span>{}{}
</li>""".format( </li>""".format(page_count, title, pluralize(page_count)))
page_count, title, pluralize(page_count)
)
)
@hooks.register("construct_homepage_summary_items") @hooks.register('construct_homepage_summary_items')
def add_personalisation_summary_panels(request, items): def add_personalisation_summary_panels(request, items):
"""Adds a summary panel to the Wagtail dashboard showing the total amount """Adds a summary panel to the Wagtail dashboard showing the total amount
of segments on the site and allowing quick access to the Segment of segments on the site and allowing quick access to the Segment
@ -291,61 +256,52 @@ def add_personalisation_summary_panels(request, items):
items.append(VariantPagesSummaryPanel(request)) items.append(VariantPagesSummaryPanel(request))
@hooks.register("before_delete_page") @hooks.register('before_delete_page')
def delete_related_variants(request, page): def delete_related_variants(request, page):
if ( if not isinstance(page, models.PersonalisablePageMixin) \
not isinstance(page, models.PersonalisablePageMixin) or not page.personalisation_metadata.is_canonical:
or not page.personalisation_metadata.is_canonical
):
return return
# Get a list of related personalisation metadata for all the related # Get a list of related personalisation metadata for all the related
# variants. # variants.
variants_metadata = page.personalisation_metadata.variants_metadata.select_related( variants_metadata = (
"variant" page.personalisation_metadata.variants_metadata
.select_related('variant')
) )
next_url = get_valid_next_url_from_request(request) next_url = get_valid_next_url_from_request(request)
if request.method == "POST": if request.method == 'POST':
parent_id = page.get_parent().id parent_id = page.get_parent().id
variants_metadata = variants_metadata.select_related('variant')
with transaction.atomic(): with transaction.atomic():
# To ensure variants are deleted for all descendants, start with for metadata in variants_metadata.iterator():
# the deepest ones, and explicitly delete variants and metadata # Call delete() on objects to trigger any signals or hooks.
# for all of them, including the page itself. Otherwise protected metadata.variant.delete()
# foreign key constraints are violated. Only consider canonical # Delete the page's main variant and the page itself.
# pages. page.personalisation_metadata.delete()
for metadata in PersonalisablePageMetadata.objects.filter( page.delete()
canonical_page__in=page.get_descendants(inclusive=True),
variant=F("canonical_page"),
).order_by("-canonical_page__depth"):
for variant_metadata in metadata.variants_metadata.select_related(
"variant"
):
# Call delete() on objects to trigger any signals or hooks.
variant_metadata.variant.delete()
metadata.delete()
metadata.canonical_page.delete()
msg = _("Page '{0}' and its variants deleted.") msg = _("Page '{0}' and its variants deleted.")
messages.success(request, msg.format(page.get_admin_display_title())) messages.success(
request,
msg.format(page.get_admin_display_title())
)
for fn in hooks.get_hooks("after_delete_page"): for fn in hooks.get_hooks('after_delete_page'):
result = fn(request, page) result = fn(request, page)
if hasattr(result, "status_code"): if hasattr(result, 'status_code'):
return result return result
if next_url: if next_url:
return redirect(next_url) return redirect(next_url)
return redirect("wagtailadmin_explore", parent_id) return redirect('wagtailadmin_explore', parent_id)
return render( return render(
request, request,
"wagtailadmin/pages/wagtail_personalisation/confirm_delete.html", 'wagtailadmin/pages/wagtail_personalisation/confirm_delete.html', {
{ 'page': page,
"page": page, 'descendant_count': page.get_descendant_count(),
"descendant_count": page.get_descendant_count(), 'next': next_url,
"next": next_url, 'variants': Page.objects.filter(
"variants": Page.objects.filter( pk__in=variants_metadata.values_list('variant_id')
pk__in=variants_metadata.values_list("variant_id") )
), }
},
) )

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, unicode_literals
import pytest import pytest
pytest_plugins = [ pytest_plugins = [
@ -5,11 +7,6 @@ pytest_plugins = [
] ]
@pytest.fixture(autouse=True)
def enable_db_access(db):
pass
@pytest.fixture(scope='session') @pytest.fixture(scope='session')
def django_db_setup(django_db_setup, django_db_blocker): def django_db_setup(django_db_setup, django_db_blocker):
from wagtail.core.models import Page, Site from wagtail.core.models import Page, Site

View File

@ -5,18 +5,7 @@ from django.utils.text import slugify
from wagtail_factories.factories import PageFactory from wagtail_factories.factories import PageFactory
from tests.site.pages import models from tests.site.pages import models
from wagtail_personalisation.models import PersonalisablePageMetadata
try:
from wagtail.core.models import Locale
class LocaleFactory(factory.DjangoModelFactory):
language_code = "en"
class Meta:
model = Locale
except ImportError:
pass
class ContentPageFactory(PageFactory): class ContentPageFactory(PageFactory):
parent = None parent = None
@ -33,9 +22,3 @@ class RegularPageFactory(PageFactory):
class Meta: class Meta:
model = models.RegularPage model = models.RegularPage
class PersonalisablePageMetadataFactory(factory.DjangoModelFactory):
class Meta:
model = PersonalisablePageMetadata

View File

@ -46,8 +46,3 @@ class VisitCountRuleFactory(factory.DjangoModelFactory):
class Meta: class Meta:
model = rules.VisitCountRule model = rules.VisitCountRule
class OriginCountryRuleFactory(factory.DjangoModelFactory):
class Meta:
model = rules.OriginCountryRule

View File

@ -7,7 +7,7 @@ from wagtail_personalisation import models
class SegmentFactory(factory.DjangoModelFactory): class SegmentFactory(factory.DjangoModelFactory):
name = 'TestSegment' name = 'TestSegment'
status = models.Segment.STATUS_ENABLED enabled = True
class Meta: class Meta:
model = models.Segment model = models.Segment

View File

@ -23,7 +23,7 @@ def site():
return site return site
@pytest.fixture() @pytest.fixture
def segmented_page(site): def segmented_page(site):
page = ContentPageFactory(parent=site.root_page, slug='personalised') page = ContentPageFactory(parent=site.root_page, slug='personalised')
segment = SegmentFactory() segment = SegmentFactory()
@ -46,6 +46,6 @@ class RequestFactory(BaseRequestFactory):
return request return request
@pytest.fixture() @pytest.fixture
def user(django_user_model): def user(django_user_model):
return django_user_model.objects.create(username='user') return django_user_model.objects.create(username='user')

View File

@ -1,5 +1,6 @@
from __future__ import absolute_import, unicode_literals
import os import os
from importlib.util import find_spec
DATABASES = { DATABASES = {
'default': { 'default': {
@ -51,7 +52,6 @@ TEMPLATES = [
}, },
] ]
MIDDLEWARE = ( MIDDLEWARE = (
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@ -59,14 +59,10 @@ MIDDLEWARE = (
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.core.middleware.SiteMiddleware',
) )
if find_spec('wagtail.contrib.legacy'):
MIDDLEWARE += ('wagtail.contrib.legacy.sitemiddleware.SiteMiddleware',)
else:
MIDDLEWARE += ('wagtail.core.middleware.SiteMiddleware', )
INSTALLED_APPS = ( INSTALLED_APPS = (
'wagtail_personalisation', 'wagtail_personalisation',

View File

@ -64,7 +64,7 @@ def test_refresh_removes_disabled(rf):
adapter.set_segments([segment_1, segment_2]) adapter.set_segments([segment_1, segment_2])
adapter = adapters.SessionSegmentsAdapter(request) adapter = adapters.SessionSegmentsAdapter(request)
segment_1.status = segment_1.STATUS_DISABLED segment_1.enabled = False
segment_1.save() segment_1.save()
adapter.refresh() adapter.refresh()

View File

@ -15,14 +15,14 @@ from wagtail_personalisation.rules import TimeRule
@pytest.mark.django_db @pytest.mark.django_db
def test_segment_create(): def test_segment_create():
factoried_segment = SegmentFactory() factoried_segment = SegmentFactory()
segment = Segment(name='TestSegment', status='enabled') segment = Segment(name='TestSegment', enabled=True)
TimeRule( TimeRule(
start_time=datetime.time(8, 0, 0), start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0), end_time=datetime.time(23, 0, 0),
segment=segment) segment=segment)
assert factoried_segment.name == segment.name assert factoried_segment.name == segment.name
assert factoried_segment.status == segment.status assert factoried_segment.enabled == segment.enabled
@pytest.mark.django_db @pytest.mark.django_db

View File

@ -1,3 +1,5 @@
from __future__ import absolute_import, unicode_literals
import datetime import datetime
import pytest import pytest
@ -6,7 +8,7 @@ from django.db.models import ProtectedError
from tests.factories.page import ContentPageFactory from tests.factories.page import ContentPageFactory
from tests.factories.segment import SegmentFactory from tests.factories.segment import SegmentFactory
from tests.site.pages import models from tests.site.pages import models
from wagtail_personalisation.models import PersonalisablePageMetadata, Segment from wagtail_personalisation.models import PersonalisablePageMetadata
from wagtail_personalisation.rules import TimeRule from wagtail_personalisation.rules import TimeRule
@ -58,26 +60,3 @@ def test_page_protection_when_deleting_segment(segmented_page):
assert len(segment.get_used_pages()) assert len(segment.get_used_pages())
with pytest.raises(ProtectedError): with pytest.raises(ProtectedError):
segment.delete() segment.delete()
@pytest.mark.django_db
def test_sitemap_generation_for_canonical_pages_is_enabled(segmented_page):
canonical = segmented_page.personalisation_metadata.canonical_page
assert canonical.personalisation_metadata.is_canonical
assert canonical.get_sitemap_urls()
@pytest.mark.django_db
def test_sitemap_generation_for_variants_is_disabled(segmented_page):
assert not segmented_page.personalisation_metadata.is_canonical
assert not segmented_page.get_sitemap_urls()
@pytest.mark.django_db
def test_segment_edit_view(site, client, django_user_model):
test_segment = SegmentFactory()
try:
new_panel = test_segment.panels[1].children[0].bind_to(model=Segment)
except AttributeError:
new_panel = test_segment.panels[1].children[0].bind_to_model(Segment)
assert new_panel.related.name == "wagtail_personalisation_timerules"

View File

@ -1,202 +0,0 @@
from importlib.util import find_spec
from unittest.mock import MagicMock, call, patch
import pytest
from tests.factories.rule import OriginCountryRuleFactory
from tests.factories.segment import SegmentFactory
from wagtail_personalisation.rules import get_geoip_module
skip_if_geoip2_installed = pytest.mark.skipif(
find_spec('geoip2'), reason='requires GeoIP2 to be not installed'
)
skip_if_geoip2_not_installed = pytest.mark.skipif(
not find_spec('geoip2'), reason='requires GeoIP2 to be installed.'
)
@pytest.mark.django_db
def test_get_cloudflare_country_with_header(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/', HTTP_CF_IPCOUNTRY='PL')
assert rule.get_cloudflare_country(request) == 'pl'
@pytest.mark.django_db
def test_get_cloudflare_country_with_no_header(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/')
assert 'HTTP_CF_IPCOUNTRY' not in request.META
assert rule.get_cloudflare_country(request) is None
@pytest.mark.django_db
def test_get_cloudfront_country_with_header(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/', HTTP_CLOUDFRONT_VIEWER_COUNTRY='BY')
assert rule.get_cloudfront_country(request) == 'by'
@pytest.mark.django_db
def test_get_cloudfront_country_with_no_header(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/')
assert 'HTTP_CLOUDFRONT_VIEWER_COUNTRY' not in request.META
assert rule.get_cloudfront_country(request) is None
@pytest.mark.django_db
def test_get_geoip_country_with_remote_addr(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/', REMOTE_ADDR='173.254.89.34')
geoip_mock = MagicMock()
with patch('wagtail_personalisation.rules.get_geoip_module',
return_value=geoip_mock) as geoip_import_mock:
rule.get_geoip_country(request)
geoip_import_mock.assert_called_once()
geoip_mock.assert_called_once()
assert geoip_mock.mock_calls[1] == call().country_code('173.254.89.34')
@pytest.mark.django_db
def test_get_country_calls_all_methods(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/')
@patch.object(rule, 'get_geoip_country', return_value='')
@patch.object(rule, 'get_cloudflare_country', return_value='')
@patch.object(rule, 'get_cloudfront_country', return_value='')
def test_mock(cloudfront_mock, cloudflare_mock, geoip_mock):
country = rule.get_country(request)
cloudflare_mock.assert_called_once_with(request)
cloudfront_mock.assert_called_once_with(request)
geoip_mock.assert_called_once_with(request)
assert country is None
test_mock()
@pytest.mark.django_db
def test_get_country_does_not_use_all_detection_methods_unnecessarily(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/')
@patch.object(rule, 'get_geoip_country', return_value='')
@patch.object(rule, 'get_cloudflare_country', return_value='t1')
@patch.object(rule, 'get_cloudfront_country', return_value='')
def test_mock(cloudfront_mock, cloudflare_mock, geoip_mock):
country = rule.get_country(request)
cloudflare_mock.assert_called_once_with(request)
cloudfront_mock.assert_not_called()
geoip_mock.assert_not_called()
assert country == 't1'
test_mock()
@pytest.mark.django_db
def test_test_user_calls_get_country(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/')
with patch.object(rule, 'get_country') as get_country_mock:
rule.test_user(request)
get_country_mock.assert_called_once_with(request)
@pytest.mark.django_db
def test_test_user_returns_true_if_cloudflare_country_match(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/', HTTP_CF_IPCOUNTRY='GB')
assert rule.test_user(request) is True
@pytest.mark.django_db
def test_test_user_returns_false_if_cloudflare_country_doesnt_match(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/', HTTP_CF_IPCOUNTRY='NL')
assert not rule.test_user(request)
@pytest.mark.django_db
def test_test_user_returns_false_if_cloudfront_country_doesnt_match(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='GB')
request = rf.get('/', HTTP_CLOUDFRONT_VIEWER_COUNTRY='NL')
assert rule.test_user(request) is False
@pytest.mark.django_db
def test_test_user_returns_true_if_cloudfront_country_matches(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='se')
request = rf.get('/', HTTP_CLOUDFRONT_VIEWER_COUNTRY='SE')
assert rule.test_user(request) is True
@skip_if_geoip2_not_installed
@pytest.mark.django_db
def test_test_user_geoip_module_matches(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='se')
request = rf.get('/', REMOTE_ADDR='123.120.0.2')
GeoIP2Mock = MagicMock()
GeoIP2Mock().configure_mock(**{'country_code.return_value': 'SE'})
GeoIP2Mock.reset_mock()
with patch('wagtail_personalisation.rules.get_geoip_module',
return_value=GeoIP2Mock):
assert rule.test_user(request) is True
assert GeoIP2Mock.mock_calls == [
call(),
call().country_code('123.120.0.2'),
]
@skip_if_geoip2_not_installed
@pytest.mark.django_db
def test_test_user_geoip_module_does_not_match(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='nl')
request = rf.get('/', REMOTE_ADDR='123.120.0.2')
GeoIP2Mock = MagicMock()
GeoIP2Mock().configure_mock(**{'country_code.return_value': 'SE'})
GeoIP2Mock.reset_mock()
with patch('wagtail_personalisation.rules.get_geoip_module',
return_value=GeoIP2Mock):
assert rule.test_user(request) is False
assert GeoIP2Mock.mock_calls == [
call(),
call().country_code('123.120.0.2')
]
@skip_if_geoip2_installed
@pytest.mark.django_db
def test_test_user_does_not_use_geoip_module_if_disabled(rf):
segment = SegmentFactory(name='Test segment')
rule = OriginCountryRuleFactory(segment=segment, country='se')
request = rf.get('/', REMOTE_ADDR='123.120.0.2')
assert rule.test_user(request) is False
@skip_if_geoip2_installed
def test_get_geoip_module_disabled():
with pytest.raises(ImportError):
from django.contrib.gis.geoip2 import GeoIP2 # noqa
assert get_geoip_module() is None
@skip_if_geoip2_not_installed
def test_get_geoip_module_enabled():
from django.contrib.gis.geoip2 import GeoIP2
assert get_geoip_module() is GeoIP2

View File

@ -12,7 +12,7 @@ from wagtail_personalisation.rules import TimeRule, VisitCountRule
def form_with_data(segment, *rules): def form_with_data(segment, *rules):
model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent'] model_fields = ['type', 'enabled', 'count', 'name', 'match_any', 'randomisation_percent']
class TestSegmentAdminForm(SegmentAdminForm): class TestSegmentAdminForm(SegmentAdminForm):
class Meta: class Meta:
@ -487,7 +487,7 @@ def test_count_users_matching_static_rules(site, client, mocker, django_user_mod
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True) mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
assert form.count_matching_users([rule], True) == 2 assert form.count_matching_users([rule], True) is 2
@pytest.mark.django_db @pytest.mark.django_db
@ -500,7 +500,7 @@ def test_count_matching_users_excludes_staff(site, client, mocker, django_user_m
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True) mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
assert form.count_matching_users([rule], True) == 1 assert form.count_matching_users([rule], True) is 1
assert mock_test_user.call_count == 1 assert mock_test_user.call_count == 1
@ -514,7 +514,7 @@ def test_count_matching_users_excludes_inactive(site, client, mocker, django_use
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True) mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
assert form.count_matching_users([rule], True) == 1 assert form.count_matching_users([rule], True) is 1
assert mock_test_user.call_count == 1 assert mock_test_user.call_count == 1
@ -532,7 +532,7 @@ def test_count_matching_users_only_counts_static_rules(site, client, mocker, dja
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
mock_test_user = mocker.patch('wagtail_personalisation.rules.TimeRule.test_user') mock_test_user = mocker.patch('wagtail_personalisation.rules.TimeRule.test_user')
assert form.count_matching_users([rule], True) == 0 assert form.count_matching_users([rule], True) is 0
assert mock_test_user.call_count == 0 assert mock_test_user.call_count == 0
@ -551,7 +551,7 @@ def test_count_matching_users_handles_match_any(site, client, mocker, django_use
'wagtail_personalisation.rules.VisitCountRule.test_user', 'wagtail_personalisation.rules.VisitCountRule.test_user',
side_effect=[True, False, True, False]) side_effect=[True, False, True, False])
assert form.count_matching_users([first_rule, second_rule], True) == 2 assert form.count_matching_users([first_rule, second_rule], True) is 2
mock_test_user.call_count == 4 mock_test_user.call_count == 4
@ -570,5 +570,5 @@ def test_count_matching_users_handles_match_all(site, client, mocker, django_use
'wagtail_personalisation.rules.VisitCountRule.test_user', 'wagtail_personalisation.rules.VisitCountRule.test_user',
side_effect=[True, True, False, True]) side_effect=[True, True, False, True])
assert form.count_matching_users([first_rule, second_rule], False) == 1 assert form.count_matching_users([first_rule, second_rule], False) is 1
mock_test_user.call_count == 4 mock_test_user.call_count == 4

View File

@ -1,19 +1,8 @@
import pytest import pytest
from django.test import override_settings
from wagtail.core.models import Page as WagtailPage
from tests.factories.page import (ContentPageFactory, PersonalisablePageMetadataFactory) from tests.factories.page import ContentPageFactory
from wagtail_personalisation.utils import ( from wagtail_personalisation.utils import (
can_delete_pages, exclude_variants, get_client_ip, impersonate_other_page) can_delete_pages, impersonate_other_page)
locale_factory = False
try:
from tests.factories.page import LocaleFactory
locale_factory = True
except ImportError:
pass
@pytest.fixture @pytest.fixture
@ -47,83 +36,3 @@ def test_can_delete_pages_with_superuser(rf, user, segmented_page):
@pytest.mark.django_db @pytest.mark.django_db
def test_cannot_delete_pages_with_standard_user(user, segmented_page): def test_cannot_delete_pages_with_standard_user(user, segmented_page):
assert not can_delete_pages([segmented_page], user) assert not can_delete_pages([segmented_page], user)
def test_get_client_ip_with_remote_addr(rf):
request = rf.get('/', REMOTE_ADDR='173.231.235.87')
assert get_client_ip(request) == '173.231.235.87'
def test_get_client_ip_with_x_forwarded_for(rf):
request = rf.get('/', HTTP_X_FORWARDED_FOR='173.231.235.87',
REMOTE_ADDR='10.0.23.24')
assert get_client_ip(request) == '173.231.235.87'
@override_settings(
WAGTAIL_PERSONALISATION_IP_FUNCTION='some.non.existent.path'
)
def test_get_client_ip_custom_get_client_ip_function_does_not_exist(rf):
with pytest.raises(ImportError):
get_client_ip(rf.get('/'))
@override_settings(
WAGTAIL_PERSONALISATION_IP_FUNCTION='tests.utils.get_custom_ip'
)
def test_get_client_ip_custom_get_client_ip_used(rf):
assert get_client_ip(rf.get('/')) == '123.123.123.123'
def test_exclude_variants_with_pages_querysets():
'''
Test that excludes variant works for querysets
'''
for i in range(5):
page = ContentPageFactory(path="/" + str(i), depth=0, url_path="/", title="Hoi " + str(i))
page.save()
pages = WagtailPage.objects.all().specific().order_by('id')
result = exclude_variants(pages)
assert type(result) == type(pages)
assert set(result.values_list('pk', flat=True)) == set(pages.values_list('pk', flat=True))
def test_exclude_variants_with_pages_querysets_not_canonical():
'''
Test that excludes variant works for querysets with
personalisation_metadata canonical False
'''
for i in range(5):
page = ContentPageFactory(path="/" + str(i), depth=0, url_path="/", title="Hoi " + str(i))
page.save()
pages = WagtailPage.objects.all().specific().order_by('id')
# add variants
for page in pages:
variant = ContentPageFactory(title='variant %d' % page.pk)
page.personalisation_metadata = PersonalisablePageMetadataFactory(canonical_page=page, variant=variant)
page.save()
pages = WagtailPage.objects.all().specific()
result = exclude_variants(pages)
assert type(result) == type(pages)
assert result.count() < pages.count()
def test_exclude_variants_with_pages_querysets_meta_none():
'''
Test that excludes variant works for querysets with meta as none
'''
for i in range(5):
page = ContentPageFactory(path="/" + str(i), depth=0, url_path="/", title="Hoi " + str(i))
page.save()
pages = WagtailPage.objects.all().specific().order_by('id')
# add variants
for page in pages:
page.personalisation_metadata = PersonalisablePageMetadataFactory(canonical_page=page, variant=page)
page.save()
pages = WagtailPage.objects.all().specific()
result = exclude_variants(pages)
assert type(result) == type(pages)
assert set(result.values_list('pk', flat=True)) == set(pages.values_list('pk', flat=True))

View File

@ -6,7 +6,7 @@ from wagtail.core.models import Page
from wagtail_personalisation.models import Segment from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import VisitCountRule from wagtail_personalisation.rules import VisitCountRule
from wagtail_personalisation.views import ( from wagtail_personalisation.views import (
SegmentModelAdmin, SegmentModelDeleteView) SegmentModelDeleteView, SegmentModelAdmin)
@pytest.mark.django_db @pytest.mark.django_db
@ -106,5 +106,5 @@ def test_segment_delete_view_raises_permission_denied(rf, segmented_page, user):
) )
view.request = request view.request = request
message = 'User have no permission to delete variant page objects.' message = 'User have no permission to delete variant page objects.'
with pytest.raises(PermissionDenied): with pytest.raises(PermissionDenied, message=message):
view.delete_instance() view.delete_instance()

View File

@ -1,8 +1,6 @@
import pytest import pytest
from django.http import Http404
from wagtail.core.models import Page from wagtail.core.models import Page
from tests.factories.page import ContentPageFactory
from tests.factories.segment import SegmentFactory from tests.factories.segment import SegmentFactory
from wagtail_personalisation import adapters, wagtail_hooks from wagtail_personalisation import adapters, wagtail_hooks
@ -18,15 +16,6 @@ def test_serve_variant_no_variant(site, rf):
assert result is None assert result is None
@pytest.mark.django_db
def test_variant_accessed_directly_returns_404(segmented_page, rf):
request = rf.get('/')
args = tuple()
kwargs = {}
with pytest.raises(Http404):
wagtail_hooks.serve_variant(segmented_page, request, args, kwargs)
@pytest.mark.django_db @pytest.mark.django_db
def test_serve_variant_with_variant_no_segment(site, rf, segmented_page): def test_serve_variant_with_variant_no_segment(site, rf, segmented_page):
request = rf.get('/') request = rf.get('/')
@ -123,21 +112,3 @@ def test_custom_delete_page_view_deletes_variants(rf, segmented_page, user):
# Make sure all the variant pages have been deleted. # Make sure all the variant pages have been deleted.
assert not len(variants.all()) assert not len(variants.all())
assert not len(variants_metadata.all()) assert not len(variants_metadata.all())
@pytest.mark.django_db
def test_custom_delete_page_view_deletes_variants_of_child_pages(rf, segmented_page, user):
"""
Regression test for deleting pages that have children with variants
"""
post_request = rf.post('/')
user.is_superuser = True
rf.user = user
canonical_page = segmented_page.personalisation_metadata.canonical_page
# Create a child with a variant
child_page = ContentPageFactory(parent=canonical_page, slug='personalised-child')
child_page.personalisation_metadata.copy_for_segment(segmented_page.personalisation_metadata.segment)
# A ProtectedError would be raised if the bug persists
wagtail_hooks.delete_related_variants(
post_request, canonical_page
)

View File

@ -5,7 +5,3 @@ def render_template(value, **context):
template = engines['django'].from_string(value) template = engines['django'].from_string(value)
request = context.pop('request', None) request = context.pop('request', None)
return template.render(context, request) return template.render(context, request)
def get_custom_ip(request):
return '123.123.123.123'

39
tox.ini
View File

@ -1,30 +1,14 @@
[tox] [tox]
envlist = envlist = py{36}-django{20}-wagtail{20,21},lint
flake8
py{36,37,38}-dj{22}-wt{211,212,213}
py{37,38}-dj{30,31}-wt{211,212,213}
[gh-actions]
python =
3.6: py36
3.7: py37
3.8: py38
[testenv] [testenv]
basepython = basepython = python3.6
py36: python3.6 commands = coverage run --parallel -m pytest {posargs}
py37: python3.7
py38: python3.8
commands = coverage run --parallel -m pytest -rs {posargs}
extras = test extras = test
deps = deps =
dj22: Django>=2.2.8,<2.3 django20: django>=2.0,<2.1
dj30: Django>=3.0,<3.1 wagtail20: wagtail>=2.0,<2.1
dj31: Django>=3.1,<3.2 wagtail21: wagtail>=2.1,<2.2
wt211: wagtail>=2.11,<2.12
wt212: wagtail>=2.12,<2.13
wt213: wagtail>=2.13,<2.14
geoip2: geoip2
[testenv:coverage-report] [testenv:coverage-report]
basepython = python3.6 basepython = python3.6
@ -36,16 +20,7 @@ commands =
[testenv:lint] [testenv:lint]
basepython = python3.6 basepython = python3.6
deps = flake8==3.5.0 deps = flake8
commands = commands =
flake8 src tests setup.py flake8 src tests setup.py
isort -q --recursive --diff src/ tests/ isort -q --recursive --diff src/ tests/
[testenv:format]
basepython = python3.8
deps =
isort
black
skip_install = true
commands =
black --check setup.py src/wagtail_personalisation/ tests/

4773
yarn.lock

File diff suppressed because it is too large Load Diff