Compare commits
97 Commits
feature/se
...
0.15.2
Author | SHA1 | Date | |
---|---|---|---|
cb525a2b39 | |||
53880228e4 | |||
2bee66d0ae | |||
16e24b6791 | |||
477bfb9665 | |||
6108469047 | |||
686f180081 | |||
9b1dbe35cb | |||
7e0594e341 | |||
0c19456053 | |||
18140f76ab | |||
88b17ceeb8 | |||
570de7d128 | |||
b82d5165c3 | |||
8d802dbbf4 | |||
9274073c68 | |||
1f1264cf95 | |||
3f16ad686e | |||
7101b63122 | |||
ffd839159b | |||
d074ef85b9 | |||
f3e403bec6 | |||
137b5b411c | |||
39f3500813 | |||
6a6c3e8d7b | |||
336ed2317c | |||
06569a3cc1 | |||
da6e5127ed | |||
3d054ec585 | |||
43b5b62e60 | |||
40a9959680 | |||
13e13ccae5 | |||
318b65b3eb | |||
6b9b4e0af2 | |||
69a4514129 | |||
585cb0b16a | |||
4ae8a5e60b | |||
d7ad1be51f | |||
bd5b85cedb | |||
956c1bf4f5 | |||
d775ef57e6 | |||
d34c449638 | |||
23af862798 | |||
88263dea60 | |||
2e1e09f60b | |||
86e669e4f4 | |||
807005461e | |||
7f9e0971f5 | |||
a4a1a2ddca | |||
9cc6e966ba | |||
311abeb6c8 | |||
60675203c6 | |||
ceef806301 | |||
650e061f91 | |||
9235932f00 | |||
1e0efc975a | |||
7517dcd051 | |||
94c9efa315 | |||
f73e59421b | |||
f054b86e07 | |||
cbb56847ae | |||
b135e79c77 | |||
5cd8751450 | |||
c07b280276 | |||
28266c4500 | |||
94a5c6b289 | |||
875d8302de | |||
4c09ad4ca7 | |||
0d260a12a4 | |||
7888f0b615 | |||
02e63ed82c | |||
a411ad1ccc | |||
1a1df18bf3 | |||
56d28faec8 | |||
f95b8dcb93 | |||
d3f4d42d82 | |||
4c08581919 | |||
11886ae135 | |||
83cc7f790e | |||
dcdeb4e9a2 | |||
2e827be41a | |||
0f9bfb0343 | |||
1c74e6cfb9 | |||
9c45ac56db | |||
2f7b92fb2e | |||
1e69d929aa | |||
a178a8b533 | |||
f2e01c803a | |||
eb9d4f3e31 | |||
4ceb59c719 | |||
6fcab3ac11 | |||
1f464adaa7 | |||
d15f6c37d3 | |||
7d679d7111 | |||
b11a6ce4ca | |||
4e9a6e902d | |||
3ce0aef8d5 |
89
.github/workflows/python-test.yml
vendored
Normal file
89
.github/workflows/python-test.yml
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
---
|
||||
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
1
.gitignore
vendored
@ -13,6 +13,7 @@
|
||||
.vscode/
|
||||
|
||||
build/
|
||||
ve/
|
||||
dist/
|
||||
htmlcov/
|
||||
docs/_build
|
||||
|
22
.travis.yml
22
.travis.yml
@ -1,22 +0,0 @@
|
||||
---
|
||||
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
30
CHANGES
@ -1,3 +1,33 @@
|
||||
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
|
||||
==================
|
||||
- Bugfix: Handle errors when testing an invalid visit count rule
|
||||
|
@ -54,7 +54,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'wagtail-personalisation'
|
||||
copyright = '2018, Lab Digital BV'
|
||||
copyright = '2019, Lab Digital BV'
|
||||
author = 'Lab Digital BV'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
@ -62,10 +62,10 @@ author = 'Lab Digital BV'
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.12.0'
|
||||
version = '0.15.2'
|
||||
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.12.0'
|
||||
release = '0.15.2'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
|
@ -131,3 +131,47 @@ Is logged in Whether the user is logged in or logged out.
|
||||
================== ==========================================================
|
||||
|
||||
``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``
|
||||
|
@ -1,4 +1,4 @@
|
||||
Django>=2.0,<2.1
|
||||
wagtail>=2.1,<2.2
|
||||
django-debug-toolbar==1.9.1
|
||||
Django>=2.2,<2.3
|
||||
wagtail>=2.6,<2.7
|
||||
django-debug-toolbar==2.0
|
||||
-e .[docs,test]
|
||||
|
@ -0,0 +1,20 @@
|
||||
# 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()),
|
||||
],
|
||||
),
|
||||
]
|
@ -1,14 +1,44 @@
|
||||
from django.contrib.auth.models import (
|
||||
AbstractBaseUser, PermissionsMixin, UserManager)
|
||||
AbstractBaseUser, PermissionsMixin, BaseUserManager)
|
||||
from django.core.mail import send_mail
|
||||
from django.db import connections, models
|
||||
from django.dispatch import receiver
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
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):
|
||||
"""Cusomtized version of the default `AbstractUser` from Django.
|
||||
"""Customized version of the default `AbstractUser` from Django.
|
||||
|
||||
"""
|
||||
first_name = models.CharField(_('first name'), max_length=100, blank=True)
|
||||
|
@ -14,6 +14,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
from importlib.util import find_spec
|
||||
|
||||
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||
@ -78,11 +79,14 @@ MIDDLEWARE = [
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
|
||||
'wagtail.core.middleware.SiteMiddleware',
|
||||
'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'
|
||||
|
||||
TEMPLATES = [
|
||||
|
@ -11,9 +11,9 @@ from wagtail.documents import urls as wagtaildocs_urls
|
||||
from sandbox.apps.search import views as search_views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^django-admin/', admin.site.urls),
|
||||
|
||||
url(r'^cms/', include(wagtailadmin_urls)),
|
||||
url(r'^admin/', include(wagtailadmin_urls)),
|
||||
url(r'^documents/', include(wagtaildocs_urls)),
|
||||
|
||||
url(r'^search/$', search_views.search, name='search'),
|
||||
|
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.12.0
|
||||
current_version = 0.15.2
|
||||
commit = true
|
||||
tag = true
|
||||
tag_name = {new_version}
|
||||
@ -15,7 +15,7 @@ python_paths = .
|
||||
[flake8]
|
||||
ignore = E731
|
||||
max-line-length = 120
|
||||
exclude =
|
||||
exclude =
|
||||
src/**/migrations/*.py
|
||||
|
||||
[wheel]
|
||||
@ -28,4 +28,3 @@ omit = src/**/migrations/*.py
|
||||
[bumpversion:file:setup.py]
|
||||
|
||||
[bumpversion:file:docs/conf.py]
|
||||
|
||||
|
17
setup.py
17
setup.py
@ -2,24 +2,25 @@ import re
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
install_requires = [
|
||||
'wagtail>=2.0,<2.2',
|
||||
'wagtail>=2.0',
|
||||
'user-agents>=1.1.0',
|
||||
'wagtailfontawesome>=1.1.3',
|
||||
'pycountry',
|
||||
]
|
||||
|
||||
tests_require = [
|
||||
'factory_boy==2.8.1',
|
||||
'flake8-blind-except',
|
||||
'flake8-debugger',
|
||||
'flake8-imports',
|
||||
'flake8-isort',
|
||||
'flake8',
|
||||
'freezegun==0.3.8',
|
||||
'pytest-cov==2.5.1',
|
||||
'pytest-django==3.1.2',
|
||||
'pytest-django==4.1.0',
|
||||
'pytest-pythonpath==0.7.2',
|
||||
'pytest-sugar==0.9.1',
|
||||
'pytest==3.4.2',
|
||||
'wagtail_factories==1.0.0',
|
||||
'pytest==6.1.2',
|
||||
'wagtail_factories==1.1.0',
|
||||
'pytest-mock==1.6.3',
|
||||
]
|
||||
|
||||
@ -34,7 +35,7 @@ with open('README.rst') as fh:
|
||||
|
||||
setup(
|
||||
name='wagtail-personalisation',
|
||||
version='0.12.0',
|
||||
version='0.15.2',
|
||||
description='A Wagtail add-on for showing personalized content',
|
||||
author='Lab Digital BV and others',
|
||||
author_email='opensource@labdigital.nl',
|
||||
@ -51,7 +52,7 @@ setup(
|
||||
license='MIT',
|
||||
long_description=long_description,
|
||||
classifiers=[
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: BSD License',
|
||||
@ -59,7 +60,7 @@ setup(
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Framework :: Django',
|
||||
'Framework :: Django :: 2',
|
||||
'Framework :: Django :: 2.0',
|
||||
'Topic :: Internet :: WWW/HTTP :: Site Management',
|
||||
],
|
||||
)
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.utils.module_loading import import_string
|
||||
@ -196,8 +194,10 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
|
||||
for segment in enabled_segments:
|
||||
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
|
||||
additional_segments.append(segment)
|
||||
elif (segment.excluded_users.filter(id=self.request.user.id).exists() or
|
||||
segment in excluded_segments):
|
||||
elif any((
|
||||
segment.excluded_users.filter(id=self.request.user.id).exists(),
|
||||
segment in excluded_segments
|
||||
)):
|
||||
continue
|
||||
elif not segment.is_static or not segment.is_full:
|
||||
segment_rules = []
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
from wagtail_personalisation import models, rules
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from wagtail_personalisation import views
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.core import blocks
|
||||
|
||||
@ -8,6 +6,7 @@ from wagtail_personalisation.models import Segment
|
||||
|
||||
|
||||
def list_segment_choices():
|
||||
yield -1, ("Show to everyone")
|
||||
for pk, name in Segment.objects.values_list('pk', 'name'):
|
||||
yield pk, name
|
||||
|
||||
@ -35,10 +34,19 @@ class PersonalisedStructBlock(blocks.StructBlock):
|
||||
adapter = get_segment_adapter(request)
|
||||
user_segments = adapter.get_segments()
|
||||
|
||||
if value['segment']:
|
||||
try:
|
||||
segment_id = int(value['segment'])
|
||||
except (ValueError, TypeError):
|
||||
return ''
|
||||
|
||||
if segment_id > 0:
|
||||
for segment in user_segments:
|
||||
if segment.id == int(value['segment']):
|
||||
if segment.id == segment_id:
|
||||
return super(PersonalisedStructBlock, self).render(
|
||||
value, context)
|
||||
|
||||
return ""
|
||||
if segment_id == -1:
|
||||
return super(PersonalisedStructBlock, self).render(
|
||||
value, context)
|
||||
|
||||
return ''
|
||||
|
@ -1,21 +1,19 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
import functools
|
||||
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.templatetags.static 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.admin.forms import WagtailAdminModelForm
|
||||
|
||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||
|
||||
|
||||
@lru_cache(maxsize=1000)
|
||||
@functools.lru_cache(maxsize=1000)
|
||||
def user_from_data(user_id):
|
||||
User = get_user_model()
|
||||
try:
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Generated by Django 2.0.7 on 2018-07-04 15:26
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Generated by Django 2.0.5 on 2018-07-19 09:57
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,11 +1,11 @@
|
||||
import random
|
||||
|
||||
import wagtail
|
||||
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 _
|
||||
@ -20,12 +20,19 @@ from wagtail_personalisation.utils import count_active_days
|
||||
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):
|
||||
def enabled(self):
|
||||
return self.filter(status=self.model.STATUS_ENABLED)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Segment(ClusterableModel):
|
||||
"""The segment model."""
|
||||
STATUS_ENABLED = 'enabled'
|
||||
@ -120,8 +127,8 @@ class Segment(ClusterableModel):
|
||||
FieldPanel('randomisation_percent', classname='percent_field'),
|
||||
], heading="Segment"),
|
||||
MultiFieldPanel([
|
||||
InlinePanel(
|
||||
"{}s".format(rule_model._meta.db_table),
|
||||
RulePanel(
|
||||
"{}_related".format(rule_model._meta.db_table),
|
||||
label='{}{}'.format(
|
||||
rule_model._meta.verbose_name,
|
||||
' ({})'.format(_('Static compatible')) if rule_model.static else ''
|
||||
@ -302,3 +309,15 @@ class PersonalisablePageMixin:
|
||||
metadata = PersonalisablePageMetadata.objects.create(
|
||||
canonical_page=self, variant=self)
|
||||
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()
|
||||
|
@ -1,9 +1,8 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import re
|
||||
from datetime import datetime
|
||||
from importlib import import_module
|
||||
|
||||
import pycountry
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.models import Session
|
||||
@ -11,17 +10,36 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.template.defaultfilters import slugify
|
||||
from django.test.client import RequestFactory
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from modelcluster.fields import ParentalKey
|
||||
from user_agents import parse
|
||||
from wagtail.admin.edit_handlers import (
|
||||
FieldPanel, FieldRowPanel, PageChooserPanel)
|
||||
|
||||
from wagtail_personalisation.utils import get_client_ip
|
||||
|
||||
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):
|
||||
"""Base for creating rules to segment users with."""
|
||||
icon = 'fa-circle-o'
|
||||
@ -37,7 +55,7 @@ class AbstractBaseRule(models.Model):
|
||||
verbose_name = 'Abstract segmentation rule'
|
||||
|
||||
def __str__(self):
|
||||
return force_text(self._meta.verbose_name)
|
||||
return str(self._meta.verbose_name)
|
||||
|
||||
def test_user(self):
|
||||
"""Test if the user matches this rule."""
|
||||
@ -45,7 +63,7 @@ class AbstractBaseRule(models.Model):
|
||||
|
||||
def encoded_name(self):
|
||||
"""Return a string with a slug for the rule."""
|
||||
return slugify(force_text(self).lower())
|
||||
return slugify(str(self).lower())
|
||||
|
||||
def description(self):
|
||||
"""Return a description explaining the functionality of the rule.
|
||||
@ -91,7 +109,7 @@ class TimeRule(AbstractBaseRule):
|
||||
verbose_name = _('Time Rule')
|
||||
|
||||
def test_user(self, request=None):
|
||||
return self.start_time <= datetime.now().time() <= self.end_time
|
||||
return self.start_time <= timezone.now().time() <= self.end_time
|
||||
|
||||
def description(self):
|
||||
return {
|
||||
@ -135,7 +153,7 @@ class DayRule(AbstractBaseRule):
|
||||
|
||||
def test_user(self, request=None):
|
||||
return [self.mon, self.tue, self.wed, self.thu,
|
||||
self.fri, self.sat, self.sun][datetime.today().weekday()]
|
||||
self.fri, self.sat, self.sun][timezone.now().date().weekday()]
|
||||
|
||||
def description(self):
|
||||
days = (
|
||||
@ -401,10 +419,72 @@ class UserIsLoggedInRule(AbstractBaseRule):
|
||||
verbose_name = _('Logged in Rule')
|
||||
|
||||
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):
|
||||
return {
|
||||
'title': _('These visitors are'),
|
||||
'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()
|
||||
|
@ -37,8 +37,6 @@
|
||||
/******/ 3: 0
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ var resolvedPromise = new Promise(function(resolve) { resolve(); });
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
@ -66,20 +64,21 @@
|
||||
/******/ // This file contains only the entry chunk.
|
||||
/******/ // The chunk loading function for additional chunks
|
||||
/******/ __webpack_require__.e = function requireEnsure(chunkId) {
|
||||
/******/ if(installedChunks[chunkId] === 0) {
|
||||
/******/ return resolvedPromise;
|
||||
/******/ var installedChunkData = installedChunks[chunkId];
|
||||
/******/ if(installedChunkData === 0) {
|
||||
/******/ return new Promise(function(resolve) { resolve(); });
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // a Promise means "currently loading".
|
||||
/******/ if(installedChunks[chunkId]) {
|
||||
/******/ return installedChunks[chunkId][2];
|
||||
/******/ if(installedChunkData) {
|
||||
/******/ return installedChunkData[2];
|
||||
/******/ }
|
||||
/******/
|
||||
/******/ // setup Promise in chunk cache
|
||||
/******/ var promise = new Promise(function(resolve, reject) {
|
||||
/******/ installedChunks[chunkId] = [resolve, reject];
|
||||
/******/ installedChunkData = installedChunks[chunkId] = [resolve, reject];
|
||||
/******/ });
|
||||
/******/ installedChunks[chunkId][2] = promise;
|
||||
/******/ installedChunkData[2] = promise;
|
||||
/******/
|
||||
/******/ // start chunk loading
|
||||
/******/ var head = document.getElementsByTagName('head')[0];
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{% extends "modeladmin/index.html" %}
|
||||
{% load i18n l10n staticfiles modeladmin_tags %}
|
||||
{% load i18n l10n static modeladmin_tags %}
|
||||
|
||||
{% block titletag %}{{ view.get_meta_title }}{% endblock %}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{% extends "modeladmin/wagtail_personalisation/segment/base.html" %}
|
||||
{% load i18n l10n staticfiles modeladmin_tags wagtail_personalisation_filters %}
|
||||
{% load i18n l10n static modeladmin_tags wagtail_personalisation_filters %}
|
||||
|
||||
{% block toggle_view %}to List {% endblock%}
|
||||
|
||||
|
@ -1,5 +1,4 @@
|
||||
{% extends "modeladmin/wagtail_personalisation/segment/base.html" %}
|
||||
{% load i18n l10n staticfiles modeladmin_tags wagtail_personalisation_filters %}
|
||||
{% load i18n l10n static modeladmin_tags wagtail_personalisation_filters %}
|
||||
|
||||
{% block toggle_view %}to Dashboard {% endblock%}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import F
|
||||
from django.template.base import FilterExpression, kwarg_re
|
||||
from django.utils import timezone
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
|
||||
def impersonate_other_page(page, other_page):
|
||||
@ -116,3 +118,17 @@ def can_delete_pages(pages, user):
|
||||
if not variant.permissions_for_user(user).can_delete():
|
||||
return False
|
||||
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']
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import csv
|
||||
|
||||
from django import forms
|
||||
@ -87,7 +85,10 @@ class SegmentModelAdmin(ModelAdmin):
|
||||
'page_count', 'variant_count', 'statistics')
|
||||
index_view_extra_js = ['js/commons.js', 'js/index.js']
|
||||
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']
|
||||
|
||||
def index_view(self, request):
|
||||
|
@ -1,9 +1,9 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf.urls import include, url
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect, render
|
||||
from django.template.defaultfilters import pluralize
|
||||
from django.urls import reverse
|
||||
@ -11,27 +11,35 @@ from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from wagtail.admin import messages
|
||||
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.core import hooks
|
||||
from wagtail.core.models import Page
|
||||
|
||||
from wagtail_personalisation import admin_urls, models, utils
|
||||
from wagtail_personalisation.adapters import get_segment_adapter
|
||||
from wagtail_personalisation.models import PersonalisablePageMetadata
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@hooks.register('register_admin_urls')
|
||||
@hooks.register("register_admin_urls")
|
||||
def register_admin_urls():
|
||||
"""Adds the administration urls for the personalisation apps."""
|
||||
return [
|
||||
url(r'^personalisation/', include(
|
||||
admin_urls, namespace='wagtail_personalisation')),
|
||||
url(
|
||||
r"^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):
|
||||
"""Tests the provided rules to see if the request still belongs
|
||||
to a segment.
|
||||
@ -46,7 +54,7 @@ def set_visit_count(page, request, serve_args, serve_kwargs):
|
||||
adapter.add_page_visit(page)
|
||||
|
||||
|
||||
@hooks.register('before_serve_page')
|
||||
@hooks.register("before_serve_page")
|
||||
def segment_user(page, request, serve_args, serve_kwargs):
|
||||
"""Apply a segment to a visitor before serving the page.
|
||||
|
||||
@ -59,7 +67,7 @@ def segment_user(page, request, serve_args, serve_kwargs):
|
||||
adapter = get_segment_adapter(request)
|
||||
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:
|
||||
segment = models.Segment.objects.filter(pk=forced_segment).first()
|
||||
if segment:
|
||||
@ -77,14 +85,14 @@ class UserbarSegmentedLinkItem:
|
||||
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):
|
||||
for item in models.Segment.objects.enabled():
|
||||
items.append(UserbarSegmentedLinkItem(item))
|
||||
return items
|
||||
|
||||
|
||||
@hooks.register('before_serve_page')
|
||||
@hooks.register("before_serve_page")
|
||||
def serve_variant(page, request, serve_args, serve_kwargs):
|
||||
"""Apply a segment to a visitor before serving the page.
|
||||
|
||||
@ -104,9 +112,13 @@ def serve_variant(page, request, serve_args, serve_kwargs):
|
||||
adapter = get_segment_adapter(request)
|
||||
user_segments = adapter.get_segments()
|
||||
|
||||
if user_segments:
|
||||
metadata = page.personalisation_metadata
|
||||
metadata = page.personalisation_metadata
|
||||
|
||||
# If page is not canonical, don't serve it.
|
||||
if not metadata.is_canonical:
|
||||
raise Http404
|
||||
|
||||
if user_segments:
|
||||
# TODO: This is never more then one page? (fix query count)
|
||||
metadata = metadata.metadata_for_segments(user_segments)
|
||||
if metadata:
|
||||
@ -114,13 +126,13 @@ def serve_variant(page, 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):
|
||||
return utils.exclude_variants(pages)
|
||||
|
||||
|
||||
@hooks.register('register_page_listing_buttons')
|
||||
def page_listing_variant_buttons(page, page_perms, is_parent=False):
|
||||
@hooks.register("register_page_listing_buttons")
|
||||
def page_listing_variant_buttons(page, page_perms, is_parent=False, *args):
|
||||
"""Adds page listing buttons to personalisable pages. Shows variants for
|
||||
the page (if any) and a 'Create a new variant' button.
|
||||
|
||||
@ -131,17 +143,18 @@ def page_listing_variant_buttons(page, page_perms, is_parent=False):
|
||||
metadata = page.personalisation_metadata
|
||||
if metadata.is_canonical:
|
||||
yield ButtonWithDropdownFromHook(
|
||||
_('Variants'),
|
||||
hook_name='register_page_listing_variant_buttons',
|
||||
_("Variants"),
|
||||
hook_name="register_page_listing_variant_buttons",
|
||||
page=page,
|
||||
page_perms=page_perms,
|
||||
is_parent=is_parent,
|
||||
attrs={'target': '_blank', 'title': _('Create or edit a variant')},
|
||||
priority=100)
|
||||
attrs={"target": "_blank", "title": _("Create or edit a variant")},
|
||||
priority=100,
|
||||
)
|
||||
|
||||
|
||||
@hooks.register('register_page_listing_variant_buttons')
|
||||
def page_listing_more_buttons(page, page_perms, is_parent=False):
|
||||
@hooks.register("register_page_listing_variant_buttons")
|
||||
def page_listing_more_buttons(page, page_perms, is_parent=False, *args):
|
||||
"""Adds a 'more' button to personalisable pages allowing users to quickly
|
||||
create a new variant for the selected segment.
|
||||
|
||||
@ -152,24 +165,30 @@ def page_listing_more_buttons(page, page_perms, is_parent=False):
|
||||
metadata = page.personalisation_metadata
|
||||
|
||||
for vm in metadata.variants_metadata:
|
||||
yield Button('%s variant' % (vm.segment.name),
|
||||
reverse('wagtailadmin_pages:edit', args=[vm.variant_id]),
|
||||
attrs={"title": _('Edit this variant')},
|
||||
classes=("icon", "icon-fa-pencil"),
|
||||
priority=0)
|
||||
yield Button(
|
||||
"%s variant" % (vm.segment.name),
|
||||
reverse("wagtailadmin_pages:edit", args=[vm.variant_id]),
|
||||
attrs={"title": _("Edit this variant")},
|
||||
classes=("icon", "icon-fa-pencil"),
|
||||
priority=0,
|
||||
)
|
||||
|
||||
for segment in metadata.get_unused_segments():
|
||||
yield Button('%s variant' % (segment.name),
|
||||
reverse('segment:copy_page', args=[page.pk, segment.pk]),
|
||||
attrs={"title": _('Create this variant')},
|
||||
classes=("icon", "icon-fa-plus"),
|
||||
priority=100)
|
||||
yield Button(
|
||||
"%s variant" % (segment.name),
|
||||
reverse("segment:copy_page", args=[page.pk, segment.pk]),
|
||||
attrs={"title": _("Create this variant")},
|
||||
classes=("icon", "icon-fa-plus"),
|
||||
priority=100,
|
||||
)
|
||||
|
||||
yield Button(_('Create a new segment'),
|
||||
reverse('wagtail_personalisation_segment_modeladmin_create'),
|
||||
attrs={"title": _('Create a new segment')},
|
||||
classes=("icon", "icon-fa-snowflake-o"),
|
||||
priority=200)
|
||||
yield Button(
|
||||
_("Create a new segment"),
|
||||
reverse("wagtail_personalisation_segment_modeladmin_create"),
|
||||
attrs={"title": _("Create a new segment")},
|
||||
classes=("icon", "icon-fa-snowflake-o"),
|
||||
priority=200,
|
||||
)
|
||||
|
||||
|
||||
class CorrectedPagesSummaryItem(PagesSummaryItem):
|
||||
@ -179,21 +198,22 @@ class CorrectedPagesSummaryItem(PagesSummaryItem):
|
||||
# The `PagesSummaryItem` will return a page count of 0 otherwise.
|
||||
# https://github.com/wagtail/wagtail/blob/5c9ff23e229acabad406c42c4e13cbaea32e6c15/wagtail/admin/site_summary.py#L38
|
||||
context = super().get_context()
|
||||
root_page = context.get('root_page', None)
|
||||
root_page = context.get("root_page", None)
|
||||
if root_page:
|
||||
pages = utils.exclude_variants(
|
||||
Page.objects.descendant_of(root_page, inclusive=True))
|
||||
Page.objects.descendant_of(root_page, inclusive=True)
|
||||
)
|
||||
page_count = pages.count()
|
||||
|
||||
if root_page.is_root():
|
||||
page_count -= 1
|
||||
|
||||
context['total_pages'] = page_count
|
||||
context["total_pages"] = page_count
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@hooks.register('construct_homepage_summary_items')
|
||||
@hooks.register("construct_homepage_summary_items")
|
||||
def add_corrected_pages_summary_panel(request, items):
|
||||
"""Replaces the Pages summary panel to hide variants."""
|
||||
for index, item in enumerate(items):
|
||||
@ -206,16 +226,21 @@ class SegmentSummaryPanel(SummaryItem):
|
||||
site and allowing quick access to the Segment dashboard.
|
||||
|
||||
"""
|
||||
|
||||
order = 2000
|
||||
|
||||
def render(self):
|
||||
segment_count = models.Segment.objects.count()
|
||||
target_url = reverse('wagtail_personalisation_segment_modeladmin_index')
|
||||
target_url = reverse("wagtail_personalisation_segment_modeladmin_index")
|
||||
title = _("Segments")
|
||||
return mark_safe("""
|
||||
return mark_safe(
|
||||
"""
|
||||
<li class="icon icon-fa-snowflake-o">
|
||||
<a href="{}"><span>{}</span>{}</a>
|
||||
</li>""".format(target_url, segment_count, title))
|
||||
</li>""".format(
|
||||
target_url, segment_count, title
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class PersonalisedPagesSummaryPanel(PagesSummaryItem):
|
||||
@ -223,12 +248,17 @@ class PersonalisedPagesSummaryPanel(PagesSummaryItem):
|
||||
|
||||
def render(self):
|
||||
page_count = models.PersonalisablePageMetadata.objects.filter(
|
||||
segment__isnull=True).count()
|
||||
segment__isnull=True
|
||||
).count()
|
||||
title = _("Personalised Page")
|
||||
return mark_safe("""
|
||||
return mark_safe(
|
||||
"""
|
||||
<li class="icon icon-fa-file-o">
|
||||
<span>{}</span>{}{}
|
||||
</li>""".format(page_count, title, pluralize(page_count)))
|
||||
</li>""".format(
|
||||
page_count, title, pluralize(page_count)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class VariantPagesSummaryPanel(PagesSummaryItem):
|
||||
@ -236,15 +266,20 @@ class VariantPagesSummaryPanel(PagesSummaryItem):
|
||||
|
||||
def render(self):
|
||||
page_count = models.PersonalisablePageMetadata.objects.filter(
|
||||
segment__isnull=False).count()
|
||||
segment__isnull=False
|
||||
).count()
|
||||
title = _("Variant")
|
||||
return mark_safe("""
|
||||
return mark_safe(
|
||||
"""
|
||||
<li class="icon icon-fa-files-o">
|
||||
<span>{}</span>{}{}
|
||||
</li>""".format(page_count, title, pluralize(page_count)))
|
||||
</li>""".format(
|
||||
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):
|
||||
"""Adds a summary panel to the Wagtail dashboard showing the total amount
|
||||
of segments on the site and allowing quick access to the Segment
|
||||
@ -256,52 +291,61 @@ def add_personalisation_summary_panels(request, items):
|
||||
items.append(VariantPagesSummaryPanel(request))
|
||||
|
||||
|
||||
@hooks.register('before_delete_page')
|
||||
@hooks.register("before_delete_page")
|
||||
def delete_related_variants(request, page):
|
||||
if not isinstance(page, models.PersonalisablePageMixin) \
|
||||
or not page.personalisation_metadata.is_canonical:
|
||||
if (
|
||||
not isinstance(page, models.PersonalisablePageMixin)
|
||||
or not page.personalisation_metadata.is_canonical
|
||||
):
|
||||
return
|
||||
# Get a list of related personalisation metadata for all the related
|
||||
# variants.
|
||||
variants_metadata = (
|
||||
page.personalisation_metadata.variants_metadata
|
||||
.select_related('variant')
|
||||
variants_metadata = page.personalisation_metadata.variants_metadata.select_related(
|
||||
"variant"
|
||||
)
|
||||
next_url = get_valid_next_url_from_request(request)
|
||||
|
||||
if request.method == 'POST':
|
||||
if request.method == "POST":
|
||||
parent_id = page.get_parent().id
|
||||
variants_metadata = variants_metadata.select_related('variant')
|
||||
with transaction.atomic():
|
||||
for metadata in variants_metadata.iterator():
|
||||
# Call delete() on objects to trigger any signals or hooks.
|
||||
metadata.variant.delete()
|
||||
# Delete the page's main variant and the page itself.
|
||||
page.personalisation_metadata.delete()
|
||||
page.delete()
|
||||
msg = _("Page '{0}' and its variants deleted.")
|
||||
messages.success(
|
||||
request,
|
||||
msg.format(page.get_admin_display_title())
|
||||
)
|
||||
# To ensure variants are deleted for all descendants, start with
|
||||
# the deepest ones, and explicitly delete variants and metadata
|
||||
# for all of them, including the page itself. Otherwise protected
|
||||
# foreign key constraints are violated. Only consider canonical
|
||||
# pages.
|
||||
for metadata in PersonalisablePageMetadata.objects.filter(
|
||||
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()
|
||||
|
||||
for fn in hooks.get_hooks('after_delete_page'):
|
||||
msg = _("Page '{0}' and its variants deleted.")
|
||||
messages.success(request, msg.format(page.get_admin_display_title()))
|
||||
|
||||
for fn in hooks.get_hooks("after_delete_page"):
|
||||
result = fn(request, page)
|
||||
if hasattr(result, 'status_code'):
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
if next_url:
|
||||
return redirect(next_url)
|
||||
return redirect('wagtailadmin_explore', parent_id)
|
||||
return redirect("wagtailadmin_explore", parent_id)
|
||||
|
||||
return render(
|
||||
request,
|
||||
'wagtailadmin/pages/wagtail_personalisation/confirm_delete.html', {
|
||||
'page': page,
|
||||
'descendant_count': page.get_descendant_count(),
|
||||
'next': next_url,
|
||||
'variants': Page.objects.filter(
|
||||
pk__in=variants_metadata.values_list('variant_id')
|
||||
)
|
||||
}
|
||||
"wagtailadmin/pages/wagtail_personalisation/confirm_delete.html",
|
||||
{
|
||||
"page": page,
|
||||
"descendant_count": page.get_descendant_count(),
|
||||
"next": next_url,
|
||||
"variants": Page.objects.filter(
|
||||
pk__in=variants_metadata.values_list("variant_id")
|
||||
),
|
||||
},
|
||||
)
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import pytest
|
||||
|
||||
pytest_plugins = [
|
||||
@ -7,6 +5,11 @@ pytest_plugins = [
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def enable_db_access(db):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def django_db_setup(django_db_setup, django_db_blocker):
|
||||
from wagtail.core.models import Page, Site
|
||||
|
@ -5,7 +5,18 @@ from django.utils.text import slugify
|
||||
from wagtail_factories.factories import PageFactory
|
||||
|
||||
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):
|
||||
parent = None
|
||||
@ -22,3 +33,9 @@ class RegularPageFactory(PageFactory):
|
||||
|
||||
class Meta:
|
||||
model = models.RegularPage
|
||||
|
||||
|
||||
class PersonalisablePageMetadataFactory(factory.DjangoModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = PersonalisablePageMetadata
|
||||
|
@ -46,3 +46,8 @@ class VisitCountRuleFactory(factory.DjangoModelFactory):
|
||||
|
||||
class Meta:
|
||||
model = rules.VisitCountRule
|
||||
|
||||
|
||||
class OriginCountryRuleFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = rules.OriginCountryRule
|
||||
|
@ -23,7 +23,7 @@ def site():
|
||||
return site
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def segmented_page(site):
|
||||
page = ContentPageFactory(parent=site.root_page, slug='personalised')
|
||||
segment = SegmentFactory()
|
||||
@ -46,6 +46,6 @@ class RequestFactory(BaseRequestFactory):
|
||||
return request
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture()
|
||||
def user(django_user_model):
|
||||
return django_user_model.objects.create(username='user')
|
||||
|
@ -1,6 +1,5 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import os
|
||||
from importlib.util import find_spec
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
@ -52,6 +51,7 @@ TEMPLATES = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
MIDDLEWARE = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
@ -59,10 +59,14 @@ MIDDLEWARE = (
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'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 = (
|
||||
'wagtail_personalisation',
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
@ -8,7 +6,7 @@ from django.db.models import ProtectedError
|
||||
from tests.factories.page import ContentPageFactory
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from tests.site.pages import models
|
||||
from wagtail_personalisation.models import PersonalisablePageMetadata
|
||||
from wagtail_personalisation.models import PersonalisablePageMetadata, Segment
|
||||
from wagtail_personalisation.rules import TimeRule
|
||||
|
||||
|
||||
@ -60,3 +58,26 @@ def test_page_protection_when_deleting_segment(segmented_page):
|
||||
assert len(segment.get_used_pages())
|
||||
with pytest.raises(ProtectedError):
|
||||
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"
|
||||
|
202
tests/unit/test_rules_country_origin.py
Normal file
202
tests/unit/test_rules_country_origin.py
Normal file
@ -0,0 +1,202 @@
|
||||
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
|
@ -487,7 +487,7 @@ def test_count_users_matching_static_rules(site, client, mocker, django_user_mod
|
||||
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
|
||||
assert form.count_matching_users([rule], True) == 2
|
||||
|
||||
|
||||
@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)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 1
|
||||
assert form.count_matching_users([rule], True) == 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)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
|
||||
|
||||
assert form.count_matching_users([rule], True) is 1
|
||||
assert form.count_matching_users([rule], True) == 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)
|
||||
mock_test_user = mocker.patch('wagtail_personalisation.rules.TimeRule.test_user')
|
||||
|
||||
assert form.count_matching_users([rule], True) is 0
|
||||
assert form.count_matching_users([rule], True) == 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',
|
||||
side_effect=[True, False, True, False])
|
||||
|
||||
assert form.count_matching_users([first_rule, second_rule], True) is 2
|
||||
assert form.count_matching_users([first_rule, second_rule], True) == 2
|
||||
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',
|
||||
side_effect=[True, True, False, True])
|
||||
|
||||
assert form.count_matching_users([first_rule, second_rule], False) is 1
|
||||
assert form.count_matching_users([first_rule, second_rule], False) == 1
|
||||
mock_test_user.call_count == 4
|
||||
|
@ -1,8 +1,19 @@
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
from wagtail.core.models import Page as WagtailPage
|
||||
|
||||
from tests.factories.page import ContentPageFactory
|
||||
from tests.factories.page import (ContentPageFactory, PersonalisablePageMetadataFactory)
|
||||
from wagtail_personalisation.utils import (
|
||||
can_delete_pages, impersonate_other_page)
|
||||
can_delete_pages, exclude_variants, get_client_ip, impersonate_other_page)
|
||||
|
||||
|
||||
locale_factory = False
|
||||
|
||||
try:
|
||||
from tests.factories.page import LocaleFactory
|
||||
locale_factory = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -36,3 +47,83 @@ def test_can_delete_pages_with_superuser(rf, user, segmented_page):
|
||||
@pytest.mark.django_db
|
||||
def test_cannot_delete_pages_with_standard_user(user, segmented_page):
|
||||
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))
|
||||
|
@ -6,7 +6,7 @@ from wagtail.core.models import Page
|
||||
from wagtail_personalisation.models import Segment
|
||||
from wagtail_personalisation.rules import VisitCountRule
|
||||
from wagtail_personalisation.views import (
|
||||
SegmentModelDeleteView, SegmentModelAdmin)
|
||||
SegmentModelAdmin, SegmentModelDeleteView)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@ -106,5 +106,5 @@ def test_segment_delete_view_raises_permission_denied(rf, segmented_page, user):
|
||||
)
|
||||
view.request = request
|
||||
message = 'User have no permission to delete variant page objects.'
|
||||
with pytest.raises(PermissionDenied, message=message):
|
||||
with pytest.raises(PermissionDenied):
|
||||
view.delete_instance()
|
||||
|
@ -1,6 +1,8 @@
|
||||
import pytest
|
||||
from django.http import Http404
|
||||
from wagtail.core.models import Page
|
||||
|
||||
from tests.factories.page import ContentPageFactory
|
||||
from tests.factories.segment import SegmentFactory
|
||||
from wagtail_personalisation import adapters, wagtail_hooks
|
||||
|
||||
@ -16,6 +18,15 @@ def test_serve_variant_no_variant(site, rf):
|
||||
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
|
||||
def test_serve_variant_with_variant_no_segment(site, rf, segmented_page):
|
||||
request = rf.get('/')
|
||||
@ -112,3 +123,21 @@ def test_custom_delete_page_view_deletes_variants(rf, segmented_page, user):
|
||||
# Make sure all the variant pages have been deleted.
|
||||
assert not len(variants.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
|
||||
)
|
||||
|
@ -5,3 +5,7 @@ def render_template(value, **context):
|
||||
template = engines['django'].from_string(value)
|
||||
request = context.pop('request', None)
|
||||
return template.render(context, request)
|
||||
|
||||
|
||||
def get_custom_ip(request):
|
||||
return '123.123.123.123'
|
||||
|
39
tox.ini
39
tox.ini
@ -1,14 +1,30 @@
|
||||
[tox]
|
||||
envlist = py{36}-django{20}-wagtail{20,21},lint
|
||||
envlist =
|
||||
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]
|
||||
basepython = python3.6
|
||||
commands = coverage run --parallel -m pytest {posargs}
|
||||
basepython =
|
||||
py36: python3.6
|
||||
py37: python3.7
|
||||
py38: python3.8
|
||||
commands = coverage run --parallel -m pytest -rs {posargs}
|
||||
extras = test
|
||||
deps =
|
||||
django20: django>=2.0,<2.1
|
||||
wagtail20: wagtail>=2.0,<2.1
|
||||
wagtail21: wagtail>=2.1,<2.2
|
||||
dj22: Django>=2.2.8,<2.3
|
||||
dj30: Django>=3.0,<3.1
|
||||
dj31: Django>=3.1,<3.2
|
||||
wt211: wagtail>=2.11,<2.12
|
||||
wt212: wagtail>=2.12,<2.13
|
||||
wt213: wagtail>=2.13,<2.14
|
||||
geoip2: geoip2
|
||||
|
||||
[testenv:coverage-report]
|
||||
basepython = python3.6
|
||||
@ -20,7 +36,16 @@ commands =
|
||||
|
||||
[testenv:lint]
|
||||
basepython = python3.6
|
||||
deps = flake8
|
||||
deps = flake8==3.5.0
|
||||
commands =
|
||||
flake8 src tests setup.py
|
||||
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/
|
||||
|
Reference in New Issue
Block a user