7

Compare commits

...

110 Commits

Author SHA1 Message Date
449c6ed577 allow wagtail 2.16 and django 4.0 in sandbox 2022-06-30 10:40:21 +01:00
f921152aae allow django 4+ 2022-06-30 10:26:50 +01:00
24edf230b4 remove py 3.10 for now 2022-06-30 09:52:47 +01:00
a2f3979923 does this work 2022-06-30 09:40:38 +01:00
bc7ad546c9 try quotes 2022-06-30 09:14:59 +01:00
aa54d31ee5 run latest black 2022-06-30 07:28:09 +01:00
7eaac70154 avoid lint errors 2022-06-30 07:24:58 +01:00
8efb1d3583 update tox 2022-06-30 07:13:49 +01:00
66f34de1c1 Add python3.9 tests 2022-06-30 07:09:46 +01:00
87bab7e5ca remove python 3.6 tests 2022-06-30 07:03:45 +01:00
c5e3a9378f Allow wagtail>=2.9,<2.17 2022-06-30 06:58:26 +01:00
dd4530203f Bump version: 0.15.2 → 0.15.3 2022-02-04 15:12:42 +00:00
48955675be Use get_context_data override instead of get_context for Wagtail >= 2.15 (#230). Fix #228 2022-01-28 11:53:57 +00:00
a81c5b3560 Bump version: 0.15.1 → 0.15.2 2021-09-24 10:30:22 +02:00
53880228e4 Merge pull request #226 from mikedingjan/feature/remove-staticfiles-tag
Replace staticfiles with static tag (django removed the staticfiles)
2021-08-12 14:20:16 +02:00
2bee66d0ae Replace staticfiles with static tag (django removed the staticfiles) 2021-08-12 10:44:02 +02:00
16e24b6791 Bump version: 0.15.0 → 0.15.1 2021-07-13 17:01:35 +02:00
477bfb9665 Newer versions of Wagtail provide extra args for listing buttons 2021-07-13 16:40:41 +02:00
6108469047 Remove old versions from test matrix 2021-07-13 16:40:23 +02:00
686f180081 Bump version: 0.14.0 → 0.15.0 2021-07-09 11:00:14 +02:00
9b1dbe35cb fix(tox): use correct format command for current package 2021-06-28 12:15:24 +02:00
7e0594e341 fix(tox): add new tox setup for github actions 2021-06-28 12:13:55 +02:00
0c19456053 Merge pull request #212 from marcelhekking/make_compatible_with_latest_wagtail_version
Make compatible with latest wagtail version
2021-06-28 12:10:31 +02:00
18140f76ab chore(ci): trigger github actions on pr 2021-06-28 12:08:58 +02:00
88b17ceeb8 chore(ci): add github actions python test step 2021-06-28 12:06:43 +02:00
570de7d128 Flake-import failed 2021-06-24 08:38:06 +02:00
b82d5165c3 Take up wagtail 2.11 in Travis test matrix and tox settings 2021-06-24 08:16:29 +02:00
8d802dbbf4 Restore original travis settings 2021-06-24 07:58:11 +02:00
9274073c68 Fix test errors 2021-06-24 07:57:31 +02:00
1f1264cf95 Fix typo 2020-11-25 16:40:15 +01:00
3f16ad686e Remove obsolete line 2020-11-25 15:54:32 +01:00
7101b63122 Check backward compatibility with tox 2020-11-25 15:50:52 +01:00
ffd839159b Make changes backwards compatible 2020-11-25 12:08:42 +01:00
d074ef85b9 No need for these settings 2020-11-24 09:10:14 +01:00
f3e403bec6 Make compatible with latest Wagtail version (2.11.2) 2020-11-24 09:05:20 +01:00
137b5b411c Merge pull request #203 from davisnando/master
Fix is_authenticated 'bool' object is not callable error
2020-01-24 08:22:06 +01:00
39f3500813 Bump version: 0.13.0 → 0.14.0 2019-09-27 09:16:15 +02:00
6a6c3e8d7b Merge pull request #202 from wagtail/feature/wagtail-2-6
Update test matrix to include new Django and Wagtail versions
2019-09-26 11:45:29 +02:00
336ed2317c Merge pull request #198 from ixc/198_delete_variants_of_descendants
Variants are not deleted for page descendants
2019-09-19 09:57:18 +02:00
06569a3cc1 Fix 'bool' object is not callable error 2019-08-27 11:43:39 +02:00
da6e5127ed Update test matrix to include new Django and Wagtail versions 2019-08-22 09:36:27 +02:00
3d054ec585 Add migrations for country field on origincountryrule 2019-08-22 08:28:14 +02:00
43b5b62e60 Clean up test_static_dynamic_segments.py so it passes flake8 (#199)
* WP-1 clean up tests to pass flake8

* WP-1 undo yapf!
2019-03-16 08:27:37 +01:00
40a9959680 Bump version: 0.12.1 → 0.13.0 2019-03-15 14:04:42 +01:00
13e13ccae5 update sandbox 2019-03-15 13:54:59 +01:00
318b65b3eb allow wagtail24 2019-03-15 13:52:51 +01:00
6b9b4e0af2 remove extraneous file 2019-03-15 13:50:48 +01:00
69a4514129 update testcode
run isort
2019-03-15 12:58:30 +01:00
585cb0b16a clean out some python2-isms 2019-03-15 11:50:34 +01:00
4ae8a5e60b postmerge fixes 2019-03-15 11:46:44 +01:00
d7ad1be51f Merge preakholt changes 2019-03-15 11:21:14 +01:00
bd5b85cedb Merge branch 'master' into 198_delete_variants_of_descendants 2019-01-25 12:34:58 +11:00
956c1bf4f5 Use timezone-aware dates and times in rules (#197)
* use timezone-aware dates and times re #196

* remove redundant newlines

* Fix flake8 linting errors in python 3.6
2019-01-24 16:27:34 +01:00
d775ef57e6 Ensure variants are deleted for page decendants 2019-01-24 16:59:44 +11:00
d34c449638 Merge tag '1.0.4' into develop
1.0.4
2019-01-16 13:09:24 +02:00
23af862798 Merge branch 'release/1.0.4' 2019-01-16 13:09:15 +02:00
88263dea60 Bump version to 1.0.4 2019-01-16 13:08:58 +02:00
2e1e09f60b Merge pull request #31 from praekeltfoundation/feature/GEWEB-774-fix-segment-admin-js
Add custom js files to segment create view
2019-01-16 12:43:30 +02:00
86e669e4f4 Add custom js files to segment create view 2019-01-16 12:02:14 +02:00
807005461e update version to 1.0.3 2019-01-10 17:13:38 +02:00
7f9e0971f5 Merge tag '1.0.3' into develop
bugfix:exclude variant returns queryset when params is queryset
2019-01-10 16:12:20 +02:00
a4a1a2ddca Merge branch 'release/1.0.3' 2019-01-10 16:12:14 +02:00
9cc6e966ba bugfix:exclude variant returns queryset when params is queryset 2019-01-10 16:12:01 +02:00
311abeb6c8 Merge pull request #30 from praekeltfoundation/feature/GEWEB-746-fix-panel-server-error
exclude variants should return a list when a list if given or a queryset
2019-01-10 15:05:56 +02:00
60675203c6 fixed some comments 2019-01-10 14:51:43 +02:00
ceef806301 add tests for varient exclusion use cases 2019-01-10 14:47:18 +02:00
650e061f91 assign pages on exclude 2019-01-10 14:47:01 +02:00
9235932f00 update exclude varient format and add variants to tests 2019-01-10 12:41:53 +02:00
1e0efc975a Merge tag '1.0.2' into develop
1.0.2
2019-01-09 19:14:24 +02:00
7517dcd051 Merge branch 'release/1.0.2' 2019-01-09 19:14:17 +02:00
94c9efa315 Bump version to 1.0.2 2019-01-09 19:14:07 +02:00
f73e59421b Merge pull request #29 from praekeltfoundation/feature/GEWEB-740-upgrade-to-wagtail-2.2
Upgrade to wagtail 2.2
2019-01-09 17:39:44 +02:00
f054b86e07 fix flake error in conftest 2019-01-09 17:25:56 +02:00
cbb56847ae fix flake8 error 2019-01-09 17:17:51 +02:00
b135e79c77 remove trailing print 2019-01-09 17:04:00 +02:00
5cd8751450 add ve to the gitignore 2019-01-09 16:56:38 +02:00
c07b280276 allow database access for tests 2019-01-09 16:55:06 +02:00
28266c4500 fix flake error 2019-01-09 16:54:49 +02:00
94a5c6b289 tests for querysets in variant_exclude 2019-01-09 16:54:28 +02:00
875d8302de exclude variants should return a list when a list if given or a queryset 2019-01-09 16:54:04 +02:00
4c09ad4ca7 fix flake error W504 2019-01-09 16:52:26 +02:00
0d260a12a4 Tell travis to use wagtail 2.2 2019-01-09 16:28:31 +02:00
7888f0b615 Upgrade to wagtail 2.2 in requirements and tests 2019-01-09 16:12:05 +02:00
02e63ed82c Merge tag '1.0.1' into develop
1.0.1
2019-01-02 17:28:58 +02:00
a411ad1ccc Merge branch 'release/1.0.1' 2019-01-02 17:28:50 +02:00
1a1df18bf3 Bump version to 1.0.1 2019-01-02 17:28:36 +02:00
56d28faec8 Merge branch 'master' into develop 2019-01-02 17:24:40 +02:00
f95b8dcb93 Merge pull request #28 from praekeltfoundation/feature/GEWEB-740-upgrade-to-wagtail-2-and-python-3
Define panel for rules to handle InlinePanel changes
2019-01-02 17:23:32 +02:00
d3f4d42d82 Define panel for rules to handle InlinePanel changes 2019-01-02 16:51:56 +02:00
4c08581919 Merge tag '1.0.0' into develop
1.0.0
2018-12-18 14:03:39 +02:00
11886ae135 Merge branch 'release/1.0.0' 2018-12-18 14:03:29 +02:00
83cc7f790e Bump version to 1.0.0 2018-12-18 14:03:01 +02:00
dcdeb4e9a2 Update imports in examples in docs 2018-12-18 14:02:01 +02:00
2e827be41a Merge pull request #27 from praekeltfoundation/feature/GEWEB-740-upgrade-to-wagtail-2-and-python-3
Upgrade to Python 3 and Wagtail 2
2018-12-18 13:48:31 +02:00
0f9bfb0343 Tell travis to use Wagtail 2.0 2018-12-18 13:37:34 +02:00
1c74e6cfb9 Update Wagtail imports to work for 2.0 2018-12-18 13:32:02 +02:00
9c45ac56db Upgrade Wagtail to 2.0 in requirements and tests 2018-12-18 13:30:27 +02:00
2f7b92fb2e Run tests on python 3.6 2018-12-18 10:40:45 +02:00
1e69d929aa Bump version: 0.12.0 → 0.12.1 2018-09-26 20:42:26 +02:00
a178a8b533 Fix django classifier version number 2018-09-26 20:40:42 +02:00
f2e01c803a Bump version: 0.11.3 → 0.12.0 2018-09-26 14:23:05 +02:00
eb9d4f3e31 Update changelog for version 0.12.0 2018-09-26 14:20:18 +02:00
4ceb59c719 Merge pull request #193 from ixc/relax-wagtail-constraint
Remove overly restrictive wagtail dependency version constraint (#192)
2018-09-26 08:14:59 +02:00
6fcab3ac11 Remove overly restrictive wagtail dependency version constraint (#192) 2018-09-26 11:00:47 +10:00
1f464adaa7 Do not generate sitemap entries for variants (#187) 2018-09-25 07:57:41 +02:00
d15f6c37d3 Return 404 if variant page is accessed directly (#188) 2018-09-25 07:57:06 +02:00
7d679d7111 Add origin country rule (#190) 2018-09-25 07:51:25 +02:00
b11a6ce4ca Add missing TOXENV=lint to Travis (#191) 2018-09-25 07:50:54 +02:00
4e9a6e902d Merge pull request #186 from wagtail/feature/show-to-everyone-block
Add an option to show a personalised block to everyone (no segment)
2018-08-06 19:25:27 +02:00
3ce0aef8d5 Add an option to show a personalised block to everyone 2018-08-06 15:16:36 +01:00
95 changed files with 7010 additions and 2556 deletions

92
.github/workflows/python-test.yml vendored Normal file
View File

@ -0,0 +1,92 @@
---
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:
- 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.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
- python-version: 3.9
tox_env: py39-dj31-wt214
- python-version: 3.9
tox_env: py39-dj32-wt214
- python-version: 3.9
tox_env: py39-dj31-wt215
- python-version: 3.9
tox_env: py39-dj32-wt215
- python-version: 3.9
tox_env: py39-dj32-wt216
- python-version: 3.9
tox_env: py39-dj40-wt216
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,6 +13,7 @@
.vscode/
build/
ve/
dist/
htmlcov/
docs/_build

View File

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

55
CHANGES
View File

@ -1,3 +1,58 @@
0.15.3
=================
- Add wagtail >= 2.15 support with get_context_data override instead of get_context
0.15.2
=================
- Replace staticfiles tag with static
0.15.1
=================
- Remove old versions from test matrix
- Fix button support in wagtail admin for newer wagtail versions
0.15.0
=================
- Fix is_authenticated 'bool' object is not callable error
- Add wagtail <=2.11 support
- Use Github Actions to test package instead of Travis CI
0.14.0
=================
- Fix 'bool' object is not callable error
- Fix deleting descendants with variants when deleting a page
- Add wagtail 2.6 support
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

View File

@ -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.3'
# The full version, including alpha/beta/rc tags.
release = '0.12.0'
release = '0.15.3'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.

View File

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

View File

@ -1,4 +1,4 @@
Django>=2.0,<2.1
wagtail>=2.1,<2.2
django-debug-toolbar==1.9.1
Django>=2.2,<4.1
wagtail>=2.9,<3.0
django-debug-toolbar==3.5.0
-e .[docs,test]

View File

@ -2,9 +2,9 @@
# Generated by Django 1.11.1 on 2017-05-31 16:59
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import modelcluster.fields
from django.db import migrations, models
class Migration(migrations.Migration):
@ -12,19 +12,29 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0033_remove_golive_expiry_help_text'),
('wagtail_personalisation', '0011_personalisablepagemetadata'),
("wagtailcore", "0033_remove_golive_expiry_help_text"),
("wagtail_personalisation", "0011_personalisablepagemetadata"),
]
operations = [
migrations.CreateModel(
name='HomePage',
name="HomePage",
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
),
],
options={
'abstract': False,
"abstract": False,
},
bases=('wagtailcore.page', models.Model),
bases=("wagtailcore.page", models.Model),
),
]

View File

@ -6,10 +6,10 @@ from django.db import migrations
def create_homepage(apps, schema_editor):
# Get models
ContentType = apps.get_model('contenttypes.ContentType')
Page = apps.get_model('wagtailcore.Page')
Site = apps.get_model('wagtailcore.Site')
HomePage = apps.get_model('home.HomePage')
ContentType = apps.get_model("contenttypes.ContentType")
Page = apps.get_model("wagtailcore.Page")
Site = apps.get_model("wagtailcore.Site")
HomePage = apps.get_model("home.HomePage")
# Delete the default homepage
# If migration is run multiple times, it may have already been deleted
@ -17,41 +17,41 @@ def create_homepage(apps, schema_editor):
# Create content type for homepage model
homepage_content_type, __ = ContentType.objects.get_or_create(
model='homepage', app_label='home')
model="homepage", app_label="home"
)
# Create a new homepage
homepage = HomePage.objects.create(
title="Home",
slug='home',
slug="home",
content_type=homepage_content_type,
path='00010001',
path="00010001",
depth=2,
numchild=0,
url_path='/home/',
url_path="/home/",
)
# Create a site with the new homepage set as the root
Site.objects.create(
hostname='localhost', root_page=homepage, is_default_site=True)
Site.objects.create(hostname="localhost", root_page=homepage, is_default_site=True)
def remove_homepage(apps, schema_editor):
# Get models
ContentType = apps.get_model('contenttypes.ContentType')
HomePage = apps.get_model('home.HomePage')
ContentType = apps.get_model("contenttypes.ContentType")
HomePage = apps.get_model("home.HomePage")
# Delete the default homepage
# Page and Site objects CASCADE
HomePage.objects.filter(slug='home', depth=2).delete()
HomePage.objects.filter(slug="home", depth=2).delete()
# Delete content type for homepage model
ContentType.objects.filter(model='homepage', app_label='home').delete()
ContentType.objects.filter(model="homepage", app_label="home").delete()
class Migration(migrations.Migration):
dependencies = [
('home', '0001_initial'),
("home", "0001_initial"),
]
operations = [

View File

@ -2,29 +2,53 @@
# Generated by Django 1.11.1 on 2017-05-31 19:36
from __future__ import unicode_literals
from django.db import migrations
import wagtail.core.fields
from django.db import migrations
import wagtail_personalisation
class Migration(migrations.Migration):
dependencies = [
('home', '0002_create_homepage'),
("home", "0002_create_homepage"),
]
operations = [
migrations.AddField(
model_name='homepage',
name='intro',
model_name="homepage",
name="intro",
field=wagtail.core.fields.RichTextField(
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'),
default='<p>Thank you for trying <a href="http://wagxperience.io" target="_blank">Wagxperience</a>!</p>'
),
preserve_default=False,
),
migrations.AddField(
model_name='homepage',
name='body',
field=wagtail.core.fields.StreamField((('personalisable_paragraph', wagtail.core.blocks.StructBlock((('segment', wagtail.core.blocks.ChoiceBlock(choices=wagtail_personalisation.blocks.list_segment_choices, help_text='Only show this content block for users in this segment', label='Personalisation segment', required=False)), ('paragraph', wagtail.core.blocks.RichTextBlock())), icon='pilcrow')),), default=''),
model_name="homepage",
name="body",
field=wagtail.core.fields.StreamField(
(
(
"personalisable_paragraph",
wagtail.core.blocks.StructBlock(
(
(
"segment",
wagtail.core.blocks.ChoiceBlock(
choices=wagtail_personalisation.blocks.list_segment_choices,
help_text="Only show this content block for users in this segment",
label="Personalisation segment",
required=False,
),
),
("paragraph", wagtail.core.blocks.RichTextBlock()),
),
icon="pilcrow",
),
),
),
default="",
),
preserve_default=False,
),
]

View File

@ -5,19 +5,27 @@ from wagtail.core import blocks
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Page
from wagtail_personalisation.models import PersonalisablePageMixin
from wagtail_personalisation.blocks import PersonalisedStructBlock
from wagtail_personalisation.models import PersonalisablePageMixin
class HomePage(PersonalisablePageMixin, Page):
intro = RichTextField()
body = StreamField([
('personalisable_paragraph', PersonalisedStructBlock([
('paragraph', blocks.RichTextBlock()),
], icon='pilcrow'))
])
body = StreamField(
[
(
"personalisable_paragraph",
PersonalisedStructBlock(
[
("paragraph", blocks.RichTextBlock()),
],
icon="pilcrow",
),
)
]
)
content_panels = Page.content_panels + [
RichTextFieldPanel('intro'),
StreamFieldPanel('body'),
RichTextFieldPanel("intro"),
StreamFieldPanel("body"),
]

View File

@ -2,14 +2,13 @@ from __future__ import absolute_import, unicode_literals
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
from django.shortcuts import render
from wagtail.core.models import Page
from wagtail.search.models import Query
def search(request):
search_query = request.GET.get('query', None)
page = request.GET.get('page', 1)
search_query = request.GET.get("query", None)
page = request.GET.get("page", 1)
# Search
if search_query:
@ -30,7 +29,11 @@ def search(request):
except EmptyPage:
search_results = paginator.page(paginator.num_pages)
return render(request, 'search/search.html', {
'search_query': search_query,
'search_results': search_results,
})
return render(
request,
"search/search.html",
{
"search_query": search_query,
"search_results": search_results,
},
)

View File

@ -12,31 +12,30 @@ class UserAdmin(BaseUserAdmin):
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ['email']
list_filter = ['is_superuser']
list_display = ["email"]
list_filter = ["is_superuser"]
fieldsets = (
(None, {
'fields': ['email', 'password']
}),
('Personal info', {
'fields': ['first_name', 'last_name']
}),
('Permissions', {
'fields': [
'is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions'
]
}),
(None, {"fields": ["email", "password"]}),
("Personal info", {"fields": ["first_name", "last_name"]}),
(
"Permissions",
{
"fields": [
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
]
},
),
)
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ['email', 'password1', 'password2']
}),
(None, {"classes": ("wide",), "fields": ["email", "password1", "password2"]}),
)
search_fields = ['first_name', 'last_name', 'email']
ordering = ['email']
search_fields = ["first_name", "last_name", "email"]
ordering = ["email"]
filter_horizontal = []

View File

@ -1,6 +1,11 @@
from django import VERSION as DJANGO_VERSION
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.utils.translation import ugettext_lazy as _
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
from django.utils.translation import ugettext_lazy as _
from sandbox.apps.user import models
@ -10,21 +15,22 @@ class UserCreationForm(forms.ModelForm):
fields, plus a repeated password.
"""
password1 = forms.CharField(
label='Password', widget=forms.PasswordInput,
required=False)
label="Password", widget=forms.PasswordInput, required=False
)
password2 = forms.CharField(
label='Password confirmation', widget=forms.PasswordInput,
required=False)
label="Password confirmation", widget=forms.PasswordInput, required=False
)
class Meta:
model = models.User
fields = ['email']
fields = ["email"]
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
@ -32,7 +38,7 @@ class UserCreationForm(forms.ModelForm):
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data['password1'])
user.set_password(self.cleaned_data["password1"])
if commit:
user.save()
return user
@ -44,20 +50,22 @@ class UserChangeForm(forms.ModelForm):
password hash display field.
"""
password = ReadOnlyPasswordHashField(
label=_("Password"),
help_text=_("Raw passwords are not stored, so there is no way to see "
"this user's password, but you can change the password "
"using <a href=\"password/\">this form</a>."))
help_text=_(
"Raw passwords are not stored, so there is no way to see "
"this user's password, but you can change the password "
'using <a href="password/">this form</a>.'
),
)
class Meta:
model = models.User
fields = [
'email', 'password', 'is_active', 'is_superuser'
]
fields = ["email", "password", "is_active", "is_superuser"]
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial['password']
return self.initial["password"]

View File

@ -3,8 +3,8 @@
from __future__ import unicode_literals
import django.contrib.auth.models
from django.db import migrations, models
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
@ -12,32 +12,109 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
("auth", "0008_alter_user_username_max_length"),
]
operations = [
migrations.CreateModel(
name='User',
name="User",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('first_name', models.CharField(blank=True, max_length=100, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=100, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, unique=True, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=100, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=100, verbose_name="last name"
),
),
(
"email",
models.EmailField(
blank=True,
max_length=254,
unique=True,
verbose_name="email address",
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.Group",
verbose_name="groups",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.Permission",
verbose_name="user permissions",
),
),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
"verbose_name": "user",
"verbose_name_plural": "users",
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
("objects", django.contrib.auth.models.UserManager()),
],
),
]

View File

@ -0,0 +1,21 @@
# 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,43 +1,85 @@
from django import VERSION as DJANGO_VERSION
from django.contrib.auth.models import (
AbstractBaseUser, PermissionsMixin, UserManager)
AbstractBaseUser,
BaseUserManager,
PermissionsMixin,
)
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 _
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
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)
last_name = models.CharField(_('last name'), max_length=100, blank=True)
email = models.EmailField(_('email address'), blank=True, unique=True)
first_name = models.CharField(_("first name"), max_length=100, blank=True)
last_name = models.CharField(_("last name"), max_length=100, blank=True)
email = models.EmailField(_("email address"), blank=True, unique=True)
is_staff = models.BooleanField(
_('staff status'), default=False,
help_text=_('Designates whether the user can log into this admin '
'site.'))
_("staff status"),
default=False,
help_text=_("Designates whether the user can log into this admin " "site."),
)
is_active = models.BooleanField(
_('active'), default=True,
help_text=_('Designates whether this user should be treated as '
'active. Unselect this instead of deleting accounts.'))
date_joined = models.DateTimeField(_('date joined'), default=timezone.now)
_("active"),
default=True,
help_text=_(
"Designates whether this user should be treated as "
"active. Unselect this instead of deleting accounts."
),
)
date_joined = models.DateTimeField(_("date joined"), default=timezone.now)
objects = UserManager()
USERNAME_FIELD = 'email'
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
class Meta:
verbose_name = _('user')
verbose_name_plural = _('users')
verbose_name = _("user")
verbose_name_plural = _("users")
def get_full_name(self):
"""
Returns the first_name plus the last_name, with a space in between.
"""
full_name = '%s %s' % (self.first_name, self.last_name)
full_name = "%s %s" % (self.first_name, self.last_name)
return full_name.strip()
def get_short_name(self):

View File

@ -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)
@ -21,10 +22,10 @@ BASE_DIR = os.path.dirname(PROJECT_DIR)
DEBUG = True
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '^anfvx$i7%wts8j=7u1h5ua$w6c76*333(@h)rrjlak1c&x0r+'
SECRET_KEY = "^anfvx$i7%wts8j=7u1h5ua$w6c76*333(@h)rrjlak1c&x0r+"
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
@ -34,87 +35,80 @@ SITE_ID = 1
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.messages',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.staticfiles',
'wagtail.contrib.forms',
'wagtail.contrib.redirects',
'wagtail.embeds',
'wagtail.sites',
'wagtail.users',
'wagtail.snippets',
'wagtail.documents',
'wagtail.images',
'wagtail.search',
'wagtail.admin',
'wagtail.core',
'wagtail.contrib.modeladmin',
'wagtailfontawesome',
'modelcluster',
'taggit',
'debug_toolbar',
'wagtail_personalisation',
'sandbox.apps.home',
'sandbox.apps.search',
'sandbox.apps.user',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.messages",
"django.contrib.sessions",
"django.contrib.sites",
"django.contrib.staticfiles",
"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail.embeds",
"wagtail.sites",
"wagtail.users",
"wagtail.snippets",
"wagtail.documents",
"wagtail.images",
"wagtail.search",
"wagtail.admin",
"wagtail.core",
"wagtail.contrib.modeladmin",
"wagtailfontawesome",
"modelcluster",
"taggit",
"debug_toolbar",
"wagtail_personalisation",
"sandbox.apps.home",
"sandbox.apps.search",
"sandbox.apps.user",
]
MIDDLEWARE = [
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.core.middleware.SiteMiddleware',
'wagtail.contrib.redirects.middleware.RedirectMiddleware',
"debug_toolbar.middleware.DebugToolbarMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.http.ConditionalGetMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware",
]
ROOT_URLCONF = 'sandbox.urls'
ROOT_URLCONF = "sandbox.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(PROJECT_DIR, 'templates'),
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
os.path.join(PROJECT_DIR, "templates"),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = 'sandbox.wsgi.application'
WSGI_APPLICATION = "sandbox.wsgi.application"
AUTH_USER_MODEL = 'user.User'
AUTH_USER_MODEL = "user.User"
# Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'db.sqlite3',
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "db.sqlite3",
}
}
@ -122,9 +116,9 @@ DATABASES = {
# Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@ -137,19 +131,19 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.11/howto/static-files/
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
"django.contrib.staticfiles.finders.FileSystemFinder",
"django.contrib.staticfiles.finders.AppDirectoriesFinder",
]
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
os.path.join(PROJECT_DIR, "static"),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATIC_URL = "/static/"
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/media/"
# Wagtail settings
@ -158,7 +152,7 @@ WAGTAIL_SITE_NAME = "sandbox"
# Base URL to use when referring to full URLs within the Wagtail admin backend -
# e.g. in notification emails. Don't include '/admin' or a trailing slash
BASE_URL = 'http://example.com'
BASE_URL = "http://example.com"
INTERNAL_IPS = ['127.0.0.1']
INTERNAL_IPS = ["127.0.0.1"]

View File

@ -2,8 +2,8 @@ from __future__ import absolute_import, unicode_literals
import debug_toolbar
from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
from django.urls import include, re_path
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
@ -11,18 +11,14 @@ 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'^cms/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
url(r'^search/$', search_views.search, name='search'),
re_path(r"^django-admin/", admin.site.urls),
re_path(r"^admin/", include(wagtailadmin_urls)),
re_path(r"^documents/", include(wagtaildocs_urls)),
re_path(r"^search/$", search_views.search, name="search"),
# For anything not caught by a more specific rule above, hand over to
# Wagtail's page serving mechanism. This should be the last pattern in
# the list:
url(r'', include(wagtail_urls)),
re_path(r"", include(wagtail_urls)),
# Alternatively, if you want Wagtail pages to be served from a subpath
# of your site, rather than the site root:
# url(r'^pages/', include(wagtail_urls)),
@ -38,5 +34,5 @@ if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
re_path(r"^__debug__/", include(debug_toolbar.urls)),
] + urlpatterns

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.12.0
current_version = 0.15.3
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]

View File

@ -1,65 +1,68 @@
import re
from setuptools import find_packages, setup
install_requires = [
'wagtail>=2.0,<2.2',
'user-agents>=1.1.0',
'wagtailfontawesome>=1.1.3',
"wagtail>=2.9,<2.17",
"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',
'freezegun==0.3.8',
'pytest-cov==2.5.1',
'pytest-django==3.1.2',
'pytest-pythonpath==0.7.2',
'pytest-sugar==0.9.1',
'pytest==3.4.2',
'wagtail_factories==1.0.0',
'pytest-mock==1.6.3',
"factory_boy==2.8.1",
"flake8-blind-except",
"flake8-debugger",
"flake8-isort",
"flake8",
"freezegun==0.3.8",
"pytest-cov==2.5.1",
"pytest-django==4.1.0",
"pytest-pythonpath==0.7.2",
"pytest-sugar==0.9.1",
"pytest==6.1.2",
"wagtail_factories==1.1.0",
"pytest-mock==1.6.3",
]
docs_require = [
'sphinx>=1.7.6',
'sphinx_rtd_theme>=0.4.0',
"sphinx>=1.7.6",
"sphinx_rtd_theme>=0.4.0",
]
with open('README.rst') as fh:
with open("README.rst") as fh:
long_description = re.sub(
'^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S)
"^.. start-no-pypi.*^.. end-no-pypi", "", fh.read(), flags=re.M | re.S
)
setup(
name='wagtail-personalisation',
version='0.12.0',
description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV and others',
author_email='opensource@labdigital.nl',
url='https://labdigital.nl/',
name="wagtail-personalisation",
version="0.15.3",
description="A Wagtail add-on for showing personalized content",
author="Lab Digital BV and others",
author_email="opensource@labdigital.nl",
url="https://labdigital.nl/",
install_requires=install_requires,
tests_require=tests_require,
extras_require={
'docs': docs_require,
'test': tests_require,
"docs": docs_require,
"test": tests_require,
},
packages=find_packages('src'),
package_dir={'': 'src'},
packages=find_packages("src"),
package_dir={"": "src"},
include_package_data=True,
license='MIT',
license="MIT",
long_description=long_description,
classifiers=[
'Development Status :: 2 - Pre-Alpha',
'Environment :: Web Environment',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.6',
'Framework :: Django',
'Framework :: Django :: 2',
'Topic :: Internet :: WWW/HTTP :: Site Management',
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Framework :: Django",
"Framework :: Django :: 2.0",
"Topic :: Internet :: WWW/HTTP :: Site Management",
],
)

View File

@ -1 +1 @@
default_app_config = 'wagtail_personalisation.config.WagtailPersonalisationConfig'
default_app_config = "wagtail_personalisation.config.WagtailPersonalisationConfig"

View File

@ -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
@ -63,18 +61,13 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
def __init__(self, request):
super(SessionSegmentsAdapter, self).__init__(request)
self.request.session.setdefault('segments', [])
self.request.session.setdefault("segments", [])
self._segment_cache = None
def _segments(self, ids=None):
if not ids:
ids = []
segments = (
Segment.objects
.enabled()
.filter(persistent=True)
.filter(pk__in=ids)
)
segments = Segment.objects.enabled().filter(persistent=True).filter(pk__in=ids)
return segments
def get_segments(self, key="segments"):
@ -92,7 +85,7 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
if key not in self.request.session:
return []
raw_segments = self.request.session[key]
segment_ids = [segment['id'] for segment in raw_segments]
segment_ids = [segment["id"] for segment in raw_segments]
segments = self._segments(ids=segment_ids)
@ -115,7 +108,7 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
segment_ids = set()
for segment in segments:
serialized = create_segment_dictionary(segment)
if serialized['id'] in segment_ids:
if serialized["id"] in segment_ids:
continue
cache_segments.append(segment)
@ -141,41 +134,44 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
def add_page_visit(self, page):
"""Mark the page as visited by the user"""
visit_count = self.request.session.setdefault('visit_count', [])
page_visits = [visit for visit in visit_count if visit['id'] == page.pk]
visit_count = self.request.session.setdefault("visit_count", [])
page_visits = [visit for visit in visit_count if visit["id"] == page.pk]
if page_visits:
for page_visit in page_visits:
page_visit['count'] += 1
page_visit['path'] = page.url_path if page else self.request.path
page_visit["count"] += 1
page_visit["path"] = page.url_path if page else self.request.path
self.request.session.modified = True
else:
visit_count.append({
'slug': page.slug,
'id': page.pk,
'path': page.url_path if page else self.request.path,
'count': 1,
})
visit_count.append(
{
"slug": page.slug,
"id": page.pk,
"path": page.url_path if page else self.request.path,
"count": 1,
}
)
def get_visit_count(self, page=None):
"""Return the number of visits on the current request or given page"""
path = page.url_path if page else self.request.path
visit_count = self.request.session.setdefault('visit_count', [])
visit_count = self.request.session.setdefault("visit_count", [])
for visit in visit_count:
if visit['path'] == path:
return visit['count']
if visit["path"] == path:
return visit["count"]
return 0
def update_visit_count(self):
"""Update the visit count for all segments in the request session."""
segments = self.request.session['segments']
segment_pks = [s['id'] for s in segments]
segments = self.request.session["segments"]
segment_pks = [s["id"] for s in segments]
# Update counts
(Segment.objects
.enabled()
(
Segment.objects.enabled()
.filter(pk__in=segment_pks)
.update(visit_count=F('visit_count') + 1))
.update(visit_count=F("visit_count") + 1)
)
def refresh(self):
"""Retrieve the request session segments and verify whether or not they
@ -187,25 +183,31 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
current_segments = self.get_segments()
excluded_segments = self.get_segments("excluded_segments")
current_segments = list(
set(current_segments) - set(excluded_segments)
)
current_segments = list(set(current_segments) - set(excluded_segments))
# Run tests on all remaining enabled segments to verify applicability.
additional_segments = []
for segment in enabled_segments:
if segment.is_static and segment.static_users.filter(id=self.request.user.id).exists():
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 = []
for rule_model in rule_models:
segment_rules.extend(rule_model.objects.filter(segment=segment))
result = self._test_rules(segment_rules, self.request,
match_any=segment.match_any)
result = self._test_rules(
segment_rules, self.request, match_any=segment.match_any
)
if result and segment.randomise_into_segment():
if segment.is_static and not segment.is_full:
@ -223,14 +225,17 @@ class SessionSegmentsAdapter(BaseSegmentsAdapter):
self.update_visit_count()
SEGMENT_ADAPTER_CLASS = import_string(getattr(
settings,
'PERSONALISATION_SEGMENTS_ADAPTER',
'wagtail_personalisation.adapters.SessionSegmentsAdapter'))
SEGMENT_ADAPTER_CLASS = import_string(
getattr(
settings,
"PERSONALISATION_SEGMENTS_ADAPTER",
"wagtail_personalisation.adapters.SessionSegmentsAdapter",
)
)
def get_segment_adapter(request):
"""Return the Segment Adapter for the given request"""
if not hasattr(request, 'segment_adapter'):
if not hasattr(request, "segment_adapter"):
request.segment_adapter = SEGMENT_ADAPTER_CLASS(request)
return request.segment_adapter

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import admin
from wagtail_personalisation import models, rules
@ -10,6 +8,7 @@ class UserIsLoggedInRuleAdminInline(admin.TabularInline):
administration interface for segments.
"""
model = rules.UserIsLoggedInRule
@ -18,6 +17,7 @@ class TimeRuleAdminInline(admin.TabularInline):
administration interface for segments.
"""
model = rules.TimeRule
@ -26,6 +26,7 @@ class ReferralRuleAdminInline(admin.TabularInline):
administration interface for segments.
"""
model = rules.ReferralRule
@ -34,13 +35,19 @@ class VisitCountRuleAdminInline(admin.TabularInline):
administration interface for segments.
"""
model = rules.VisitCountRule
class SegmentAdmin(admin.ModelAdmin):
"""Add the inline models to the Segment admin interface."""
inlines = (UserIsLoggedInRuleAdminInline, TimeRuleAdminInline,
ReferralRuleAdminInline, VisitCountRuleAdminInline)
inlines = (
UserIsLoggedInRuleAdminInline,
TimeRuleAdminInline,
ReferralRuleAdminInline,
VisitCountRuleAdminInline,
)
admin.site.register(models.Segment, SegmentAdmin)

View File

@ -1,18 +1,24 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from django.urls import re_path
from wagtail_personalisation import views
app_name = 'segment'
app_name = "segment"
urlpatterns = [
url(r'^segment/(?P<segment_id>[0-9]+)/toggle/$',
views.toggle, name='toggle'),
url(r'^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$',
views.copy_page_view, name='copy_page'),
url(r'^segment/toggle_segment_view/$',
views.toggle_segment_view, name='toggle_segment_view'),
url(r'^segment/users/(?P<segment_id>[0-9]+)$',
views.segment_user_data, name='segment_user_data'),
re_path(r"^segment/(?P<segment_id>[0-9]+)/toggle/$", views.toggle, name="toggle"),
re_path(
r"^(?P<page_id>[0-9]+)/copy/(?P<segment_id>[0-9]+)$",
views.copy_page_view,
name="copy_page",
),
re_path(
r"^segment/toggle_segment_view/$",
views.toggle_segment_view,
name="toggle_segment_view",
),
re_path(
r"^segment/users/(?P<segment_id>[0-9]+)$",
views.segment_user_data,
name="segment_user_data",
),
]

View File

@ -1,6 +1,10 @@
from __future__ import absolute_import, unicode_literals
from django import VERSION as DJANGO_VERSION
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from wagtail.core import blocks
from wagtail_personalisation.adapters import get_segment_adapter
@ -8,7 +12,8 @@ from wagtail_personalisation.models import Segment
def list_segment_choices():
for pk, name in Segment.objects.values_list('pk', 'name'):
yield -1, ("Show to everyone")
for pk, name in Segment.objects.values_list("pk", "name"):
yield pk, name
@ -17,8 +22,10 @@ class PersonalisedStructBlock(blocks.StructBlock):
segment = blocks.ChoiceBlock(
choices=list_segment_choices,
required=False, label=_("Personalisation segment"),
help_text=_("Only show this content block for users in this segment"))
required=False,
label=_("Personalisation segment"),
help_text=_("Only show this content block for users in this segment"),
)
def render(self, value, context=None):
"""Only render this content block for users in this segment.
@ -31,14 +38,21 @@ class PersonalisedStructBlock(blocks.StructBlock):
:rtype: blocks.StructBlock or empty str
"""
request = context['request']
request = context["request"]
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']):
return super(PersonalisedStructBlock, self).render(
value, context)
if segment.id == segment_id:
return super(PersonalisedStructBlock, self).render(value, context)
if segment_id == -1:
return super(PersonalisedStructBlock, self).render(value, context)
return ""

View File

@ -1,11 +1,16 @@
from django import VERSION as DJANGO_VERSION
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
from django.utils.translation import ugettext_lazy as _
class WagtailPersonalisationConfig(AppConfig):
label = 'wagtail_personalisation'
name = 'wagtail_personalisation'
verbose_name = _('Wagtail Personalisation')
label = "wagtail_personalisation"
name = "wagtail_personalisation"
verbose_name = _("Wagtail Personalisation")
def ready(self):
from wagtail_personalisation import receivers

View File

@ -1,21 +1,25 @@
from __future__ import absolute_import, unicode_literals
import functools
from datetime import datetime
from importlib import import_module
from django import VERSION as DJANGO_VERSION
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 _
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
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:
@ -25,10 +29,8 @@ def user_from_data(user_id):
class SegmentAdminForm(WagtailAdminModelForm):
def count_matching_users(self, rules, match_any):
""" Calculates how many users match the given static rules
"""
"""Calculates how many users match the given static rules"""
count = 0
static_rules = [rule for rule in rules if rule.static]
@ -53,18 +55,28 @@ class SegmentAdminForm(WagtailAdminModelForm):
Segment = self._meta.model
rules = [
form.instance for formset in self.formsets.values()
form.instance
for formset in self.formsets.values()
for form in formset
if form not in formset.deleted_forms
]
consistent = rules and Segment.all_static(rules)
if cleaned_data.get('type') == Segment.TYPE_STATIC and not cleaned_data.get('count') and not consistent:
self.add_error('count', _('Static segments with non-static compatible rules must include a count.'))
if (
cleaned_data.get("type") == Segment.TYPE_STATIC
and not cleaned_data.get("count")
and not consistent
):
self.add_error(
"count",
_(
"Static segments with non-static compatible rules must include a count."
),
)
if self.instance.id and self.instance.is_static:
if self.has_changed():
self.add_error_to_fields(self, excluded=['name', 'enabled'])
self.add_error_to_fields(self, excluded=["name", "enabled"])
for formset in self.formsets.values():
if formset.has_changed():
@ -77,7 +89,7 @@ class SegmentAdminForm(WagtailAdminModelForm):
def add_error_to_fields(self, form, excluded=list()):
for field in form.changed_data:
if field not in excluded:
form.add_error(field, _('Cannot update a static segment'))
form.add_error(field, _("Cannot update a static segment"))
def save(self, *args, **kwargs):
is_new = not self.instance.id
@ -87,12 +99,14 @@ class SegmentAdminForm(WagtailAdminModelForm):
if is_new and self.instance.is_static and not self.instance.all_rules_static:
rules = [
form.instance for formset in self.formsets.values()
form.instance
for formset in self.formsets.values()
for form in formset
if form not in formset.deleted_forms
]
self.instance.matched_users_count = self.count_matching_users(
rules, self.instance.match_any)
rules, self.instance.match_any
)
self.instance.matched_count_updated_at = datetime.now()
instance = super(SegmentAdminForm, self).save(*args, **kwargs)
@ -100,7 +114,7 @@ class SegmentAdminForm(WagtailAdminModelForm):
if is_new and instance.is_static and instance.all_rules_static:
from .adapters import get_segment_adapter
request = RequestFactory().get('/')
request = RequestFactory().get("/")
request.session = SessionStore()
adapter = get_segment_adapter(request)
@ -113,7 +127,9 @@ class SegmentAdminForm(WagtailAdminModelForm):
matched_count = 0
for user in users.iterator():
request.user = user
passes = adapter._test_rules(instance.get_rules(), request, instance.match_any)
passes = adapter._test_rules(
instance.get_rules(), request, instance.match_any
)
if passes:
matched_count += 1
if instance.count == 0 or len(users_to_add) < instance.count:
@ -132,7 +148,5 @@ class SegmentAdminForm(WagtailAdminModelForm):
@property
def media(self):
media = super(SegmentAdminForm, self).media
media.add_js(
[static('js/segment_form_control.js')]
)
media.add_js([static("js/segment_form_control.js")])
return media

View File

@ -12,81 +12,189 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0001_initial'),
("wagtailcore", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='PersonalisablePage',
name="PersonalisablePage",
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')),
('is_segmented', models.BooleanField(default=False)),
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='variants', to='wagtail_personalisation.PersonalisablePage')),
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
),
("is_segmented", models.BooleanField(default=False)),
(
"canonical_page",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="variants",
to="wagtail_personalisation.PersonalisablePage",
),
),
],
options={
'abstract': False,
"abstract": False,
},
bases=('wagtailcore.page',),
bases=("wagtailcore.page",),
),
migrations.CreateModel(
name='ReferralRule',
name="ReferralRule",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('regex_string', models.TextField(verbose_name='Regex string to match the referer with')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"regex_string",
models.TextField(
verbose_name="Regex string to match the referer with"
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
migrations.CreateModel(
name='Segment',
name="Segment",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('create_date', models.DateTimeField(auto_now_add=True)),
('edit_date', models.DateTimeField(auto_now=True)),
('enable_date', models.DateTimeField(editable=False, null=True)),
('disable_date', models.DateTimeField(editable=False, null=True)),
('visit_count', models.PositiveIntegerField(default=0, editable=False)),
('status', models.CharField(choices=[('enabled', 'Enabled'), ('disabled', 'Disabled')], default='enabled', max_length=20)),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("create_date", models.DateTimeField(auto_now_add=True)),
("edit_date", models.DateTimeField(auto_now=True)),
("enable_date", models.DateTimeField(editable=False, null=True)),
("disable_date", models.DateTimeField(editable=False, null=True)),
("visit_count", models.PositiveIntegerField(default=0, editable=False)),
(
"status",
models.CharField(
choices=[("enabled", "Enabled"), ("disabled", "Disabled")],
default="enabled",
max_length=20,
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
migrations.CreateModel(
name='TimeRule',
name="TimeRule",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.TimeField(verbose_name='Starting time')),
('end_time', models.TimeField(verbose_name='Ending time')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_timerule_related', related_query_name='wagtail_personalisation_timerules', to='wagtail_personalisation.Segment')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("start_time", models.TimeField(verbose_name="Starting time")),
("end_time", models.TimeField(verbose_name="Ending time")),
(
"segment",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_timerule_related",
related_query_name="wagtail_personalisation_timerules",
to="wagtail_personalisation.Segment",
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
migrations.CreateModel(
name='VisitCountRule',
name="VisitCountRule",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('operator', models.CharField(choices=[('more_than', 'More than'), ('less_than', 'Less than'), ('equal_to', 'Equal to')], default='ht', max_length=20)),
('count', models.PositiveSmallIntegerField(default=0, null=True)),
('counted_page', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='wagtailcore.Page')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_visitcountrule_related', related_query_name='wagtail_personalisation_visitcountrules', to='wagtail_personalisation.Segment')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"operator",
models.CharField(
choices=[
("more_than", "More than"),
("less_than", "Less than"),
("equal_to", "Equal to"),
],
default="ht",
max_length=20,
),
),
("count", models.PositiveSmallIntegerField(default=0, null=True)),
(
"counted_page",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to="wagtailcore.Page",
),
),
(
"segment",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_visitcountrule_related",
related_query_name="wagtail_personalisation_visitcountrules",
to="wagtail_personalisation.Segment",
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
migrations.AddField(
model_name='referralrule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_referralrule_related', related_query_name='wagtail_personalisation_referralrules', to='wagtail_personalisation.Segment'),
model_name="referralrule",
name="segment",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_referralrule_related",
related_query_name="wagtail_personalisation_referralrules",
to="wagtail_personalisation.Segment",
),
),
migrations.AddField(
model_name='personalisablepage',
name='segment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='segments', to='wagtail_personalisation.Segment'),
model_name="personalisablepage",
name="segment",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="segments",
to="wagtail_personalisation.Segment",
),
),
]

View File

@ -10,25 +10,57 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0001_initial'),
("wagtail_personalisation", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='QueryRule',
name="QueryRule",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('query_parameter', models.TextField(verbose_name='The query parameter to search for')),
('query_value', models.TextField(verbose_name='The value of the parameter to match')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_queryrule_related', related_query_name='wagtail_personalisation_queryrules', to='wagtail_personalisation.Segment')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"query_parameter",
models.TextField(verbose_name="The query parameter to search for"),
),
(
"query_value",
models.TextField(
verbose_name="The value of the parameter to match"
),
),
(
"segment",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_queryrule_related",
related_query_name="wagtail_personalisation_queryrules",
to="wagtail_personalisation.Segment",
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
migrations.AlterField(
model_name='visitcountrule',
name='operator',
field=models.CharField(choices=[('more_than', 'More than'), ('less_than', 'Less than'), ('equal_to', 'Equal to')], default='more_than', max_length=20),
model_name="visitcountrule",
name="operator",
field=models.CharField(
choices=[
("more_than", "More than"),
("less_than", "Less than"),
("equal_to", "Equal to"),
],
default="more_than",
max_length=20,
),
),
]

View File

@ -8,28 +8,36 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0002_auto_20161205_1623'),
("wagtail_personalisation", "0002_auto_20161205_1623"),
]
operations = [
migrations.RemoveField(
model_name='queryrule',
name='query_parameter',
model_name="queryrule",
name="query_parameter",
),
migrations.RemoveField(
model_name='queryrule',
name='query_value',
model_name="queryrule",
name="query_value",
),
migrations.AddField(
model_name='queryrule',
name='parameter',
field=models.SlugField(default='test', max_length=20, verbose_name='The query parameter to search for'),
model_name="queryrule",
name="parameter",
field=models.SlugField(
default="test",
max_length=20,
verbose_name="The query parameter to search for",
),
preserve_default=False,
),
migrations.AddField(
model_name='queryrule',
name='value',
field=models.SlugField(default='test', max_length=20, verbose_name='The value of the parameter to match'),
model_name="queryrule",
name="value",
field=models.SlugField(
default="test",
max_length=20,
verbose_name="The value of the parameter to match",
),
preserve_default=False,
),
]

View File

@ -8,13 +8,15 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0003_auto_20161206_1005'),
("wagtail_personalisation", "0003_auto_20161206_1005"),
]
operations = [
migrations.AddField(
model_name='segment',
name='persistent',
field=models.BooleanField(default=False, help_text='Should the segment persist between visits?'),
model_name="segment",
name="persistent",
field=models.BooleanField(
default=False, help_text="Should the segment persist between visits?"
),
),
]

View File

@ -10,19 +10,35 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0004_segment_persistent'),
("wagtail_personalisation", "0004_segment_persistent"),
]
operations = [
migrations.CreateModel(
name='UserIsLoggedInRule',
name="UserIsLoggedInRule",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_logged_in', models.BooleanField(default=False)),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_userisloggedinrule_related', related_query_name='wagtail_personalisation_userisloggedinrules', to='wagtail_personalisation.Segment')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("is_logged_in", models.BooleanField(default=False)),
(
"segment",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_userisloggedinrule_related",
related_query_name="wagtail_personalisation_userisloggedinrules",
to="wagtail_personalisation.Segment",
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
]

View File

@ -8,13 +8,16 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0005_userisloggedinrule'),
("wagtail_personalisation", "0005_userisloggedinrule"),
]
operations = [
migrations.AddField(
model_name='segment',
name='match_any',
field=models.BooleanField(default=False, help_text='Should the segment match all the rules or just one of them?'),
model_name="segment",
name="match_any",
field=models.BooleanField(
default=False,
help_text="Should the segment match all the rules or just one of them?",
),
),
]

View File

@ -10,25 +10,41 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0006_segment_match_any'),
("wagtail_personalisation", "0006_segment_match_any"),
]
operations = [
migrations.CreateModel(
name='DayRule',
name="DayRule",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mon', models.BooleanField(default=False, verbose_name='Monday')),
('tue', models.BooleanField(default=False, verbose_name='Tuesday')),
('wed', models.BooleanField(default=False, verbose_name='Wednesday')),
('thu', models.BooleanField(default=False, verbose_name='Thursday')),
('fri', models.BooleanField(default=False, verbose_name='Friday')),
('sat', models.BooleanField(default=False, verbose_name='Saturday')),
('sun', models.BooleanField(default=False, verbose_name='Sunday')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_dayrule_related', related_query_name='wagtail_personalisation_dayrules', to='wagtail_personalisation.Segment')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("mon", models.BooleanField(default=False, verbose_name="Monday")),
("tue", models.BooleanField(default=False, verbose_name="Tuesday")),
("wed", models.BooleanField(default=False, verbose_name="Wednesday")),
("thu", models.BooleanField(default=False, verbose_name="Thursday")),
("fri", models.BooleanField(default=False, verbose_name="Friday")),
("sat", models.BooleanField(default=False, verbose_name="Saturday")),
("sun", models.BooleanField(default=False, verbose_name="Sunday")),
(
"segment",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_dayrule_related",
related_query_name="wagtail_personalisation_dayrules",
to="wagtail_personalisation.Segment",
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
]

View File

@ -10,21 +10,40 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0007_dayrule'),
("wagtail_personalisation", "0007_dayrule"),
]
operations = [
migrations.CreateModel(
name='DeviceRule',
name="DeviceRule",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('mobile', models.BooleanField(default=False, verbose_name='Mobile phone')),
('tablet', models.BooleanField(default=False, verbose_name='Tablet')),
('desktop', models.BooleanField(default=False, verbose_name='Desktop')),
('segment', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_devicerule_related', related_query_name='wagtail_personalisation_devicerules', to='wagtail_personalisation.Segment')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"mobile",
models.BooleanField(default=False, verbose_name="Mobile phone"),
),
("tablet", models.BooleanField(default=False, verbose_name="Tablet")),
("desktop", models.BooleanField(default=False, verbose_name="Desktop")),
(
"segment",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_devicerule_related",
related_query_name="wagtail_personalisation_devicerules",
to="wagtail_personalisation.Segment",
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
]

View File

@ -8,23 +8,23 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0008_devicerule'),
("wagtail_personalisation", "0008_devicerule"),
]
operations = [
migrations.RemoveField(
model_name='personalisablepage',
name='canonical_page',
model_name="personalisablepage",
name="canonical_page",
),
migrations.RemoveField(
model_name='personalisablepage',
name='page_ptr',
model_name="personalisablepage",
name="page_ptr",
),
migrations.RemoveField(
model_name='personalisablepage',
name='segment',
model_name="personalisablepage",
name="segment",
),
migrations.DeleteModel(
name='PersonalisablePage',
name="PersonalisablePage",
),
]

View File

@ -8,41 +8,43 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0009_auto_20170531_0428'),
("wagtail_personalisation", "0009_auto_20170531_0428"),
]
operations = [
migrations.AlterModelOptions(
name='dayrule',
options={'verbose_name': 'Day Rule'},
name="dayrule",
options={"verbose_name": "Day Rule"},
),
migrations.AlterModelOptions(
name='devicerule',
options={'verbose_name': 'Device Rule'},
name="devicerule",
options={"verbose_name": "Device Rule"},
),
migrations.AlterModelOptions(
name='queryrule',
options={'verbose_name': 'Query Rule'},
name="queryrule",
options={"verbose_name": "Query Rule"},
),
migrations.AlterModelOptions(
name='referralrule',
options={'verbose_name': 'Referral Rule'},
name="referralrule",
options={"verbose_name": "Referral Rule"},
),
migrations.AlterModelOptions(
name='timerule',
options={'verbose_name': 'Time Rule'},
name="timerule",
options={"verbose_name": "Time Rule"},
),
migrations.AlterModelOptions(
name='userisloggedinrule',
options={'verbose_name': 'Logged in Rule'},
name="userisloggedinrule",
options={"verbose_name": "Logged in Rule"},
),
migrations.AlterModelOptions(
name='visitcountrule',
options={'verbose_name': 'Visit count Rule'},
name="visitcountrule",
options={"verbose_name": "Visit count Rule"},
),
migrations.AlterField(
model_name='referralrule',
name='regex_string',
field=models.TextField(verbose_name='Regular expression to match the referrer'),
model_name="referralrule",
name="regex_string",
field=models.TextField(
verbose_name="Regular expression to match the referrer"
),
),
]

View File

@ -9,22 +9,55 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0001_initial'),
('wagtail_personalisation', '0010_auto_20170531_1101'),
("wagtailcore", "0001_initial"),
("wagtail_personalisation", "0010_auto_20170531_1101"),
]
operations = [
migrations.CreateModel(
name='PersonalisablePageMetadata',
name="PersonalisablePageMetadata",
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_segmented', models.BooleanField(default=False)),
('canonical_page', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='personalisable_canonical_metadata', to='wagtailcore.Page')),
('segment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='page_metadata', to='wagtail_personalisation.Segment')),
('variant', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page')),
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("is_segmented", models.BooleanField(default=False)),
(
"canonical_page",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="personalisable_canonical_metadata",
to="wagtailcore.Page",
),
),
(
"segment",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="page_metadata",
to="wagtail_personalisation.Segment",
),
),
(
"variant",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="_personalisable_page_metadata",
to="wagtailcore.Page",
),
),
],
options={
'abstract': False,
"abstract": False,
},
),
]

View File

@ -8,12 +8,12 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0011_personalisablepagemetadata'),
("wagtail_personalisation", "0011_personalisablepagemetadata"),
]
operations = [
migrations.RemoveField(
model_name='personalisablepagemetadata',
name='is_segmented',
model_name="personalisablepagemetadata",
name="is_segmented",
),
]

View File

@ -8,24 +8,35 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sessions', '0001_initial'),
('wagtail_personalisation', '0012_remove_personalisablepagemetadata_is_segmented'),
("sessions", "0001_initial"),
(
"wagtail_personalisation",
"0012_remove_personalisablepagemetadata_is_segmented",
),
]
operations = [
migrations.AddField(
model_name='segment',
name='count',
field=models.PositiveSmallIntegerField(default=0, help_text='If this number is set for a static segment users will be added to the set until the number is reached. After this no more users will be added.'),
model_name="segment",
name="count",
field=models.PositiveSmallIntegerField(
default=0,
help_text="If this number is set for a static segment users will be added to the set until the number is reached. After this no more users will be added.",
),
),
migrations.AddField(
model_name='segment',
name='sessions',
field=models.ManyToManyField(to='sessions.Session'),
model_name="segment",
name="sessions",
field=models.ManyToManyField(to="sessions.Session"),
),
migrations.AddField(
model_name='segment',
name='type',
field=models.CharField(choices=[('dynamic', 'Dynamic'), ('static', 'Static')], default='dynamic', help_text='\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ', max_length=20),
model_name="segment",
name="type",
field=models.CharField(
choices=[("dynamic", "Dynamic"), ("static", "Static")],
default="dynamic",
help_text="\n </br></br><strong>Dynamic:</strong> Users in this segment will change\n as more or less meet the rules specified in the segment.\n </br><strong>Static:</strong> If the segment contains only static\n compatible rules the segment will contain the members that pass\n those rules when the segment is created. Mixed static segments or\n those containing entirely non static compatible rules will be\n populated using the count variable.\n ",
max_length=20,
),
),
]

View File

@ -10,17 +10,17 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtail_personalisation', '0013_add_dynamic_static_to_segment'),
("wagtail_personalisation", "0013_add_dynamic_static_to_segment"),
]
operations = [
migrations.RemoveField(
model_name='segment',
name='sessions',
model_name="segment",
name="sessions",
),
migrations.AddField(
model_name='segment',
name='static_users',
model_name="segment",
name="static_users",
field=models.ManyToManyField(to=settings.AUTH_USER_MODEL),
),
]

View File

@ -8,18 +8,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0015_static_users'),
("wagtail_personalisation", "0015_static_users"),
]
operations = [
migrations.AddField(
model_name='segment',
name='matched_count_updated_at',
model_name="segment",
name="matched_count_updated_at",
field=models.DateTimeField(editable=False, null=True),
),
migrations.AddField(
model_name='segment',
name='matched_users_count',
model_name="segment",
name="matched_users_count",
field=models.PositiveIntegerField(default=0, editable=False),
),
]

View File

@ -9,13 +9,22 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0016_auto_20180125_0918'),
("wagtail_personalisation", "0016_auto_20180125_0918"),
]
operations = [
migrations.AddField(
model_name='segment',
name='randomisation_percent',
field=models.PositiveSmallIntegerField(blank=True, default=None, help_text='If this number is set each user matching the rules will have this percentage chance of being placed in the segment.', null=True, validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(0)]),
model_name="segment",
name="randomisation_percent",
field=models.PositiveSmallIntegerField(
blank=True,
default=None,
help_text="If this number is set each user matching the rules will have this percentage chance of being placed in the segment.",
null=True,
validators=[
django.core.validators.MaxValueValidator(100),
django.core.validators.MinValueValidator(0),
],
),
),
]

View File

@ -10,13 +10,17 @@ class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('wagtail_personalisation', '0017_segment_randomisation_percent'),
("wagtail_personalisation", "0017_segment_randomisation_percent"),
]
operations = [
migrations.AddField(
model_name='segment',
name='excluded_users',
field=models.ManyToManyField(help_text='Users that matched the rules but were excluded from the segment for some reason e.g. randomisation', related_name='excluded_segments', to=settings.AUTH_USER_MODEL),
model_name="segment",
name="excluded_users",
field=models.ManyToManyField(
help_text="Users that matched the rules but were excluded from the segment for some reason e.g. randomisation",
related_name="excluded_segments",
to=settings.AUTH_USER_MODEL,
),
),
]

View File

@ -7,13 +7,19 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0018_segment_excluded_users'),
("wagtail_personalisation", "0018_segment_excluded_users"),
]
operations = [
migrations.AlterField(
model_name='personalisablepagemetadata',
name='segment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='page_metadata', to='wagtail_personalisation.Segment'),
model_name="personalisablepagemetadata",
name="segment",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="page_metadata",
to="wagtail_personalisation.Segment",
),
),
]

View File

@ -8,43 +8,71 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0019_auto_20180526_1425'),
("wagtail_personalisation", "0019_auto_20180526_1425"),
]
operations = [
migrations.AlterField(
model_name='dayrule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_dayrules', to='wagtail_personalisation.Segment'),
model_name="dayrule",
name="segment",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_dayrules",
to="wagtail_personalisation.Segment",
),
),
migrations.AlterField(
model_name='devicerule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_devicerules', to='wagtail_personalisation.Segment'),
model_name="devicerule",
name="segment",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_devicerules",
to="wagtail_personalisation.Segment",
),
),
migrations.AlterField(
model_name='queryrule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_queryrules', to='wagtail_personalisation.Segment'),
model_name="queryrule",
name="segment",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_queryrules",
to="wagtail_personalisation.Segment",
),
),
migrations.AlterField(
model_name='referralrule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_referralrules', to='wagtail_personalisation.Segment'),
model_name="referralrule",
name="segment",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_referralrules",
to="wagtail_personalisation.Segment",
),
),
migrations.AlterField(
model_name='timerule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_timerules', to='wagtail_personalisation.Segment'),
model_name="timerule",
name="segment",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_timerules",
to="wagtail_personalisation.Segment",
),
),
migrations.AlterField(
model_name='userisloggedinrule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_userisloggedinrules', to='wagtail_personalisation.Segment'),
model_name="userisloggedinrule",
name="segment",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_userisloggedinrules",
to="wagtail_personalisation.Segment",
),
),
migrations.AlterField(
model_name='visitcountrule',
name='segment',
field=modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='wagtail_personalisation_visitcountrules', to='wagtail_personalisation.Segment'),
model_name="visitcountrule",
name="segment",
field=modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_visitcountrules",
to="wagtail_personalisation.Segment",
),
),
]

View File

@ -1,19 +1,24 @@
# 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):
dependencies = [
('wagtail_personalisation', '0020_rules_delete_relatedqueryname'),
("wagtail_personalisation", "0020_rules_delete_relatedqueryname"),
]
operations = [
migrations.AlterField(
model_name='personalisablepagemetadata',
name='segment',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='page_metadata', to='wagtail_personalisation.Segment'),
model_name="personalisablepagemetadata",
name="segment",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="page_metadata",
to="wagtail_personalisation.Segment",
),
),
]

View File

@ -7,13 +7,21 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtail_personalisation', '0021_personalisablepagemetadata_segment_set_on_delete_protect'),
(
"wagtail_personalisation",
"0021_personalisablepagemetadata_segment_set_on_delete_protect",
),
]
operations = [
migrations.AlterField(
model_name='personalisablepagemetadata',
name='canonical_page',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='personalisable_canonical_metadata', to='wagtailcore.Page'),
model_name="personalisablepagemetadata",
name="canonical_page",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="personalisable_canonical_metadata",
to="wagtailcore.Page",
),
),
]

View File

@ -1,19 +1,27 @@
# 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):
dependencies = [
('wagtail_personalisation', '0022_personalisablepagemetadata_canonical_protect'),
(
"wagtail_personalisation",
"0022_personalisablepagemetadata_canonical_protect",
),
]
operations = [
migrations.AlterField(
model_name='personalisablepagemetadata',
name='variant',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_personalisable_page_metadata', to='wagtailcore.Page'),
model_name="personalisablepagemetadata",
name="variant",
field=models.OneToOneField(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="_personalisable_page_metadata",
to="wagtailcore.Page",
),
),
]

View File

@ -0,0 +1,298 @@
# Generated by Django 2.0.6 on 2018-08-10 14:39
import django.db.models.deletion
import modelcluster.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtail_personalisation", "0023_personalisablepagemetadata_variant_cascade"),
]
operations = [
migrations.CreateModel(
name="OriginCountryRule",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"country",
models.CharField(
choices=[
("aw", "Aruba"),
("af", "Afghanistan"),
("ao", "Angola"),
("ai", "Anguilla"),
("ax", "Åland Islands"),
("al", "Albania"),
("ad", "Andorra"),
("ae", "United Arab Emirates"),
("ar", "Argentina"),
("am", "Armenia"),
("as", "American Samoa"),
("aq", "Antarctica"),
("tf", "French Southern Territories"),
("ag", "Antigua and Barbuda"),
("au", "Australia"),
("at", "Austria"),
("az", "Azerbaijan"),
("bi", "Burundi"),
("be", "Belgium"),
("bj", "Benin"),
("bq", "Bonaire, Sint Eustatius and Saba"),
("bf", "Burkina Faso"),
("bd", "Bangladesh"),
("bg", "Bulgaria"),
("bh", "Bahrain"),
("bs", "Bahamas"),
("ba", "Bosnia and Herzegovina"),
("bl", "Saint Barthélemy"),
("by", "Belarus"),
("bz", "Belize"),
("bm", "Bermuda"),
("bo", "Bolivia, Plurinational State of"),
("br", "Brazil"),
("bb", "Barbados"),
("bn", "Brunei Darussalam"),
("bt", "Bhutan"),
("bv", "Bouvet Island"),
("bw", "Botswana"),
("cf", "Central African Republic"),
("ca", "Canada"),
("cc", "Cocos (Keeling) Islands"),
("ch", "Switzerland"),
("cl", "Chile"),
("cn", "China"),
("ci", "Côte d'Ivoire"),
("cm", "Cameroon"),
("cd", "Congo, The Democratic Republic of the"),
("cg", "Congo"),
("ck", "Cook Islands"),
("co", "Colombia"),
("km", "Comoros"),
("cv", "Cabo Verde"),
("cr", "Costa Rica"),
("cu", "Cuba"),
("cw", "Curaçao"),
("cx", "Christmas Island"),
("ky", "Cayman Islands"),
("cy", "Cyprus"),
("cz", "Czechia"),
("de", "Germany"),
("dj", "Djibouti"),
("dm", "Dominica"),
("dk", "Denmark"),
("do", "Dominican Republic"),
("dz", "Algeria"),
("ec", "Ecuador"),
("eg", "Egypt"),
("er", "Eritrea"),
("eh", "Western Sahara"),
("es", "Spain"),
("ee", "Estonia"),
("et", "Ethiopia"),
("fi", "Finland"),
("fj", "Fiji"),
("fk", "Falkland Islands (Malvinas)"),
("fr", "France"),
("fo", "Faroe Islands"),
("fm", "Micronesia, Federated States of"),
("ga", "Gabon"),
("gb", "United Kingdom"),
("ge", "Georgia"),
("gg", "Guernsey"),
("gh", "Ghana"),
("gi", "Gibraltar"),
("gn", "Guinea"),
("gp", "Guadeloupe"),
("gm", "Gambia"),
("gw", "Guinea-Bissau"),
("gq", "Equatorial Guinea"),
("gr", "Greece"),
("gd", "Grenada"),
("gl", "Greenland"),
("gt", "Guatemala"),
("gf", "French Guiana"),
("gu", "Guam"),
("gy", "Guyana"),
("hk", "Hong Kong"),
("hm", "Heard Island and McDonald Islands"),
("hn", "Honduras"),
("hr", "Croatia"),
("ht", "Haiti"),
("hu", "Hungary"),
("id", "Indonesia"),
("im", "Isle of Man"),
("in", "India"),
("io", "British Indian Ocean Territory"),
("ie", "Ireland"),
("ir", "Iran, Islamic Republic of"),
("iq", "Iraq"),
("is", "Iceland"),
("il", "Israel"),
("it", "Italy"),
("jm", "Jamaica"),
("je", "Jersey"),
("jo", "Jordan"),
("jp", "Japan"),
("kz", "Kazakhstan"),
("ke", "Kenya"),
("kg", "Kyrgyzstan"),
("kh", "Cambodia"),
("ki", "Kiribati"),
("kn", "Saint Kitts and Nevis"),
("kr", "Korea, Republic of"),
("kw", "Kuwait"),
("la", "Lao People's Democratic Republic"),
("lb", "Lebanon"),
("lr", "Liberia"),
("ly", "Libya"),
("lc", "Saint Lucia"),
("li", "Liechtenstein"),
("lk", "Sri Lanka"),
("ls", "Lesotho"),
("lt", "Lithuania"),
("lu", "Luxembourg"),
("lv", "Latvia"),
("mo", "Macao"),
("mf", "Saint Martin (French part)"),
("ma", "Morocco"),
("mc", "Monaco"),
("md", "Moldova, Republic of"),
("mg", "Madagascar"),
("mv", "Maldives"),
("mx", "Mexico"),
("mh", "Marshall Islands"),
("mk", "Macedonia, Republic of"),
("ml", "Mali"),
("mt", "Malta"),
("mm", "Myanmar"),
("me", "Montenegro"),
("mn", "Mongolia"),
("mp", "Northern Mariana Islands"),
("mz", "Mozambique"),
("mr", "Mauritania"),
("ms", "Montserrat"),
("mq", "Martinique"),
("mu", "Mauritius"),
("mw", "Malawi"),
("my", "Malaysia"),
("yt", "Mayotte"),
("na", "Namibia"),
("nc", "New Caledonia"),
("ne", "Niger"),
("nf", "Norfolk Island"),
("ng", "Nigeria"),
("ni", "Nicaragua"),
("nu", "Niue"),
("nl", "Netherlands"),
("no", "Norway"),
("np", "Nepal"),
("nr", "Nauru"),
("nz", "New Zealand"),
("om", "Oman"),
("pk", "Pakistan"),
("pa", "Panama"),
("pn", "Pitcairn"),
("pe", "Peru"),
("ph", "Philippines"),
("pw", "Palau"),
("pg", "Papua New Guinea"),
("pl", "Poland"),
("pr", "Puerto Rico"),
("kp", "Korea, Democratic People's Republic of"),
("pt", "Portugal"),
("py", "Paraguay"),
("ps", "Palestine, State of"),
("pf", "French Polynesia"),
("qa", "Qatar"),
("re", "Réunion"),
("ro", "Romania"),
("ru", "Russian Federation"),
("rw", "Rwanda"),
("sa", "Saudi Arabia"),
("sd", "Sudan"),
("sn", "Senegal"),
("sg", "Singapore"),
("gs", "South Georgia and the South Sandwich Islands"),
("sh", "Saint Helena, Ascension and Tristan da Cunha"),
("sj", "Svalbard and Jan Mayen"),
("sb", "Solomon Islands"),
("sl", "Sierra Leone"),
("sv", "El Salvador"),
("sm", "San Marino"),
("so", "Somalia"),
("pm", "Saint Pierre and Miquelon"),
("rs", "Serbia"),
("ss", "South Sudan"),
("st", "Sao Tome and Principe"),
("sr", "Suriname"),
("sk", "Slovakia"),
("si", "Slovenia"),
("se", "Sweden"),
("sz", "Swaziland"),
("sx", "Sint Maarten (Dutch part)"),
("sc", "Seychelles"),
("sy", "Syrian Arab Republic"),
("tc", "Turks and Caicos Islands"),
("td", "Chad"),
("tg", "Togo"),
("th", "Thailand"),
("tj", "Tajikistan"),
("tk", "Tokelau"),
("tm", "Turkmenistan"),
("tl", "Timor-Leste"),
("to", "Tonga"),
("tt", "Trinidad and Tobago"),
("tn", "Tunisia"),
("tr", "Turkey"),
("tv", "Tuvalu"),
("tw", "Taiwan, Province of China"),
("tz", "Tanzania, United Republic of"),
("ug", "Uganda"),
("ua", "Ukraine"),
("um", "United States Minor Outlying Islands"),
("uy", "Uruguay"),
("us", "United States"),
("uz", "Uzbekistan"),
("va", "Holy See (Vatican City State)"),
("vc", "Saint Vincent and the Grenadines"),
("ve", "Venezuela, Bolivarian Republic of"),
("vg", "Virgin Islands, British"),
("vi", "Virgin Islands, U.S."),
("vn", "Viet Nam"),
("vu", "Vanuatu"),
("wf", "Wallis and Futuna"),
("ws", "Samoa"),
("ye", "Yemen"),
("za", "South Africa"),
("zm", "Zambia"),
("zw", "Zimbabwe"),
],
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.",
max_length=2,
),
),
(
"segment",
modelcluster.fields.ParentalKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wagtail_personalisation_origincountryrules",
to="wagtail_personalisation.Segment",
),
),
],
options={
"verbose_name": "origin country rule",
},
),
]

View File

@ -0,0 +1,272 @@
# Generated by Django 2.1.11 on 2019-08-22 06:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("wagtail_personalisation", "0024_origincountryrule"),
]
operations = [
migrations.AlterField(
model_name="origincountryrule",
name="country",
field=models.CharField(
choices=[
("aw", "Aruba"),
("af", "Afghanistan"),
("ao", "Angola"),
("ai", "Anguilla"),
("ax", "Åland Islands"),
("al", "Albania"),
("ad", "Andorra"),
("ae", "United Arab Emirates"),
("ar", "Argentina"),
("am", "Armenia"),
("as", "American Samoa"),
("aq", "Antarctica"),
("tf", "French Southern Territories"),
("ag", "Antigua and Barbuda"),
("au", "Australia"),
("at", "Austria"),
("az", "Azerbaijan"),
("bi", "Burundi"),
("be", "Belgium"),
("bj", "Benin"),
("bq", "Bonaire, Sint Eustatius and Saba"),
("bf", "Burkina Faso"),
("bd", "Bangladesh"),
("bg", "Bulgaria"),
("bh", "Bahrain"),
("bs", "Bahamas"),
("ba", "Bosnia and Herzegovina"),
("bl", "Saint Barthélemy"),
("by", "Belarus"),
("bz", "Belize"),
("bm", "Bermuda"),
("bo", "Bolivia, Plurinational State of"),
("br", "Brazil"),
("bb", "Barbados"),
("bn", "Brunei Darussalam"),
("bt", "Bhutan"),
("bv", "Bouvet Island"),
("bw", "Botswana"),
("cf", "Central African Republic"),
("ca", "Canada"),
("cc", "Cocos (Keeling) Islands"),
("ch", "Switzerland"),
("cl", "Chile"),
("cn", "China"),
("ci", "Côte d'Ivoire"),
("cm", "Cameroon"),
("cd", "Congo, The Democratic Republic of the"),
("cg", "Congo"),
("ck", "Cook Islands"),
("co", "Colombia"),
("km", "Comoros"),
("cv", "Cabo Verde"),
("cr", "Costa Rica"),
("cu", "Cuba"),
("cw", "Curaçao"),
("cx", "Christmas Island"),
("ky", "Cayman Islands"),
("cy", "Cyprus"),
("cz", "Czechia"),
("de", "Germany"),
("dj", "Djibouti"),
("dm", "Dominica"),
("dk", "Denmark"),
("do", "Dominican Republic"),
("dz", "Algeria"),
("ec", "Ecuador"),
("eg", "Egypt"),
("er", "Eritrea"),
("eh", "Western Sahara"),
("es", "Spain"),
("ee", "Estonia"),
("et", "Ethiopia"),
("fi", "Finland"),
("fj", "Fiji"),
("fk", "Falkland Islands (Malvinas)"),
("fr", "France"),
("fo", "Faroe Islands"),
("fm", "Micronesia, Federated States of"),
("ga", "Gabon"),
("gb", "United Kingdom"),
("ge", "Georgia"),
("gg", "Guernsey"),
("gh", "Ghana"),
("gi", "Gibraltar"),
("gn", "Guinea"),
("gp", "Guadeloupe"),
("gm", "Gambia"),
("gw", "Guinea-Bissau"),
("gq", "Equatorial Guinea"),
("gr", "Greece"),
("gd", "Grenada"),
("gl", "Greenland"),
("gt", "Guatemala"),
("gf", "French Guiana"),
("gu", "Guam"),
("gy", "Guyana"),
("hk", "Hong Kong"),
("hm", "Heard Island and McDonald Islands"),
("hn", "Honduras"),
("hr", "Croatia"),
("ht", "Haiti"),
("hu", "Hungary"),
("id", "Indonesia"),
("im", "Isle of Man"),
("in", "India"),
("io", "British Indian Ocean Territory"),
("ie", "Ireland"),
("ir", "Iran, Islamic Republic of"),
("iq", "Iraq"),
("is", "Iceland"),
("il", "Israel"),
("it", "Italy"),
("jm", "Jamaica"),
("je", "Jersey"),
("jo", "Jordan"),
("jp", "Japan"),
("kz", "Kazakhstan"),
("ke", "Kenya"),
("kg", "Kyrgyzstan"),
("kh", "Cambodia"),
("ki", "Kiribati"),
("kn", "Saint Kitts and Nevis"),
("kr", "Korea, Republic of"),
("kw", "Kuwait"),
("la", "Lao People's Democratic Republic"),
("lb", "Lebanon"),
("lr", "Liberia"),
("ly", "Libya"),
("lc", "Saint Lucia"),
("li", "Liechtenstein"),
("lk", "Sri Lanka"),
("ls", "Lesotho"),
("lt", "Lithuania"),
("lu", "Luxembourg"),
("lv", "Latvia"),
("mo", "Macao"),
("mf", "Saint Martin (French part)"),
("ma", "Morocco"),
("mc", "Monaco"),
("md", "Moldova, Republic of"),
("mg", "Madagascar"),
("mv", "Maldives"),
("mx", "Mexico"),
("mh", "Marshall Islands"),
("mk", "North Macedonia"),
("ml", "Mali"),
("mt", "Malta"),
("mm", "Myanmar"),
("me", "Montenegro"),
("mn", "Mongolia"),
("mp", "Northern Mariana Islands"),
("mz", "Mozambique"),
("mr", "Mauritania"),
("ms", "Montserrat"),
("mq", "Martinique"),
("mu", "Mauritius"),
("mw", "Malawi"),
("my", "Malaysia"),
("yt", "Mayotte"),
("na", "Namibia"),
("nc", "New Caledonia"),
("ne", "Niger"),
("nf", "Norfolk Island"),
("ng", "Nigeria"),
("ni", "Nicaragua"),
("nu", "Niue"),
("nl", "Netherlands"),
("no", "Norway"),
("np", "Nepal"),
("nr", "Nauru"),
("nz", "New Zealand"),
("om", "Oman"),
("pk", "Pakistan"),
("pa", "Panama"),
("pn", "Pitcairn"),
("pe", "Peru"),
("ph", "Philippines"),
("pw", "Palau"),
("pg", "Papua New Guinea"),
("pl", "Poland"),
("pr", "Puerto Rico"),
("kp", "Korea, Democratic People's Republic of"),
("pt", "Portugal"),
("py", "Paraguay"),
("ps", "Palestine, State of"),
("pf", "French Polynesia"),
("qa", "Qatar"),
("re", "Réunion"),
("ro", "Romania"),
("ru", "Russian Federation"),
("rw", "Rwanda"),
("sa", "Saudi Arabia"),
("sd", "Sudan"),
("sn", "Senegal"),
("sg", "Singapore"),
("gs", "South Georgia and the South Sandwich Islands"),
("sh", "Saint Helena, Ascension and Tristan da Cunha"),
("sj", "Svalbard and Jan Mayen"),
("sb", "Solomon Islands"),
("sl", "Sierra Leone"),
("sv", "El Salvador"),
("sm", "San Marino"),
("so", "Somalia"),
("pm", "Saint Pierre and Miquelon"),
("rs", "Serbia"),
("ss", "South Sudan"),
("st", "Sao Tome and Principe"),
("sr", "Suriname"),
("sk", "Slovakia"),
("si", "Slovenia"),
("se", "Sweden"),
("sz", "Eswatini"),
("sx", "Sint Maarten (Dutch part)"),
("sc", "Seychelles"),
("sy", "Syrian Arab Republic"),
("tc", "Turks and Caicos Islands"),
("td", "Chad"),
("tg", "Togo"),
("th", "Thailand"),
("tj", "Tajikistan"),
("tk", "Tokelau"),
("tm", "Turkmenistan"),
("tl", "Timor-Leste"),
("to", "Tonga"),
("tt", "Trinidad and Tobago"),
("tn", "Tunisia"),
("tr", "Turkey"),
("tv", "Tuvalu"),
("tw", "Taiwan, Province of China"),
("tz", "Tanzania, United Republic of"),
("ug", "Uganda"),
("ua", "Ukraine"),
("um", "United States Minor Outlying Islands"),
("uy", "Uruguay"),
("us", "United States"),
("uz", "Uzbekistan"),
("va", "Holy See (Vatican City State)"),
("vc", "Saint Vincent and the Grenadines"),
("ve", "Venezuela, Bolivarian Republic of"),
("vg", "Virgin Islands, British"),
("vi", "Virgin Islands, U.S."),
("vn", "Viet Nam"),
("vu", "Vanuatu"),
("wf", "Wallis and Futuna"),
("ws", "Samoa"),
("ye", "Yemen"),
("za", "South Africa"),
("zm", "Zambia"),
("zw", "Zimbabwe"),
],
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.",
max_length=2,
),
),
]

View File

@ -1,17 +1,27 @@
import random
import wagtail
from django import VERSION as DJANGO_VERSION
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 _
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel
from wagtail.admin.edit_handlers import (
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
FieldPanel,
FieldRowPanel,
InlinePanel,
MultiFieldPanel,
)
from wagtail.core.models import Page
from wagtail_personalisation.rules import AbstractBaseRule
@ -20,28 +30,36 @@ 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'
STATUS_DISABLED = 'disabled'
STATUS_ENABLED = "enabled"
STATUS_DISABLED = "disabled"
STATUS_CHOICES = (
(STATUS_ENABLED, _('Enabled')),
(STATUS_DISABLED, _('Disabled')),
(STATUS_ENABLED, _("Enabled")),
(STATUS_DISABLED, _("Disabled")),
)
TYPE_DYNAMIC = 'dynamic'
TYPE_STATIC = 'static'
TYPE_DYNAMIC = "dynamic"
TYPE_STATIC = "static"
TYPE_CHOICES = (
(TYPE_DYNAMIC, _('Dynamic')),
(TYPE_STATIC, _('Static')),
(TYPE_DYNAMIC, _("Dynamic")),
(TYPE_STATIC, _("Static")),
)
name = models.CharField(max_length=255)
@ -51,18 +69,22 @@ class Segment(ClusterableModel):
disable_date = models.DateTimeField(null=True, editable=False)
visit_count = models.PositiveIntegerField(default=0, editable=False)
status = models.CharField(
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED)
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED
)
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(
default=False,
help_text=_("Should the segment match all the rules or just one of them?")
help_text=_("Should the segment match all the rules or just one of them?"),
)
type = models.CharField(
max_length=20,
choices=TYPE_CHOICES,
default=TYPE_DYNAMIC,
help_text=mark_safe(_("""
help_text=mark_safe(
_(
"""
</br></br><strong>Dynamic:</strong> Users in this segment will change
as more or less meet the rules specified in the segment.
</br><strong>Static:</strong> If the segment contains only static
@ -70,37 +92,42 @@ class Segment(ClusterableModel):
those rules when the segment is created. Mixed static segments or
those containing entirely non static compatible rules will be
populated using the count variable.
"""))
"""
)
),
)
count = models.PositiveSmallIntegerField(
default=0,
help_text=_(
"If this number is set for a static segment users will be added to the "
"set until the number is reached. After this no more users will be added."
)
),
)
static_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
)
excluded_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
help_text=_("Users that matched the rules but were excluded from the "
"segment for some reason e.g. randomisation"),
related_name="excluded_segments"
help_text=_(
"Users that matched the rules but were excluded from the "
"segment for some reason e.g. randomisation"
),
related_name="excluded_segments",
)
matched_users_count = models.PositiveIntegerField(default=0, editable=False)
matched_count_updated_at = models.DateTimeField(null=True, editable=False)
randomisation_percent = models.PositiveSmallIntegerField(
null=True, blank=True, default=None,
null=True,
blank=True,
default=None,
help_text=_(
"If this number is set each user matching the rules will "
"have this percentage chance of being placed in the segment."
), validators=[
MaxValueValidator(100),
MinValueValidator(0)
])
),
validators=[MaxValueValidator(100), MinValueValidator(0)],
)
objects = SegmentQuerySet.as_manager()
@ -108,26 +135,37 @@ class Segment(ClusterableModel):
def __init__(self, *args, **kwargs):
Segment.panels = [
MultiFieldPanel([
FieldPanel('name', classname="title"),
FieldRowPanel([
FieldPanel('status'),
FieldPanel('persistent'),
]),
FieldPanel('match_any'),
FieldPanel('type', widget=forms.RadioSelect),
FieldPanel('count', classname='count_field'),
FieldPanel('randomisation_percent', classname='percent_field'),
], heading="Segment"),
MultiFieldPanel([
InlinePanel(
"{}s".format(rule_model._meta.db_table),
label='{}{}'.format(
rule_model._meta.verbose_name,
' ({})'.format(_('Static compatible')) if rule_model.static else ''
MultiFieldPanel(
[
FieldPanel("name", classname="title"),
FieldRowPanel(
[
FieldPanel("status"),
FieldPanel("persistent"),
]
),
) for rule_model in AbstractBaseRule.__subclasses__()
], heading=_("Rules")),
FieldPanel("match_any"),
FieldPanel("type", widget=forms.RadioSelect),
FieldPanel("count", classname="count_field"),
FieldPanel("randomisation_percent", classname="percent_field"),
],
heading="Segment",
),
MultiFieldPanel(
[
RulePanel(
"{}_related".format(rule_model._meta.db_table),
label="{}{}".format(
rule_model._meta.verbose_name,
" ({})".format(_("Static compatible"))
if rule_model.static
else "",
),
)
for rule_model in AbstractBaseRule.__subclasses__()
],
heading=_("Rules"),
),
]
super(Segment, self).__init__(*args, **kwargs)
@ -172,20 +210,21 @@ class Segment(ClusterableModel):
"""Retrieve all rules in the segment."""
segment_rules = []
for rule_model in AbstractBaseRule.get_descendant_models():
segment_rules.extend(
rule_model._default_manager.filter(segment=self))
segment_rules.extend(rule_model._default_manager.filter(segment=self))
return segment_rules
def toggle(self, save=True):
self.status = (
self.STATUS_ENABLED if self.status == self.STATUS_DISABLED
else self.STATUS_DISABLED)
self.STATUS_ENABLED
if self.status == self.STATUS_DISABLED
else self.STATUS_DISABLED
)
if save:
self.save()
def randomise_into_segment(self):
""" Returns True if randomisation_percent is not set or it generates
"""Returns True if randomisation_percent is not set or it generates
a random number less than the randomisation_percent
This is so there is some randomisation in which users are added to the
segment
@ -203,21 +242,24 @@ class PersonalisablePageMetadata(ClusterableModel):
segments.
"""
# Canonical pages should not ever be deleted if they have variants
# because the variants will be orphaned.
canonical_page = models.ForeignKey(
Page, models.PROTECT, related_name='personalisable_canonical_metadata',
null=True
Page,
models.PROTECT,
related_name="personalisable_canonical_metadata",
null=True,
)
# Delete metadata of the variant if its page gets deleted.
variant = models.OneToOneField(
Page, models.CASCADE, related_name='_personalisable_page_metadata',
null=True
Page, models.CASCADE, related_name="_personalisable_page_metadata", null=True
)
segment = models.ForeignKey(Segment, models.PROTECT, null=True,
related_name='page_metadata')
segment = models.ForeignKey(
Segment, models.PROTECT, null=True, related_name="page_metadata"
)
@cached_property
def has_variants(self):
@ -234,10 +276,12 @@ class PersonalisablePageMetadata(ClusterableModel):
@cached_property
def variants_metadata(self):
return (
PersonalisablePageMetadata.objects
.filter(canonical_page_id=self.canonical_page_id)
PersonalisablePageMetadata.objects.filter(
canonical_page_id=self.canonical_page_id
)
.exclude(variant_id=self.variant_id)
.exclude(variant_id=self.canonical_page_id))
.exclude(variant_id=self.canonical_page_id)
)
@cached_property
def is_canonical(self):
@ -258,33 +302,31 @@ class PersonalisablePageMetadata(ClusterableModel):
slug = "{}-{}".format(page.slug, segment.encoded_name())
title = "{} ({})".format(page.title, segment.name)
update_attrs = {
'title': title,
'slug': slug,
'live': False,
"title": title,
"slug": slug,
"live": False,
}
with transaction.atomic():
new_page = self.canonical_page.copy(
update_attrs=update_attrs, copy_revisions=False)
update_attrs=update_attrs, copy_revisions=False
)
PersonalisablePageMetadata.objects.create(
canonical_page=page,
variant=new_page,
segment=segment)
canonical_page=page, variant=new_page, segment=segment
)
return new_page
def metadata_for_segments(self, segments):
return (
self.__class__.objects
.filter(
canonical_page_id=self.canonical_page_id,
segment__in=segments))
return self.__class__.objects.filter(
canonical_page_id=self.canonical_page_id, segment__in=segments
)
def get_unused_segments(self):
if self.is_canonical:
return (
Segment.objects
.exclude(page_metadata__canonical_page_id=self.canonical_page_id))
return Segment.objects.exclude(
page_metadata__canonical_page_id=self.canonical_page_id
)
return Segment.objects.none()
@ -300,5 +342,18 @@ class PersonalisablePageMixin:
metadata = self._personalisable_page_metadata
except AttributeError:
metadata = PersonalisablePageMetadata.objects.create(
canonical_page=self, variant=self)
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()

View File

@ -1,9 +1,9 @@
from __future__ import absolute_import, unicode_literals
import logging
import re
from datetime import datetime
from importlib import import_module
import pycountry
from django import VERSION as DJANGO_VERSION
from django.apps import apps
from django.conf import settings
from django.contrib.sessions.models import Session
@ -11,33 +11,59 @@ 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.translation import ugettext_lazy as _
from django.utils import timezone
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
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.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'
icon = "fa-circle-o"
static = False
segment = ParentalKey(
'wagtail_personalisation.Segment',
"wagtail_personalisation.Segment",
related_name="%(app_label)s_%(class)ss",
)
class Meta:
abstract = True
verbose_name = 'Abstract segmentation rule'
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 +71,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.
@ -56,16 +82,17 @@ class AbstractBaseRule(models.Model):
"""
description = {
'title': _('Abstract segmentation rule'),
'value': '',
"title": _("Abstract segmentation rule"),
"value": "",
}
return description
@classmethod
def get_descendant_models(cls):
return [model for model in apps.get_models()
if issubclass(model, AbstractBaseRule)]
return [
model for model in apps.get_models() if issubclass(model, AbstractBaseRule)
]
class TimeRule(AbstractBaseRule):
@ -75,30 +102,32 @@ class TimeRule(AbstractBaseRule):
set start time and end time.
"""
icon = 'fa-clock-o'
icon = "fa-clock-o"
start_time = models.TimeField(_("Starting time"))
end_time = models.TimeField(_("Ending time"))
panels = [
FieldRowPanel([
FieldPanel('start_time'),
FieldPanel('end_time'),
]),
FieldRowPanel(
[
FieldPanel("start_time"),
FieldPanel("end_time"),
]
),
]
class Meta:
verbose_name = _('Time Rule')
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 {
'title': _('These users visit between'),
'value': _('{} and {}').format(
self.start_time.strftime("%H:%M"),
self.end_time.strftime("%H:%M")
"title": _("These users visit between"),
"value": _("{} and {}").format(
self.start_time.strftime("%H:%M"), self.end_time.strftime("%H:%M")
),
}
@ -110,7 +139,8 @@ class DayRule(AbstractBaseRule):
set in the rule.
"""
icon = 'fa-calendar-check-o'
icon = "fa-calendar-check-o"
mon = models.BooleanField(_("Monday"), default=False)
tue = models.BooleanField(_("Tuesday"), default=False)
@ -121,34 +151,39 @@ class DayRule(AbstractBaseRule):
sun = models.BooleanField(_("Sunday"), default=False)
panels = [
FieldPanel('mon'),
FieldPanel('tue'),
FieldPanel('wed'),
FieldPanel('thu'),
FieldPanel('fri'),
FieldPanel('sat'),
FieldPanel('sun'),
FieldPanel("mon"),
FieldPanel("tue"),
FieldPanel("wed"),
FieldPanel("thu"),
FieldPanel("fri"),
FieldPanel("sat"),
FieldPanel("sun"),
]
class Meta:
verbose_name = _('Day Rule')
verbose_name = _("Day Rule")
def test_user(self, request=None):
return [self.mon, self.tue, self.wed, self.thu,
self.fri, self.sat, self.sun][datetime.today().weekday()]
return [self.mon, self.tue, self.wed, self.thu, self.fri, self.sat, self.sun][
timezone.now().date().weekday()
]
def description(self):
days = (
('mon', self.mon), ('tue', self.tue), ('wed', self.wed),
('thu', self.thu), ('fri', self.fri), ('sat', self.sat),
('sun', self.sun),
("mon", self.mon),
("tue", self.tue),
("wed", self.wed),
("thu", self.thu),
("fri", self.fri),
("sat", self.sat),
("sun", self.sun),
)
chosen_days = [day_name for day_name, chosen in days if chosen]
return {
'title': _('These users visit on'),
'value': ", ".join([day for day in chosen_days]).title(),
"title": _("These users visit on"),
"value": ", ".join([day for day in chosen_days]).title(),
}
@ -159,32 +194,32 @@ class ReferralRule(AbstractBaseRule):
the set regex test.
"""
icon = 'fa-globe'
regex_string = models.TextField(
_("Regular expression to match the referrer"))
icon = "fa-globe"
regex_string = models.TextField(_("Regular expression to match the referrer"))
panels = [
FieldPanel('regex_string'),
FieldPanel("regex_string"),
]
class Meta:
verbose_name = _('Referral Rule')
verbose_name = _("Referral Rule")
def test_user(self, request):
pattern = re.compile(self.regex_string)
if 'HTTP_REFERER' in request.META:
referer = request.META['HTTP_REFERER']
if "HTTP_REFERER" in request.META:
referer = request.META["HTTP_REFERER"]
if pattern.search(referer):
return True
return False
def description(self):
return {
'title': _('These visits originate from'),
'value': self.regex_string,
'code': True
"title": _("These visits originate from"),
"value": self.regex_string,
"code": True,
}
@ -196,48 +231,55 @@ class VisitCountRule(AbstractBaseRule):
when visiting the set page.
"""
icon = 'fa-calculator'
icon = "fa-calculator"
static = True
OPERATOR_CHOICES = (
('more_than', _("More than")),
('less_than', _("Less than")),
('equal_to', _("Equal to")),
("more_than", _("More than")),
("less_than", _("Less than")),
("equal_to", _("Equal to")),
)
operator = models.CharField(
max_length=20, choices=OPERATOR_CHOICES, default="more_than"
)
operator = models.CharField(max_length=20,
choices=OPERATOR_CHOICES, default="more_than")
count = models.PositiveSmallIntegerField(default=0, null=True)
counted_page = models.ForeignKey(
'wagtailcore.Page',
"wagtailcore.Page",
null=False,
blank=False,
on_delete=models.CASCADE,
related_name='+',
related_name="+",
)
panels = [
PageChooserPanel('counted_page'),
FieldRowPanel([
FieldPanel('operator'),
FieldPanel('count'),
]),
PageChooserPanel("counted_page"),
FieldRowPanel(
[
FieldPanel("operator"),
FieldPanel("count"),
]
),
]
class Meta:
verbose_name = _('Visit count Rule')
verbose_name = _("Visit count Rule")
def _get_user_session(self, user):
sessions = Session.objects.iterator()
for session in sessions:
session_data = session.get_decoded()
if session_data.get('_auth_user_id') == str(user.id):
if session_data.get("_auth_user_id") == str(user.id):
return SessionStore(session_key=session.session_key)
return SessionStore()
def test_user(self, request, user=None):
# Local import for cyclic import
from wagtail_personalisation.adapters import (
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
SEGMENT_ADAPTER_CLASS,
SessionSegmentsAdapter,
get_segment_adapter,
)
# Django formsets don't honour 'required' fields so check rule is valid
try:
@ -247,7 +289,7 @@ class VisitCountRule(AbstractBaseRule):
if user:
# Create a fake request so we can use the adapter
request = RequestFactory().get('/')
request = RequestFactory().get("/")
request.user = user
# If we're using the session adapter check for an active session
@ -279,13 +321,8 @@ class VisitCountRule(AbstractBaseRule):
def description(self):
return {
'title': _('These users visited {}').format(
self.counted_page
),
'value': _('{} {} times').format(
self.get_operator_display(),
self.count
),
"title": _("These users visited {}").format(self.counted_page),
"value": _("{} {} times").format(self.get_operator_display(), self.count),
}
def get_column_header(self):
@ -294,10 +331,13 @@ class VisitCountRule(AbstractBaseRule):
def get_user_info_string(self, user):
# Local import for cyclic import
from wagtail_personalisation.adapters import (
get_segment_adapter, SessionSegmentsAdapter, SEGMENT_ADAPTER_CLASS)
SEGMENT_ADAPTER_CLASS,
SessionSegmentsAdapter,
get_segment_adapter,
)
# Create a fake request so we can use the adapter
request = RequestFactory().get('/')
request = RequestFactory().get("/")
request.user = user
# If we're using the session adapter check for an active session
@ -318,32 +358,28 @@ class QueryRule(AbstractBaseRule):
present in the request query.
"""
icon = 'fa-link'
parameter = models.SlugField(_("The query parameter to search for"),
max_length=20)
value = models.SlugField(_("The value of the parameter to match"),
max_length=20)
icon = "fa-link"
parameter = models.SlugField(_("The query parameter to search for"), max_length=20)
value = models.SlugField(_("The value of the parameter to match"), max_length=20)
panels = [
FieldPanel('parameter'),
FieldPanel('value'),
FieldPanel("parameter"),
FieldPanel("value"),
]
class Meta:
verbose_name = _('Query Rule')
verbose_name = _("Query Rule")
def test_user(self, request):
return request.GET.get(self.parameter, '') == self.value
return request.GET.get(self.parameter, "") == self.value
def description(self):
return {
'title': _('These users used a URL with the query'),
'value': _('?{}={}').format(
self.parameter,
self.value
),
'code': True
"title": _("These users used a URL with the query"),
"value": _("?{}={}").format(self.parameter, self.value),
"code": True,
}
@ -354,23 +390,24 @@ class DeviceRule(AbstractBaseRule):
in the request user agent headers.
"""
icon = 'fa-tablet'
icon = "fa-tablet"
mobile = models.BooleanField(_("Mobile phone"), default=False)
tablet = models.BooleanField(_("Tablet"), default=False)
desktop = models.BooleanField(_("Desktop"), default=False)
panels = [
FieldPanel('mobile'),
FieldPanel('tablet'),
FieldPanel('desktop'),
FieldPanel("mobile"),
FieldPanel("tablet"),
FieldPanel("desktop"),
]
class Meta:
verbose_name = _('Device Rule')
verbose_name = _("Device Rule")
def test_user(self, request=None):
ua_header = request.META['HTTP_USER_AGENT']
ua_header = request.META["HTTP_USER_AGENT"]
user_agent = parse(ua_header)
if user_agent.is_mobile:
@ -389,22 +426,90 @@ class UserIsLoggedInRule(AbstractBaseRule):
Matches when the user is authenticated.
"""
icon = 'fa-user'
icon = "fa-user"
is_logged_in = models.BooleanField(default=False)
panels = [
FieldPanel('is_logged_in'),
FieldPanel("is_logged_in"),
]
class Meta:
verbose_name = _('Logged in Rule')
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'),
"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()

View File

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

View File

@ -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 %}

View File

@ -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%}

View File

@ -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%}

View File

@ -5,6 +5,6 @@ from wagtail_personalisation.utils import count_active_days
register = Library()
@register.filter(name='days_since')
@register.filter(name="days_since")
def active_days(enable_date, disable_date):
return count_active_days(enable_date, disable_date)

View File

@ -15,17 +15,17 @@ def do_segment(parser, token):
tag_name, _, kwargs = parse_tag(token, parser)
# If no segment is provided this block will raise an error
if set(kwargs.keys()) != {'name'}:
if set(kwargs.keys()) != {"name"}:
usage = '{% segment name="segmentname" %} ... {% endsegment %}'
raise TemplateSyntaxError("Usage: %s" % usage)
nodelist = parser.parse(('endsegment',))
nodelist = parser.parse(("endsegment",))
parser.delete_first_token()
return SegmentNode(nodelist, name=kwargs['name'])
return SegmentNode(nodelist, name=kwargs["name"])
register.tag('segment', do_segment)
register.tag("segment", do_segment)
class SegmentNode(template.Node):
@ -36,6 +36,7 @@ class SegmentNode(template.Node):
If not it will return nothing
"""
def __init__(self, nodelist, name):
self.nodelist = nodelist
self.name = name
@ -48,10 +49,10 @@ class SegmentNode(template.Node):
return ""
# Check if user has segment
adapter = get_segment_adapter(context['request'])
adapter = get_segment_adapter(context["request"])
user_segment = adapter.get_segment_by_id(segment_id=segment.pk)
if not user_segment:
return ''
return ""
content = self.nodelist.render(context)
content = mark_safe(content)

View File

@ -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):
@ -34,7 +36,7 @@ def create_segment_dictionary(segment):
"encoded_name": segment.encoded_name(),
"id": segment.pk,
"timestamp": int(time.time()),
"persistent": segment.persistent
"persistent": segment.persistent,
}
@ -105,9 +107,10 @@ def exclude_variants(pages):
:rtype: QuerySet
"""
from wagtail_personalisation.models import PersonalisablePageMetadata
excluded_variant_pages = PersonalisablePageMetadata.objects.exclude(
canonical_page_id=F('variant_id')
).values_list('variant_id')
canonical_page_id=F("variant_id")
).values_list("variant_id")
return pages.exclude(pk__in=excluded_variant_pages)
@ -116,3 +119,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"]

View File

@ -1,15 +1,18 @@
from __future__ import absolute_import, unicode_literals
import csv
from django import VERSION as DJANGO_VERSION
from django import forms
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.http import (
HttpResponse, HttpResponseForbidden, HttpResponseRedirect)
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
from django.utils.translation import ugettext_lazy as _
from wagtail.contrib.modeladmin.options import ModelAdmin, modeladmin_register
from wagtail.contrib.modeladmin.views import DeleteView, IndexView
from wagtail.core.models import Page
@ -20,41 +23,42 @@ from wagtail_personalisation.utils import can_delete_pages
class SegmentModelIndexView(IndexView):
"""Placeholder for additional list functionality."""
pass
class SegmentModelDashboardView(IndexView):
"""Additional dashboard functionality."""
def media(self):
return forms.Media(
css={'all': ['css/dashboard.css']},
js=['js/commons.js', 'js/dashboard.js']
css={"all": ["css/dashboard.css"]}, js=["js/commons.js", "js/dashboard.js"]
)
def get_template_names(self):
return [
'modeladmin/wagtail_personalisation/segment/dashboard.html',
'modeladmin/index.html'
"modeladmin/wagtail_personalisation/segment/dashboard.html",
"modeladmin/index.html",
]
class SegmentModelDeleteView(DeleteView):
def get_affected_page_objects(self):
return Page.objects.filter(pk__in=(
self.instance.get_used_pages().values_list('variant_id', flat=True)
))
return Page.objects.filter(
pk__in=(self.instance.get_used_pages().values_list("variant_id", flat=True))
)
def get_template_names(self):
return [
'modeladmin/wagtail_personalisation/segment/delete.html',
'modeladmin/delete.html',
"modeladmin/wagtail_personalisation/segment/delete.html",
"modeladmin/delete.html",
]
def delete_instance(self):
page_variants = self.get_affected_page_objects()
if not can_delete_pages(page_variants, self.request.user):
raise PermissionDenied(
'User has no permission to delete variant page objects.'
"User has no permission to delete variant page objects."
)
# Deleting page objects triggers deletion of the personalisation
# metadata too because of models.CASCADE.
@ -65,8 +69,7 @@ class SegmentModelDeleteView(DeleteView):
super().delete_instance()
def post(self, request, *args, **kwargs):
if not can_delete_pages(self.get_affected_page_objects(),
self.request.user):
if not can_delete_pages(self.get_affected_page_objects(), self.request.user):
context = self.get_context_data(
cannot_delete_page_variants_error=True,
)
@ -77,25 +80,39 @@ class SegmentModelDeleteView(DeleteView):
@modeladmin_register
class SegmentModelAdmin(ModelAdmin):
"""The model admin for the Segments administration interface."""
model = Segment
index_view_class = SegmentModelIndexView
dashboard_view_class = SegmentModelDashboardView
delete_view_class = SegmentModelDeleteView
menu_icon = 'fa-snowflake-o'
menu_icon = "fa-snowflake-o"
add_to_settings_menu = False
list_display = ('name', 'persistent', 'match_any', 'status',
'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_css = ['css/form.css']
list_display = (
"name",
"persistent",
"match_any",
"status",
"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",
"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):
kwargs = {'model_admin': self}
kwargs = {"model_admin": self}
view_class = self.dashboard_view_class
request.session.setdefault('segment_view', 'dashboard')
if request.session['segment_view'] != 'dashboard':
request.session.setdefault("segment_view", "dashboard")
if request.session["segment_view"] != "dashboard":
view_class = self.index_view_class
return view_class.as_view(**kwargs)(request)
@ -108,7 +125,8 @@ class SegmentModelAdmin(ModelAdmin):
def statistics(self, obj):
return _("{visits} visits in {days} days").format(
visits=obj.visit_count, days=obj.get_active_days())
visits=obj.visit_count, days=obj.get_active_days()
)
def toggle_segment_view(request):
@ -120,14 +138,14 @@ def toggle_segment_view(request):
:rtype: django.http.HttpResponseRedirect
"""
if request.user.has_perm('wagtailadmin.access_admin'):
if request.session['segment_view'] == 'dashboard':
request.session['segment_view'] = 'list'
if request.user.has_perm("wagtailadmin.access_admin"):
if request.session["segment_view"] == "dashboard":
request.session["segment_view"] = "list"
elif request.session['segment_view'] != 'dashboard':
request.session['segment_view'] = 'dashboard'
elif request.session["segment_view"] != "dashboard":
request.session["segment_view"] = "dashboard"
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
return HttpResponseForbidden()
@ -143,12 +161,12 @@ def toggle(request, segment_id):
:rtype: django.http.HttpResponseRedirect
"""
if request.user.has_perm('wagtailadmin.access_admin'):
if request.user.has_perm("wagtailadmin.access_admin"):
segment = get_object_or_404(Segment, pk=segment_id)
segment.toggle()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/'))
return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))
return HttpResponseForbidden()
@ -167,7 +185,7 @@ def copy_page_view(request, page_id, segment_id):
:rtype: django.http.HttpResponseRedirect
"""
if request.user.has_perm('wagtailadmin.access_admin'):
if request.user.has_perm("wagtailadmin.access_admin"):
segment = get_object_or_404(Segment, pk=segment_id)
page = get_object_or_404(Page, pk=page_id).specific
@ -177,7 +195,7 @@ def copy_page_view(request, page_id, segment_id):
variant = variant_metadata.first()
else:
variant = metadata.copy_for_segment(segment)
edit_url = reverse('wagtailadmin_pages:edit', args=[variant.id])
edit_url = reverse("wagtailadmin_pages:edit", args=[variant.id])
return HttpResponseRedirect(edit_url)
@ -186,14 +204,15 @@ def copy_page_view(request, page_id, segment_id):
# CSV download views
def segment_user_data(request, segment_id):
if request.user.has_perm('wagtailadmin.access_admin'):
if request.user.has_perm("wagtailadmin.access_admin"):
segment = get_object_or_404(Segment, pk=segment_id)
response = HttpResponse(content_type='text/csv; charset=utf-8')
response['Content-Disposition'] = \
'attachment;filename=segment-%s-users.csv' % str(segment_id)
response = HttpResponse(content_type="text/csv; charset=utf-8")
response[
"Content-Disposition"
] = "attachment;filename=segment-%s-users.csv" % str(segment_id)
headers = ['Username']
headers = ["Username"]
for rule in segment.get_rules():
if rule.static:
headers.append(rule.get_column_header())

View File

@ -1,37 +1,51 @@
from __future__ import absolute_import, unicode_literals
import logging
from django.conf.urls import include, url
from django import VERSION as DJANGO_VERSION
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
from django.urls import include, re_path, reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
if DJANGO_VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as _
else:
from django.utils.translation import ugettext_lazy as _
from wagtail import VERSION as WAGTAIL_VERSION
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')),
re_path(
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 +60,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 +73,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 +91,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 +118,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 +132,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 +149,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,48 +171,66 @@ 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):
def get_context(self):
def get_total_pages(self, context):
# Perform the same check as Wagtail to get the correct count.
# Only correct the count when a root page is available to the user.
# 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
return page_count
return context
if WAGTAIL_VERSION >= (2, 15):
def get_context_data(self, parent_context):
context = super().get_context_data(parent_context)
context["total_pages"] = self.get_total_pages(context)
return context
else:
def get_context(self):
context = super().get_context()
context["total_pages"] = self.get_total_pages(context)
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 +243,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 +265,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 +283,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 +308,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 # noqa
):
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")
),
},
)

View File

@ -1,13 +1,14 @@
from __future__ import absolute_import, unicode_literals
import pytest
pytest_plugins = [
'tests.fixtures'
]
pytest_plugins = ["tests.fixtures"]
@pytest.fixture(scope='session')
@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

View File

@ -5,11 +5,24 @@ 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
title = 'Test page'
title = "Test page"
slug = factory.LazyAttribute(lambda obj: slugify(obj.title))
class Meta:
@ -17,8 +30,13 @@ class ContentPageFactory(PageFactory):
class RegularPageFactory(PageFactory):
title = 'Regular page'
title = "Regular page"
slug = factory.LazyAttribute(lambda obj: slugify(obj.title))
class Meta:
model = models.RegularPage
class PersonalisablePageMetadataFactory(factory.DjangoModelFactory):
class Meta:
model = PersonalisablePageMetadata

View File

@ -8,19 +8,16 @@ from wagtail_personalisation import rules
class DayRuleFactory(factory.DjangoModelFactory):
class Meta:
model = rules.DayRule
class DeviceRuleFactory(factory.DjangoModelFactory):
class Meta:
model = rules.DeviceRule
class QueryRuleFactory(factory.DjangoModelFactory):
class Meta:
model = rules.QueryRule
@ -46,3 +43,8 @@ class VisitCountRuleFactory(factory.DjangoModelFactory):
class Meta:
model = rules.VisitCountRule
class OriginCountryRuleFactory(factory.DjangoModelFactory):
class Meta:
model = rules.OriginCountryRule

View File

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

View File

@ -5,9 +5,9 @@ from tests.factories.page import ContentPageFactory
class SiteFactory(factory.DjangoModelFactory):
hostname = 'localhost'
hostname = "localhost"
port = factory.Sequence(lambda n: 81 + n)
site_name = 'Test site'
site_name = "Test site"
root_page = factory.SubFactory(ContentPageFactory, parent=None)
is_default_site = False

View File

@ -9,23 +9,23 @@ from tests.factories.segment import SegmentFactory
from tests.factories.site import SiteFactory
@pytest.fixture(scope='function')
@pytest.fixture(scope="function")
def site():
root_page = ContentPageFactory(parent=None, slug='')
root_page = ContentPageFactory(parent=None, slug="")
site = SiteFactory(is_default_site=True, root_page=root_page)
page1 = ContentPageFactory(parent=root_page, slug='page-1')
page2 = ContentPageFactory(parent=root_page, slug='page-2')
ContentPageFactory(parent=page1, slug='page-1-1')
ContentPageFactory(parent=page2, slug='page-2-1')
page1 = ContentPageFactory(parent=root_page, slug="page-1")
page2 = ContentPageFactory(parent=root_page, slug="page-2")
ContentPageFactory(parent=page1, slug="page-1-1")
ContentPageFactory(parent=page2, slug="page-2-1")
RegularPageFactory(parent=root_page, slug='regular')
RegularPageFactory(parent=root_page, slug="regular")
return site
@pytest.fixture
@pytest.fixture()
def segmented_page(site):
page = ContentPageFactory(parent=site.root_page, slug='personalised')
page = ContentPageFactory(parent=site.root_page, slug="personalised")
segment = SegmentFactory()
return page.personalisation_metadata.copy_for_segment(segment)
@ -37,7 +37,6 @@ def rf():
class RequestFactory(BaseRequestFactory):
def request(self, user=None, **request):
request = super(RequestFactory, self).request(**request)
request.user = AnonymousUser()
@ -46,6 +45,6 @@ class RequestFactory(BaseRequestFactory):
return request
@pytest.fixture
@pytest.fixture()
def user(django_user_model):
return django_user_model.objects.create(username='user')
return django_user_model.objects.create(username="user")

View File

@ -1,94 +1,86 @@
from __future__ import absolute_import, unicode_literals
import os
DATABASES = {
'default': {
'ENGINE': os.environ.get('DATABASE_ENGINE', 'django.db.backends.sqlite3'),
'NAME': os.environ.get('DATABASE_NAME', 'db.sqlite3'),
"default": {
"ENGINE": os.environ.get("DATABASE_ENGINE", "django.db.backends.sqlite3"),
"NAME": os.environ.get("DATABASE_NAME", "db.sqlite3"),
}
}
ALLOWED_HOSTS = ['localhost']
ALLOWED_HOSTS = ["localhost"]
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
"LOCATION": "unique-snowflake",
}
}
SECRET_KEY = 'not needed'
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
SECRET_KEY = "not needed"
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
ROOT_URLCONF = 'tests.site.urls'
ROOT_URLCONF = "tests.site.urls"
STATIC_URL = '/static/'
STATIC_URL = "/static/"
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
STATICFILES_FINDERS = ("django.contrib.staticfiles.finders.AppDirectoriesFinder",)
USE_TZ = False
TESTS_ROOT = os.path.dirname(os.path.abspath(__file__))
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(TESTS_ROOT, 'site', 'templates'),
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [
os.path.join(TESTS_ROOT, "site", "templates"),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request',
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"django.template.context_processors.request",
],
'debug': True,
"debug": True,
},
},
]
MIDDLEWARE = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'wagtail.core.middleware.SiteMiddleware',
MIDDLEWARE = (
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
)
INSTALLED_APPS = (
'wagtail_personalisation',
'wagtail.contrib.modeladmin',
'wagtail.search',
'wagtail.sites',
'wagtail.users',
'wagtail.images',
'wagtail.documents',
'wagtail.admin',
'wagtail.core',
'taggit',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'tests.site.pages',
"wagtail_personalisation",
"wagtail.contrib.modeladmin",
"wagtail.search",
"wagtail.sites",
"wagtail.users",
"wagtail.images",
"wagtail.documents",
"wagtail.admin",
"wagtail.core",
"taggit",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"tests.site.pages",
)
PASSWORD_HASHERS = (
'django.contrib.auth.hashers.MD5PasswordHasher', # don't use the intentionally slow default password hasher
"django.contrib.auth.hashers.MD5PasswordHasher", # don't use the intentionally slow default password hasher
)
WAGTAIL_SITE_NAME = 'wagtail-personalisation test'
WAGTAIL_SITE_NAME = "wagtail-personalisation test"

View File

@ -14,20 +14,33 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('wagtailcore', '0001_initial'),
("wagtailcore", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='ContentPage',
name="ContentPage",
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
('subtitle', models.CharField(blank=True, default='', max_length=255)),
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
), # noqa: E501
("subtitle", models.CharField(blank=True, default="", max_length=255)),
("body", wagtail.core.fields.RichTextField(blank=True, default="")),
],
options={
'abstract': False,
"abstract": False,
},
bases=(wagtail_personalisation.models.PersonalisablePageMixin, 'wagtailcore.page'),
bases=(
wagtail_personalisation.models.PersonalisablePageMixin,
"wagtailcore.page",
),
),
]

View File

@ -9,21 +9,31 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0001_initial'),
('pages', '0001_initial'),
("wagtailcore", "0001_initial"),
("pages", "0001_initial"),
]
operations = [
migrations.CreateModel(
name='RegularPage',
name="RegularPage",
fields=[
('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), # noqa: E501
('subtitle', models.CharField(blank=True, default='', max_length=255)),
('body', wagtail.core.fields.RichTextField(blank=True, default='')),
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
), # noqa: E501
("subtitle", models.CharField(blank=True, default="", max_length=255)),
("body", wagtail.core.fields.RichTextField(blank=True, default="")),
],
options={
'abstract': False,
"abstract": False,
},
bases=('wagtailcore.page',),
bases=("wagtailcore.page",),
),
]

View File

@ -7,20 +7,20 @@ from wagtail_personalisation.models import PersonalisablePageMixin
class RegularPage(Page):
subtitle = models.CharField(max_length=255, blank=True, default='')
body = RichTextField(blank=True, default='')
subtitle = models.CharField(max_length=255, blank=True, default="")
body = RichTextField(blank=True, default="")
content_panels = Page.content_panels + [
FieldPanel('subtitle'),
FieldPanel('body'),
FieldPanel("subtitle"),
FieldPanel("body"),
]
class ContentPage(PersonalisablePageMixin, Page):
subtitle = models.CharField(max_length=255, blank=True, default='')
body = RichTextField(blank=True, default='')
subtitle = models.CharField(max_length=255, blank=True, default="")
body = RichTextField(blank=True, default="")
content_panels = Page.content_panels + [
FieldPanel('subtitle'),
FieldPanel('body'),
FieldPanel("subtitle"),
FieldPanel("body"),
]

View File

@ -1,16 +1,14 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import include, url
from django.contrib import admin
from django.urls import include, re_path
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.core import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls
urlpatterns = [
url(r'^django-admin/', admin.site.urls),
url(r'^admin/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
url(r'', include(wagtail_urls)),
re_path(r"^django-admin/", admin.site.urls),
re_path(r"^admin/", include(wagtailadmin_urls)),
re_path(r"^documents/", include(wagtaildocs_urls)),
re_path(r"", include(wagtail_urls)),
]

View File

@ -6,15 +6,15 @@ from wagtail_personalisation import adapters
@pytest.mark.django_db
def test_get_segments(rf):
request = rf.get('/')
request = rf.get("/")
adapter = adapters.SessionSegmentsAdapter(request)
segment_1 = SegmentFactory(name='segment-1', persistent=True)
segment_2 = SegmentFactory(name='segment-2', persistent=True)
segment_1 = SegmentFactory(name="segment-1", persistent=True)
segment_2 = SegmentFactory(name="segment-2", persistent=True)
adapter.set_segments([segment_1, segment_2])
assert len(request.session['segments']) == 2
assert len(request.session["segments"]) == 2
segments = adapter.get_segments()
assert segments == [segment_1, segment_2]
@ -22,15 +22,15 @@ def test_get_segments(rf):
@pytest.mark.django_db
def test_get_segments_session(rf):
request = rf.get('/')
request = rf.get("/")
adapter = adapters.SessionSegmentsAdapter(request)
segment_1 = SegmentFactory(name='segment-1', persistent=True)
segment_2 = SegmentFactory(name='segment-2', persistent=True)
segment_1 = SegmentFactory(name="segment-1", persistent=True)
segment_2 = SegmentFactory(name="segment-2", persistent=True)
adapter.set_segments([segment_1, segment_2])
assert len(request.session['segments']) == 2
assert len(request.session["segments"]) == 2
adapter._segment_cache = None
segments = adapter.get_segments()
@ -39,12 +39,12 @@ def test_get_segments_session(rf):
@pytest.mark.django_db
def test_get_segment_by_id(rf):
request = rf.get('/')
request = rf.get("/")
adapter = adapters.SessionSegmentsAdapter(request)
segment_1 = SegmentFactory(name='segment-1', persistent=True)
segment_2 = SegmentFactory(name='segment-2', persistent=True)
segment_1 = SegmentFactory(name="segment-1", persistent=True)
segment_2 = SegmentFactory(name="segment-2", persistent=True)
adapter.set_segments([segment_1, segment_2])
@ -54,12 +54,12 @@ def test_get_segment_by_id(rf):
@pytest.mark.django_db
def test_refresh_removes_disabled(rf):
request = rf.get('/')
request = rf.get("/")
adapter = adapters.SessionSegmentsAdapter(request)
segment_1 = SegmentFactory(name='segment-1', persistent=True)
segment_2 = SegmentFactory(name='segment-2', persistent=True)
segment_1 = SegmentFactory(name="segment-1", persistent=True)
segment_2 = SegmentFactory(name="segment-2", persistent=True)
adapter.set_segments([segment_1, segment_2])
@ -73,27 +73,27 @@ def test_refresh_removes_disabled(rf):
@pytest.mark.django_db
def test_add_page_visit(rf, site):
request = rf.get('/')
request = rf.get("/")
adapter = adapters.SessionSegmentsAdapter(request)
adapter.add_page_visit(site.root_page)
assert request.session['visit_count'][0]['count'] == 1
assert request.session["visit_count"][0]["count"] == 1
adapter.add_page_visit(site.root_page)
assert request.session['visit_count'][0]['count'] == 2
assert request.session["visit_count"][0]["count"] == 2
assert adapter.get_visit_count() == 2
@pytest.mark.django_db
def test_update_visit_count(rf, site):
request = rf.get('/')
request = rf.get("/")
adapter = adapters.SessionSegmentsAdapter(request)
segment_1 = SegmentFactory(name='segment-1', persistent=True, visit_count=0)
segment_2 = SegmentFactory(name='segment-2', persistent=True, visit_count=0)
segment_1 = SegmentFactory(name="segment-1", persistent=True, visit_count=0)
segment_2 = SegmentFactory(name="segment-2", persistent=True, visit_count=0)
adapter.set_segments([segment_1, segment_2])
adapter.update_visit_count()
@ -107,12 +107,12 @@ def test_update_visit_count(rf, site):
@pytest.mark.django_db
def test_update_visit_count_deleted_segment(rf, site):
request = rf.get('/')
request = rf.get("/")
adapter = adapters.SessionSegmentsAdapter(request)
segment_1 = SegmentFactory(name='segment-1', persistent=True, visit_count=0)
segment_2 = SegmentFactory(name='segment-2', persistent=True, visit_count=0)
segment_1 = SegmentFactory(name="segment-1", persistent=True, visit_count=0)
segment_2 = SegmentFactory(name="segment-2", persistent=True, visit_count=0)
adapter.set_segments([segment_1, segment_2])
segment_2.delete()

View File

@ -15,11 +15,12 @@ from wagtail_personalisation.rules import TimeRule
@pytest.mark.django_db
def test_segment_create():
factoried_segment = SegmentFactory()
segment = Segment(name='TestSegment', status='enabled')
segment = Segment(name="TestSegment", status="enabled")
TimeRule(
start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0),
segment=segment)
segment=segment,
)
assert factoried_segment.name == segment.name
assert factoried_segment.status == segment.status
@ -27,21 +28,16 @@ def test_segment_create():
@pytest.mark.django_db
def test_referral_rule_create():
segment = SegmentFactory(name='Referral')
referral_rule = ReferralRuleFactory(
regex_string='test.test',
segment=segment)
segment = SegmentFactory(name="Referral")
referral_rule = ReferralRuleFactory(regex_string="test.test", segment=segment)
assert referral_rule.regex_string == 'test.test'
assert referral_rule.regex_string == "test.test"
@pytest.mark.django_db
def test_query_rule_create():
segment = SegmentFactory(name='Query')
query_rule = QueryRuleFactory(
parameter="query",
value="value",
segment=segment)
segment = SegmentFactory(name="Query")
query_rule = QueryRuleFactory(parameter="query", value="value", segment=segment)
assert query_rule.parameter == 'query'
assert query_rule.value == 'value'
assert query_rule.parameter == "query"
assert query_rule.value == "value"

View File

@ -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
@ -18,7 +16,8 @@ def test_segment_create():
TimeRule(
start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0),
segment=segment)
segment=segment,
)
@pytest.mark.django_db
@ -60,3 +59,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"

View File

@ -3,5 +3,5 @@ import pytest
@pytest.mark.django_db
def test_request_device_segment_no_match(client, site):
response = client.get('/regular/')
response = client.get("/regular/")
assert response.status_code == 200

View File

@ -0,0 +1,203 @@
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

@ -9,7 +9,7 @@ from tests.factories.segment import SegmentFactory
@pytest.mark.django_db
def test_day_rule_create():
segment = SegmentFactory(name='DaySegment')
segment = SegmentFactory(name="DaySegment")
day_rule = DayRuleFactory(mon=True, thu=True, segment=segment)
assert day_rule.mon is True
@ -20,11 +20,9 @@ def test_day_rule_create():
@pytest.mark.django_db
@freeze_time("2017-01-01")
def test_request_day_segment(client, site):
day_only_segment = SegmentFactory(name='Day only')
DayRuleFactory(
sun=True,
segment=day_only_segment)
day_only_segment = SegmentFactory(name="Day only")
DayRuleFactory(sun=True, segment=day_only_segment)
client.get('/')
client.get("/")
assert client.session['segments'][0]['encoded_name'] == 'day-only'
assert client.session["segments"][0]["encoded_name"] == "day-only"

View File

@ -6,7 +6,7 @@ from tests.factories.segment import SegmentFactory
@pytest.mark.django_db
def test_device_rule_create():
segment = SegmentFactory(name='DeviceSegment')
segment = SegmentFactory(name="DeviceSegment")
device_rule = DeviceRuleFactory(mobile=True, segment=segment)
assert device_rule.mobile is True
@ -16,23 +16,25 @@ def test_device_rule_create():
@pytest.mark.django_db
def test_request_device_segment(client, site):
device_only_segment = SegmentFactory(name='Device only')
DeviceRuleFactory(
tablet=True,
segment=device_only_segment)
device_only_segment = SegmentFactory(name="Device only")
DeviceRuleFactory(tablet=True, segment=device_only_segment)
client.get('/', **{'HTTP_USER_AGENT': 'Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X)'})
client.get(
"/",
**{"HTTP_USER_AGENT": "Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X)"}
)
assert client.session['segments'][0]['encoded_name'] == 'device-only'
assert client.session["segments"][0]["encoded_name"] == "device-only"
@pytest.mark.django_db
def test_request_device_segment_no_match(client, site):
no_device_segment = SegmentFactory(name='No device')
DeviceRuleFactory(
mobile=True,
segment=no_device_segment)
no_device_segment = SegmentFactory(name="No device")
DeviceRuleFactory(mobile=True, segment=no_device_segment)
client.get('/', **{'HTTP_USER_AGENT': 'Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X)'})
client.get(
"/",
**{"HTTP_USER_AGENT": "Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X)"}
)
assert not client.session['segments']
assert not client.session["segments"]

View File

@ -11,80 +11,68 @@ from tests.factories.segment import SegmentFactory
@pytest.mark.django_db
def test_no_segments(client, site):
response = client.get('/')
response = client.get("/")
assert response.status_code == 200
assert client.session['segments'] == []
assert client.session["segments"] == []
@pytest.mark.django_db
def test_referral_segment(client, site):
referral_segment = SegmentFactory(name='Referral')
ReferralRuleFactory(
regex_string="test.test",
segment=referral_segment
)
referral_segment = SegmentFactory(name="Referral")
ReferralRuleFactory(regex_string="test.test", segment=referral_segment)
response = client.get('/', **{'HTTP_REFERER': 'test.test'})
response = client.get("/", **{"HTTP_REFERER": "test.test"})
assert response.status_code == 200
assert client.session['segments'][0]['encoded_name'] == 'referral'
assert client.session["segments"][0]["encoded_name"] == "referral"
@pytest.mark.django_db
@freeze_time("10:00:00")
def test_time_and_referral_segment(client, site):
segment = SegmentFactory(name='Both')
segment = SegmentFactory(name="Both")
TimeRuleFactory(
start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0),
segment=segment
)
ReferralRuleFactory(
regex_string="test.test",
segment=segment
segment=segment,
)
ReferralRuleFactory(regex_string="test.test", segment=segment)
response = client.get('/', **{'HTTP_REFERER': 'test.test'})
response = client.get("/", **{"HTTP_REFERER": "test.test"})
assert response.status_code == 200
assert client.session['segments'][0]['encoded_name'] == 'both'
assert client.session["segments"][0]["encoded_name"] == "both"
@pytest.mark.django_db
@freeze_time("7:00:00")
def test_no_time_but_referral_segment(client, site):
segment = SegmentFactory(name='Not both')
segment = SegmentFactory(name="Not both")
TimeRuleFactory(
start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0),
segment=segment
)
ReferralRuleFactory(
regex_string="test.test",
segment=segment
segment=segment,
)
ReferralRuleFactory(regex_string="test.test", segment=segment)
response = client.get('/', **{'HTTP_REFERER': 'test.test'})
response = client.get("/", **{"HTTP_REFERER": "test.test"})
assert response.status_code == 200
assert len(client.session['segments']) == 0
assert len(client.session["segments"]) == 0
@pytest.mark.django_db
@freeze_time("9:00:00")
def test_time_but_no_referral_segment(client, site):
segment = SegmentFactory(name='Not both')
segment = SegmentFactory(name="Not both")
TimeRuleFactory(
start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0),
segment=segment
)
ReferralRuleFactory(
regex_string="test.test",
segment=segment
segment=segment,
)
ReferralRuleFactory(regex_string="test.test", segment=segment)
response = client.get('/')
response = client.get("/")
assert response.status_code == 200
assert len(client.session['segments']) == 0
assert len(client.session["segments"]) == 0

View File

@ -6,43 +6,33 @@ from tests.factories.segment import SegmentFactory
@pytest.mark.django_db
def test_request_query_rule(client, site):
segment = SegmentFactory(name='Query')
segment = SegmentFactory(name="Query")
QueryRuleFactory(
parameter="query",
value="value",
segment=segment,
)
response = client.get('/?query=value')
response = client.get("/?query=value")
assert response.status_code == 200
assert any(
item['encoded_name'] == 'query' for item in client.session['segments'])
assert any(item["encoded_name"] == "query" for item in client.session["segments"])
@pytest.mark.django_db
def test_request_only_one_query_rule(client, site):
segment = SegmentFactory(name='Query')
QueryRuleFactory(
parameter="query",
value="value",
segment=segment
)
segment = SegmentFactory(name="Query")
QueryRuleFactory(parameter="query", value="value", segment=segment)
response = client.get('/?test=test&query=value')
response = client.get("/?test=test&query=value")
assert response.status_code == 200
assert any(
item['encoded_name'] == 'query' for item in client.session['segments'])
assert any(item["encoded_name"] == "query" for item in client.session["segments"])
@pytest.mark.django_db
def test_request_multiple_queries(client, site):
segment = SegmentFactory(name='Multiple queries')
QueryRuleFactory(
parameter="test",
value="test",
segment=segment
)
segment = SegmentFactory(name="Multiple queries")
QueryRuleFactory(parameter="test", value="test", segment=segment)
QueryRuleFactory(
parameter="query",
@ -50,77 +40,65 @@ def test_request_multiple_queries(client, site):
segment=segment,
)
response = client.get('/?test=test&query=value')
response = client.get("/?test=test&query=value")
assert response.status_code == 200
assert any(
item['encoded_name'] == 'multiple-queries'
for item in client.session['segments']
item["encoded_name"] == "multiple-queries"
for item in client.session["segments"]
)
@pytest.mark.django_db
def test_request_persistent_segmenting(client, site):
segment = SegmentFactory(name='Persistent', persistent=True)
QueryRuleFactory(
parameter="test",
value="test",
segment=segment
segment = SegmentFactory(name="Persistent", persistent=True)
QueryRuleFactory(parameter="test", value="test", segment=segment)
response = client.get("/?test=test")
assert response.status_code == 200
assert any(
item["encoded_name"] == "persistent" for item in client.session["segments"]
)
response = client.get('/?test=test')
assert response.status_code == 200
assert any(
item['encoded_name'] == 'persistent'
for item in client.session['segments'])
response = client.get('/')
response = client.get("/")
assert response.status_code == 200
assert any(
item['encoded_name'] == 'persistent'
for item in client.session['segments'])
item["encoded_name"] == "persistent" for item in client.session["segments"]
)
@pytest.mark.django_db
def test_request_non_persistent_segmenting(client, site):
segment = SegmentFactory(name='Non Persistent')
QueryRuleFactory(
parameter="test",
value="test",
segment=segment
)
segment = SegmentFactory(name="Non Persistent")
QueryRuleFactory(parameter="test", value="test", segment=segment)
response = client.get('/?test=test')
response = client.get("/?test=test")
assert response.status_code == 200
assert any(
item['encoded_name'] == 'non-persistent'
for item in client.session['segments'])
item["encoded_name"] == "non-persistent" for item in client.session["segments"]
)
response = client.get('/')
response = client.get("/")
assert response.status_code == 200
assert not any(
item['encoded_name'] == 'non-persistent'
for item in client.session['segments'])
item["encoded_name"] == "non-persistent" for item in client.session["segments"]
)
@pytest.mark.django_db
def test_request_match_any_segmenting(client, site):
segment = SegmentFactory(name='Match any', match_any=True)
segment = SegmentFactory(name="Match any", match_any=True)
QueryRuleFactory(
parameter='test',
value='test',
parameter="test",
value="test",
segment=segment,
)
QueryRuleFactory(
parameter='test2',
value='test2',
segment=segment
)
QueryRuleFactory(parameter="test2", value="test2", segment=segment)
response = client.get('/?test=test')
response = client.get("/?test=test")
assert response.status_code == 200
assert any(
item['encoded_name'] == 'match-any'
for item in client.session['segments'])
item["encoded_name"] == "match-any" for item in client.session["segments"]
)

View File

@ -1,4 +1,3 @@
import pytest
from tests.factories.rule import ReferralRuleFactory
@ -7,9 +6,7 @@ from tests.factories.segment import SegmentFactory
@pytest.mark.django_db
def test_referral_rule_create():
segment = SegmentFactory(name='Referral')
referral_rule = ReferralRuleFactory(
regex_string='test.test',
segment=segment)
segment = SegmentFactory(name="Referral")
referral_rule = ReferralRuleFactory(regex_string="test.test", segment=segment)
assert referral_rule.regex_string == 'test.test'
assert referral_rule.regex_string == "test.test"

View File

@ -9,11 +9,12 @@ from tests.factories.segment import SegmentFactory
@pytest.mark.django_db
def test_time_rule_create():
segment = SegmentFactory(name='TimeSegment')
segment = SegmentFactory(name="TimeSegment")
time_rule = TimeRuleFactory(
start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0),
segment=segment)
segment=segment,
)
assert time_rule.start_time == datetime.time(8, 0, 0)
@ -21,13 +22,14 @@ def test_time_rule_create():
@pytest.mark.django_db
@freeze_time("10:00:00")
def test_requesttime_segment(client, site):
time_only_segment = SegmentFactory(name='Time only')
time_only_segment = SegmentFactory(name="Time only")
TimeRuleFactory(
start_time=datetime.time(8, 0, 0),
end_time=datetime.time(23, 0, 0),
segment=time_only_segment)
segment=time_only_segment,
)
response = client.get('/')
response = client.get("/")
assert response.status_code == 200
assert client.session['segments'][0]['encoded_name'] == 'time-only'
assert client.session["segments"][0]["encoded_name"] == "time-only"

View File

@ -7,23 +7,23 @@ from wagtail_personalisation.rules import VisitCountRule
@pytest.mark.django_db
def test_visit_count(site, client):
response = client.get('/')
response = client.get("/")
assert response.status_code == 200
visit_count = client.session['visit_count']
assert visit_count[0]['path'] == '/'
assert visit_count[0]['count'] == 1
visit_count = client.session["visit_count"]
assert visit_count[0]["path"] == "/"
assert visit_count[0]["count"] == 1
response = client.get('/')
response = client.get("/")
assert response.status_code == 200
visit_count = client.session['visit_count']
assert visit_count[0]['path'] == '/'
assert visit_count[0]['count'] == 2
visit_count = client.session["visit_count"]
assert visit_count[0]["path"] == "/"
assert visit_count[0]["count"] == 2
response = client.get('/page-1/')
response = client.get("/page-1/")
assert response.status_code == 200
visit_count = client.session['visit_count']
assert visit_count[0]['count'] == 2
assert visit_count[1]['count'] == 1
visit_count = client.session["visit_count"]
assert visit_count[0]["count"] == 2
assert visit_count[1]["count"] == 1
@pytest.mark.django_db
@ -34,11 +34,11 @@ def test_call_test_user_on_invalid_rule_fails(site, user, mocker):
@pytest.mark.django_db
def test_visit_count_call_test_user_with_user(site, client, user):
segment = SegmentFactory(name='VisitCount')
segment = SegmentFactory(name="VisitCount")
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
session = client.session
session['visit_count'] = [{'path': '/', 'count': 2}]
session["visit_count"] = [{"path": "/", "count": 2}]
session.save()
client.force_login(user)
@ -47,11 +47,11 @@ def test_visit_count_call_test_user_with_user(site, client, user):
@pytest.mark.django_db
def test_visit_count_call_test_user_with_user_or_request_fails(site, client, user):
segment = SegmentFactory(name='VisitCount')
segment = SegmentFactory(name="VisitCount")
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
session = client.session
session['visit_count'] = [{'path': '/', 'count': 2}]
session["visit_count"] = [{"path": "/", "count": 2}]
session.save()
client.force_login(user)
@ -60,20 +60,20 @@ def test_visit_count_call_test_user_with_user_or_request_fails(site, client, use
@pytest.mark.django_db
def test_get_column_header(site):
segment = SegmentFactory(name='VisitCount')
segment = SegmentFactory(name="VisitCount")
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
assert rule.get_column_header() == 'Visit count - Test page'
assert rule.get_column_header() == "Visit count - Test page"
@pytest.mark.django_db
def test_get_user_info_string_returns_count(site, client, user):
segment = SegmentFactory(name='VisitCount')
segment = SegmentFactory(name="VisitCount")
rule = VisitCountRuleFactory(counted_page=site.root_page, segment=segment)
session = client.session
session['visit_count'] = [{'path': '/', 'count': 2}]
session["visit_count"] = [{"path": "/", "count": 2}]
session.save()
client.force_login(user)
assert rule.get_user_info_string(user) == '2'
assert rule.get_user_info_string(user) == "2"

View File

@ -12,7 +12,14 @@ from wagtail_personalisation.rules import TimeRule, VisitCountRule
def form_with_data(segment, *rules):
model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent']
model_fields = [
"type",
"status",
"count",
"name",
"match_any",
"randomisation_percent",
]
class TestSegmentAdminForm(SegmentAdminForm):
class Meta:
@ -27,10 +34,10 @@ def form_with_data(segment, *rules):
if isinstance(rule, formset.model):
rule_data = model_to_dict(rule)
for key, value in rule_data.items():
data['{}-{}-{}'.format(formset.prefix, count, key)] = value
data["{}-{}-{}".format(formset.prefix, count, key)] = value
count += 1
data['{}-INITIAL_FORMS'.format(formset.prefix)] = 0
data['{}-TOTAL_FORMS'.format(formset.prefix)] = count
data["{}-INITIAL_FORMS".format(formset.prefix)] = 0
data["{}-TOTAL_FORMS".format(formset.prefix)] = count
return TestSegmentAdminForm(data)
@ -39,21 +46,27 @@ def test_user_added_to_static_segment_at_creation(site, user, mocker):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
mocker.patch(
"wagtail_personalisation.rules.VisitCountRule.test_user", return_value=True
)
instance = form.save()
assert user in instance.static_users.all()
@pytest.mark.django_db
def test_user_not_added_to_full_static_segment_at_creation(site, django_user_model, mocker):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second')
def test_user_not_added_to_full_static_segment_at_creation(
site, django_user_model, mocker
):
django_user_model.objects.create(username="first")
django_user_model.objects.create(username="second")
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user',
side_effect=[True, True])
mocker.patch(
"wagtail_personalisation.rules.VisitCountRule.test_user",
side_effect=[True, True],
)
instance = form.save()
assert len(instance.static_users.all()) == 1
@ -68,7 +81,9 @@ def test_anonymous_user_not_added_to_static_segment_at_creation(site, client, mo
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
mock_test_rule = mocker.patch(
"wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules"
)
instance = form.save()
assert not instance.static_users.all()
@ -77,15 +92,18 @@ def test_anonymous_user_not_added_to_static_segment_at_creation(site, client, mo
@pytest.mark.django_db
def test_match_any_correct_populates(site, django_user_model, mocker):
user = django_user_model.objects.create(username='first')
other_user = django_user_model.objects.create(username='second')
user = django_user_model.objects.create(username="first")
other_user = django_user_model.objects.create(username="second")
other_page = site.root_page.get_last_child()
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, match_any=True)
rule_1 = VisitCountRule(counted_page=site.root_page)
rule_2 = VisitCountRule(counted_page=other_page)
form = form_with_data(segment, rule_1, rule_2)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', side_effect=[True, False, True, False])
mocker.patch(
"wagtail_personalisation.rules.VisitCountRule.test_user",
side_effect=[True, False, True, False],
)
instance = form.save()
assert user in instance.static_users.all()
@ -102,7 +120,9 @@ def test_mixed_static_dynamic_session_doesnt_generate_at_creation(site, mocker):
)
form = form_with_data(segment, static_rule, non_static_rule)
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
mock_test_rule = mocker.patch(
"wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules"
)
instance = form.save()
assert not instance.static_users.all()
@ -154,9 +174,11 @@ def test_anonymou_user_not_added_to_static_segment_after_creation(site, client):
@pytest.mark.django_db
def test_session_not_added_to_static_segment_after_full(site, client, django_user_model):
user = django_user_model.objects.create(username='first')
other_user = django_user_model.objects.create(username='second')
def test_session_not_added_to_static_segment_after_full(
site, client, django_user_model
):
user = django_user_model.objects.create(username="first")
other_user = django_user_model.objects.create(username="second")
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
@ -190,7 +212,9 @@ def test_sessions_not_added_to_static_segment_if_rule_not_static(mocker):
segment=segment,
)
form = form_with_data(segment, rule)
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
mock_test_rule = mocker.patch(
"wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules"
)
instance = form.save()
assert not instance.static_users.all()
@ -202,12 +226,16 @@ def test_does_not_calculate_the_segment_again(site, client, mocker, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=2)
rule = VisitCountRule(counted_page=site.root_page, segment=segment)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
mocker.patch(
"wagtail_personalisation.rules.VisitCountRule.test_user", return_value=True
)
instance = form.save()
assert user in instance.static_users.all()
mock_test_rule = mocker.patch('wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
mock_test_rule = mocker.patch(
"wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules"
)
session = client.session
session.save()
client.force_login(user)
@ -247,7 +275,9 @@ def test_dynamic_segment_with_non_static_rules_have_a_count():
@pytest.mark.django_db
def test_randomisation_percentage_added_to_segment_at_creation(site, client, mocker, django_user_model):
def test_randomisation_percentage_added_to_segment_at_creation(
site, client, mocker, django_user_model
):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
segment.randomisation_percent = 80
rule = VisitCountRule()
@ -280,80 +310,85 @@ def test_randomisation_percentage_max_100(site, client, mocker, django_user_mode
@pytest.mark.django_db
def test_in_static_segment_if_random_is_below_percentage(site, client, mocker, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
randomisation_percent=40)
segment = SegmentFactory.build(
type=Segment.TYPE_STATIC, count=1, randomisation_percent=40
)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
mocker.patch('random.randint', return_value=39)
mocker.patch("random.randint", return_value=39)
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
assert instance.id == client.session['segments'][0]['id']
assert instance.id == client.session["segments"][0]["id"]
assert user in instance.static_users.all()
assert user not in instance.excluded_users.all()
@pytest.mark.django_db
def test_not_in_static_segment_if_random_is_above_percentage(site, client, mocker, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
randomisation_percent=40)
def test_not_in_static_segment_if_random_is_above_percentage(
site, client, mocker, user
):
segment = SegmentFactory.build(
type=Segment.TYPE_STATIC, count=1, randomisation_percent=40
)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
mocker.patch('random.randint', return_value=41)
mocker.patch("random.randint", return_value=41)
session = client.session
session.save()
client.force_login(user)
client.get(site.root_page.url)
assert len(client.session['segments']) == 0
assert len(client.session["segments"]) == 0
assert user not in instance.static_users.all()
assert user in instance.excluded_users.all()
@pytest.mark.django_db
def test_offered_dynamic_segment_if_random_is_below_percentage(site, client, mocker):
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
randomisation_percent=40)
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC, randomisation_percent=40)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
mocker.patch('random.randint', return_value=39)
mocker.patch("random.randint", return_value=39)
session = client.session
session.save()
client.get(site.root_page.url)
assert len(client.session['excluded_segments']) == 0
assert instance.id == client.session['segments'][0]['id']
assert len(client.session["excluded_segments"]) == 0
assert instance.id == client.session["segments"][0]["id"]
@pytest.mark.django_db
def test_not_offered_dynamic_segment_if_random_is_above_percentage(site, client, mocker):
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
randomisation_percent=40)
def test_not_offered_dynamic_segment_if_random_is_above_percentage(
site, client, mocker
):
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC, randomisation_percent=40)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
mocker.patch('random.randint', return_value=41)
mocker.patch("random.randint", return_value=41)
session = client.session
session.save()
client.get(site.root_page.url)
assert len(client.session['segments']) == 0
assert instance.id == client.session['excluded_segments'][0]['id']
assert len(client.session["segments"]) == 0
assert instance.id == client.session["excluded_segments"][0]["id"]
@pytest.mark.django_db
def test_not_in_segment_if_percentage_is_0(site, client, mocker, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
randomisation_percent=0)
segment = SegmentFactory.build(
type=Segment.TYPE_STATIC, count=1, randomisation_percent=0
)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
@ -363,15 +398,16 @@ def test_not_in_segment_if_percentage_is_0(site, client, mocker, user):
client.force_login(user)
client.get(site.root_page.url)
assert len(client.session['segments']) == 0
assert len(client.session["segments"]) == 0
assert user not in instance.static_users.all()
assert user in instance.excluded_users.all()
@pytest.mark.django_db
def test_always_in_segment_if_percentage_is_100(site, client, mocker, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
randomisation_percent=100)
segment = SegmentFactory.build(
type=Segment.TYPE_STATIC, count=1, randomisation_percent=100
)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
@ -381,18 +417,22 @@ def test_always_in_segment_if_percentage_is_100(site, client, mocker, user):
client.force_login(user)
client.get(site.root_page.url)
assert instance.id == client.session['segments'][0]['id']
assert instance.id == client.session["segments"][0]["id"]
assert user in instance.static_users.all()
assert user not in instance.excluded_users.all()
@pytest.mark.django_db
def test_not_added_to_static_segment_at_creation_if_random_above_percent(site, mocker, user):
mocker.patch('random.randint', return_value=41)
def test_not_added_to_static_segment_at_creation_if_random_above_percent(
site, mocker, user
):
mocker.patch("random.randint", return_value=41)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, randomisation_percent=40)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
mocker.patch(
"wagtail_personalisation.rules.VisitCountRule.test_user", return_value=True
)
instance = form.save()
assert user not in instance.static_users.all()
@ -400,12 +440,16 @@ def test_not_added_to_static_segment_at_creation_if_random_above_percent(site, m
@pytest.mark.django_db
def test_added_to_static_segment_at_creation_if_random_below_percent(site, mocker, user):
mocker.patch('random.randint', return_value=39)
def test_added_to_static_segment_at_creation_if_random_below_percent(
site, mocker, user
):
mocker.patch("random.randint", return_value=39)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, randomisation_percent=40)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
mocker.patch(
"wagtail_personalisation.rules.VisitCountRule.test_user", return_value=True
)
instance = form.save()
assert user in instance.static_users.all()
@ -414,8 +458,9 @@ def test_added_to_static_segment_at_creation_if_random_below_percent(site, mocke
@pytest.mark.django_db
def test_rules_check_skipped_if_user_in_excluded(site, client, mocker, user):
segment = SegmentFactory.build(type=Segment.TYPE_STATIC, count=1,
randomisation_percent=100)
segment = SegmentFactory.build(
type=Segment.TYPE_STATIC, count=1, randomisation_percent=100
)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
@ -423,7 +468,8 @@ def test_rules_check_skipped_if_user_in_excluded(site, client, mocker, user):
instance.save
mock_test_rule = mocker.patch(
'wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
"wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules"
)
session = client.session
session.save()
@ -431,15 +477,14 @@ def test_rules_check_skipped_if_user_in_excluded(site, client, mocker, user):
client.get(site.root_page.url)
assert mock_test_rule.call_count == 0
assert len(client.session['segments']) == 0
assert len(client.session["segments"]) == 0
assert user not in instance.static_users.all()
assert user in instance.excluded_users.all()
@pytest.mark.django_db
def test_rules_check_skipped_if_dynamic_segment_in_excluded(site, client, mocker, user):
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC,
randomisation_percent=100)
segment = SegmentFactory.build(type=Segment.TYPE_DYNAMIC, randomisation_percent=100)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
instance = form.save()
@ -447,30 +492,35 @@ def test_rules_check_skipped_if_dynamic_segment_in_excluded(site, client, mocker
instance.save()
session = client.session
session['excluded_segments'] = [{'id': instance.pk}]
session["excluded_segments"] = [{"id": instance.pk}]
session.save()
mock_test_rule = mocker.patch(
'wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules')
"wagtail_personalisation.adapters.SessionSegmentsAdapter._test_rules"
)
client.force_login(user)
client.get(site.root_page.url)
assert mock_test_rule.call_count == 0
assert len(client.session['segments']) == 0
assert len(client.session["segments"]) == 0
@pytest.mark.django_db
def test_matched_user_count_added_to_segment_at_creation(site, mocker, django_user_model):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second')
def test_matched_user_count_added_to_segment_at_creation(
site, mocker, django_user_model
):
django_user_model.objects.create(username="first")
django_user_model.objects.create(username="second")
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule()
form = form_with_data(segment, rule)
form.instance.type = Segment.TYPE_STATIC
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
mock_test_user = mocker.patch(
"wagtail_personalisation.rules.VisitCountRule.test_user", return_value=True
)
instance = form.save()
assert mock_test_user.call_count == 2
@ -479,49 +529,59 @@ def test_matched_user_count_added_to_segment_at_creation(site, mocker, django_us
@pytest.mark.django_db
def test_count_users_matching_static_rules(site, client, mocker, django_user_model):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second')
django_user_model.objects.create(username="first")
django_user_model.objects.create(username="second")
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
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
def test_count_matching_users_excludes_staff(site, client, mocker, django_user_model):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second', is_staff=True)
django_user_model.objects.create(username="first")
django_user_model.objects.create(username="second", is_staff=True)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
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
@pytest.mark.django_db
def test_count_matching_users_excludes_inactive(site, client, mocker, django_user_model):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second', is_active=False)
def test_count_matching_users_excludes_inactive(
site, client, mocker, django_user_model
):
django_user_model.objects.create(username="first")
django_user_model.objects.create(username="second", is_active=False)
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = VisitCountRule(counted_page=site.root_page)
form = form_with_data(segment, rule)
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
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
@pytest.mark.django_db
def test_count_matching_users_only_counts_static_rules(site, client, mocker, django_user_model):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second')
def test_count_matching_users_only_counts_static_rules(
site, client, mocker, django_user_model
):
django_user_model.objects.create(username="first")
django_user_model.objects.create(username="second")
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
rule = TimeRule(
@ -530,16 +590,18 @@ def test_count_matching_users_only_counts_static_rules(site, client, mocker, dja
segment=segment,
)
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) is 0
assert form.count_matching_users([rule], True) == 0
assert mock_test_user.call_count == 0
@pytest.mark.django_db
def test_count_matching_users_handles_match_any(site, client, mocker, django_user_model):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second')
def test_count_matching_users_handles_match_any(
site, client, mocker, django_user_model
):
django_user_model.objects.create(username="first")
django_user_model.objects.create(username="second")
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
first_rule = VisitCountRule(counted_page=site.root_page)
@ -548,17 +610,20 @@ def test_count_matching_users_handles_match_any(site, client, mocker, django_use
form = form_with_data(segment, first_rule, second_rule)
mock_test_user = mocker.patch(
'wagtail_personalisation.rules.VisitCountRule.test_user',
side_effect=[True, False, True, False])
"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
@pytest.mark.django_db
def test_count_matching_users_handles_match_all(site, client, mocker, django_user_model):
django_user_model.objects.create(username='first')
django_user_model.objects.create(username='second')
def test_count_matching_users_handles_match_all(
site, client, mocker, django_user_model
):
django_user_model.objects.create(username="first")
django_user_model.objects.create(username="second")
segment = SegmentFactory.build(type=Segment.TYPE_STATIC)
first_rule = VisitCountRule(counted_page=site.root_page)
@ -567,8 +632,9 @@ def test_count_matching_users_handles_match_all(site, client, mocker, django_use
form = form_with_data(segment, first_rule, second_rule)
mock_test_user = mocker.patch(
'wagtail_personalisation.rules.VisitCountRule.test_user',
side_effect=[True, True, False, True])
"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

View File

@ -11,33 +11,44 @@ from tests.utils import render_template
@pytest.mark.django_db
def test_segment_template_block(rf, site):
SegmentFactory(name='test', persistent=True)
SegmentFactory(name="test", persistent=True)
request = rf.get('/')
request = rf.get("/")
request.session['segments'] = [{
"encoded_name": 'test',
"id": 1,
"timestamp": int(time.time()),
"persistent": True
}]
request.session["segments"] = [
{
"encoded_name": "test",
"id": 1,
"timestamp": int(time.time()),
"persistent": True,
}
]
content = render_template("""
content = render_template(
"""
{% load wagtail_personalisation_tags %}
{% segment name='test' %}Test{% endsegment %}
""", request=request).strip()
""",
request=request,
).strip()
assert content == "Test"
content = render_template("""
content = render_template(
"""
{% load wagtail_personalisation_tags %}
{% segment name='test2' %}Test{% endsegment %}
""", request=request).strip()
""",
request=request,
).strip()
assert content == ""
with pytest.raises(TemplateSyntaxError):
content = render_template("""
content = render_template(
"""
{% load wagtail_personalisation_tags %}
{% segment wrongname='test2' %}Test{% endsegment %}
""", request=request).strip()
""",
request=request,
).strip()

View File

@ -1,29 +1,44 @@
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 # noqa
locale_factory = True # noqa
except ImportError:
pass
@pytest.fixture
def rootpage():
return ContentPageFactory(parent=None, path='/', depth=0, title='root')
return ContentPageFactory(parent=None, path="/", depth=0, title="root")
@pytest.fixture
def page(rootpage):
return ContentPageFactory(parent=rootpage, path='/hi', title='Hi')
return ContentPageFactory(parent=rootpage, path="/hi", title="Hi")
@pytest.fixture
def otherpage(rootpage):
return ContentPageFactory(parent=rootpage, path='/bye', title='Bye')
return ContentPageFactory(parent=rootpage, path="/bye", title="Bye")
@pytest.mark.django_db
def test_impersonate_other_page(page, otherpage):
impersonate_other_page(page, otherpage)
assert page.title == otherpage.title == 'Bye'
assert page.title == otherpage.title == "Bye"
assert page.path == otherpage.path
@ -36,3 +51,94 @@ 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)
)

View File

@ -5,31 +5,29 @@ 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)
from wagtail_personalisation.views import SegmentModelAdmin, SegmentModelDeleteView
@pytest.mark.django_db
def test_segment_user_data_view_requires_admin_access(site, client, django_user_model):
user = django_user_model.objects.create(username='first')
user = django_user_model.objects.create(username="first")
segment = Segment(type=Segment.TYPE_STATIC, count=1)
segment.save()
client.force_login(user)
url = reverse('segment:segment_user_data', args=(segment.id,))
url = reverse("segment:segment_user_data", args=(segment.id,))
response = client.get(url)
assert response.status_code == 302
assert response.url == '/admin/login/?next=%s' % url
assert response.url == "/admin/login/?next=%s" % url
@pytest.mark.django_db
def test_segment_user_data_view(site, client, mocker, django_user_model):
user1 = django_user_model.objects.create(username='first')
user2 = django_user_model.objects.create(username='second')
admin_user = django_user_model.objects.create(
username='admin', is_superuser=True)
user1 = django_user_model.objects.create(username="first")
user2 = django_user_model.objects.create(username="second")
admin_user = django_user_model.objects.create(username="admin", is_superuser=True)
segment = Segment(type=Segment.TYPE_STATIC, count=1)
segment.save()
@ -37,24 +35,28 @@ def test_segment_user_data_view(site, client, mocker, django_user_model):
segment.static_users.add(user2)
rule1 = VisitCountRule(counted_page=site.root_page, segment=segment)
rule2 = VisitCountRule(counted_page=site.root_page.get_last_child(),
segment=segment)
rule2 = VisitCountRule(
counted_page=site.root_page.get_last_child(), segment=segment
)
rule1.save()
rule2.save()
mocker.patch('wagtail_personalisation.rules.VisitCountRule.get_user_info_string',
side_effect=[3, 9, 0, 1])
mocker.patch(
"wagtail_personalisation.rules.VisitCountRule.get_user_info_string",
side_effect=[3, 9, 0, 1],
)
client.force_login(admin_user)
response = client.get(
reverse('segment:segment_user_data', args=(segment.id,)))
response = client.get(reverse("segment:segment_user_data", args=(segment.id,)))
assert response.status_code == 200
data_lines = response.content.decode().split("\n")
assert data_lines[0] == 'Username,Visit count - Test page,Visit count - Regular page\r'
assert data_lines[1] == 'first,3,9\r'
assert data_lines[2] == 'second,0,1\r'
assert (
data_lines[0] == "Username,Visit count - Test page,Visit count - Regular page\r"
)
assert data_lines[1] == "first,3,9\r"
assert data_lines[2] == "second,0,1\r"
@pytest.mark.django_db
@ -64,9 +66,9 @@ def test_segment_delete_view_delete_instance(rf, segmented_page, user):
segment = segmented_page.personalisation_metadata.segment
canonical_page = segmented_page.personalisation_metadata.canonical_page
variants_metadata = segment.get_used_pages()
page_variants = Page.objects.filter(pk__in=(
variants_metadata.values_list('variant_id', flat=True)
))
page_variants = Page.objects.filter(
pk__in=(variants_metadata.values_list("variant_id", flat=True))
)
# Make sure all canonical page, variants and variants metadata exist
assert canonical_page
@ -74,11 +76,10 @@ def test_segment_delete_view_delete_instance(rf, segmented_page, user):
assert variants_metadata
# Delete the segment via the method on the view.
request = rf.get('/'.format(segment.pk))
request = rf.get("/".format(segment.pk)) # noqa
request.user = user
view = SegmentModelDeleteView(
instance_pk=str(segment.pk),
model_admin=SegmentModelAdmin()
instance_pk=str(segment.pk), model_admin=SegmentModelAdmin()
)
view.request = request
view.delete_instance()
@ -98,13 +99,12 @@ def test_segment_delete_view_delete_instance(rf, segmented_page, user):
@pytest.mark.django_db
def test_segment_delete_view_raises_permission_denied(rf, segmented_page, user):
segment = segmented_page.personalisation_metadata.segment
request = rf.get('/'.format(segment.pk))
request = rf.get("/".format(segment.pk)) # noqa
request.user = user
view = SegmentModelDeleteView(
instance_pk=str(segment.pk),
model_admin=SegmentModelAdmin()
instance_pk=str(segment.pk), model_admin=SegmentModelAdmin()
)
view.request = request
message = 'User have no permission to delete variant page objects.'
with pytest.raises(PermissionDenied, message=message):
message = "User have no permission to delete variant page objects." # noqa
with pytest.raises(PermissionDenied):
view.delete_instance()

View File

@ -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
@ -8,7 +10,7 @@ from wagtail_personalisation import adapters, wagtail_hooks
@pytest.mark.django_db
def test_serve_variant_no_variant(site, rf):
page = site.root_page
request = rf.get('/')
request = rf.get("/")
args = tuple()
kwargs = {}
@ -16,9 +18,18 @@ 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('/')
request = rf.get("/")
args = tuple()
kwargs = {}
@ -29,7 +40,7 @@ def test_serve_variant_with_variant_no_segment(site, rf, segmented_page):
@pytest.mark.django_db
def test_serve_variant_with_variant_segmented(site, rf, segmented_page):
request = rf.get('/')
request = rf.get("/")
args = tuple()
kwargs = {}
@ -47,7 +58,7 @@ def test_serve_variant_with_variant_segmented(site, rf, segmented_page):
def test_page_listing_variant_buttons(site, rf, segmented_page):
page = segmented_page.personalisation_metadata.canonical_page
SegmentFactory(name='something')
SegmentFactory(name="something")
result = wagtail_hooks.page_listing_variant_buttons(page, [])
items = list(result)
assert len(items) == 1
@ -57,47 +68,42 @@ def test_page_listing_variant_buttons(site, rf, segmented_page):
def test_page_listing_more_buttons(site, rf, segmented_page):
page = segmented_page.personalisation_metadata.canonical_page
SegmentFactory(name='something')
SegmentFactory(name="something")
result = wagtail_hooks.page_listing_more_buttons(page, [])
items = list(result)
assert len(items) == 3
@pytest.mark.django_db
def test_custom_delete_page_view_does_not_trigger_for_variants(
rf,
segmented_page
):
assert (
wagtail_hooks.delete_related_variants(rf.get('/'), segmented_page)
) is None
def test_custom_delete_page_view_does_not_trigger_for_variants(rf, segmented_page):
assert (wagtail_hooks.delete_related_variants(rf.get("/"), segmented_page)) is None
@pytest.mark.django_db
def test_custom_delete_page_view_triggers_for_canonical_pages(
rf,
segmented_page
):
def test_custom_delete_page_view_triggers_for_canonical_pages(rf, segmented_page):
assert (
wagtail_hooks.delete_related_variants(
rf.get('/'),
segmented_page.personalisation_metadata.canonical_page
rf.get("/"), segmented_page.personalisation_metadata.canonical_page
)
) is not None
@pytest.mark.django_db
def test_custom_delete_page_view_deletes_variants(rf, segmented_page, user):
post_request = rf.post('/')
post_request = rf.post("/")
user.is_superuser = True
rf.user = user
canonical_page = segmented_page.personalisation_metadata.canonical_page
canonical_page_variant = canonical_page.personalisation_metadata
assert canonical_page_variant
variants = Page.objects.filter(pk__in=(
canonical_page.personalisation_metadata.variants_metadata.values_list('variant_id', flat=True)
))
variants = Page.objects.filter(
pk__in=(
canonical_page.personalisation_metadata.variants_metadata.values_list(
"variant_id", flat=True
)
)
)
variants_metadata = canonical_page.personalisation_metadata.variants_metadata
# Make sure there are variants that exist in the database.
assert len(variants.all())
@ -112,3 +118,23 @@ 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)

View File

@ -2,6 +2,10 @@ from django.template import engines
def render_template(value, **context):
template = engines['django'].from_string(value)
request = context.pop('request', None)
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"

50
tox.ini
View File

@ -1,17 +1,40 @@
[tox]
envlist = py{36}-django{20}-wagtail{20,21},lint
envlist =
flake8
py{37,38}-dj{22}-wt{211,212,213}
py{37,38,39}-dj{30,31}-wt{211,212,213}
py{37,38,39}-dj{30,31,32}-wt{214,215}
py{39}-dj{32,40}-wt{216}
[gh-actions]
python =
3.7: py37
3.8: py38
3.9: py39
[testenv]
basepython = python3.6
commands = coverage run --parallel -m pytest {posargs}
basepython =
py37: python3.7
py38: python3.8
py39: python3.9
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
dj32: Django>=3.2,<3.3
dj40: Django>=4.0,<4.1
wt211: wagtail>=2.11,<2.12
wt212: wagtail>=2.12,<2.13
wt213: wagtail>=2.13,<2.14
wt214: wagtail>=2.14,<2.15
wt215: wagtail>=2.15,<2.16
wt216: wagtail>=2.16,<2.17
geoip2: geoip2
[testenv:coverage-report]
basepython = python3.6
basepython = python3.8
deps = coverage
pip_pre = true
skip_install = true
@ -19,8 +42,17 @@ commands =
coverage report --include="src/**/" --omit="src/**/migrations/*.py"
[testenv:lint]
basepython = python3.6
deps = flake8
basepython = python3.8
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/

4777
yarn.lock

File diff suppressed because it is too large Load Diff