7

Compare commits

..

62 Commits

Author SHA1 Message Date
2e2f63755e Bump version: 0.13.0 → 0.14.0 2019-09-27 09:15:57 +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
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
36 changed files with 3897 additions and 1259 deletions

1
.gitignore vendored
View File

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

View File

@ -14,6 +14,28 @@ matrix:
env: TOXENV=py36-django20-wagtail21 env: TOXENV=py36-django20-wagtail21
- python: 3.6 - python: 3.6
env: TOXENV=py36-django20-wagtail21-geoip2 env: TOXENV=py36-django20-wagtail21-geoip2
- python: 3.6
env: TOXENV=py36-django20-wagtail22
- python: 3.6
env: TOXENV=py36-django20-wagtail22-geoip2
- python: 3.6
env: TOXENV=py36-django21-wagtail23
- python: 3.6
env: TOXENV=py36-django21-wagtail23-geoip2
- python: 3.6
env: TOXENV=py36-django21-wagtail24
- python: 3.6
env: TOXENV=py36-django21-wagtail24-geoip2
- python: 3.6
env: TOXENV=py36-django22-wagtail25
- python: 3.6
env: TOXENV=py36-django22-wagtail25-geoip2
- python: 3.6
env: TOXENV=py36-django22-wagtail26
- python: 3.6
env: TOXENV=py36-django22-wagtail26-geoip2
- python: 3.6
env: TOXENV=py36-django111-wagtail22
install: install:
- pip install tox codecov - pip install tox codecov

12
CHANGES
View File

@ -1,3 +1,15 @@
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 0.12.0
================== ==================
- Merged forks of Torchbox and Praekelt - Merged forks of Torchbox and Praekelt

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 0.12.1 current_version = 0.14.0
commit = true commit = true
tag = true tag = true
tag_name = {new_version} tag_name = {new_version}

View File

@ -20,7 +20,7 @@ tests_require = [
'pytest-pythonpath==0.7.2', 'pytest-pythonpath==0.7.2',
'pytest-sugar==0.9.1', 'pytest-sugar==0.9.1',
'pytest==3.4.2', 'pytest==3.4.2',
'wagtail_factories==1.0.0', 'wagtail_factories==1.1.0',
'pytest-mock==1.6.3', 'pytest-mock==1.6.3',
] ]
@ -35,7 +35,7 @@ with open('README.rst') as fh:
setup( setup(
name='wagtail-personalisation', name='wagtail-personalisation',
version='0.12.1', version='0.14.0',
description='A Wagtail add-on for showing personalized content', description='A Wagtail add-on for showing personalized content',
author='Lab Digital BV and others', author='Lab Digital BV and others',
author_email='opensource@labdigital.nl', author_email='opensource@labdigital.nl',
@ -52,7 +52,7 @@ setup(
license='MIT', license='MIT',
long_description=long_description, long_description=long_description,
classifiers=[ classifiers=[
'Development Status :: 2 - Pre-Alpha', 'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment', 'Environment :: Web Environment',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',

View File

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

View File

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

View File

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

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from wagtail.core import blocks from wagtail.core import blocks

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import, unicode_literals
from datetime import datetime from datetime import datetime
from importlib import import_module from importlib import import_module

View File

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

View File

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

View File

@ -1,8 +1,8 @@
# Generated by Django 2.0.6 on 2018-08-10 14:39 # Generated by Django 2.0.6 on 2018-08-10 14:39
from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import modelcluster.fields import modelcluster.fields
from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):

File diff suppressed because one or more lines are too long

View File

@ -1,16 +1,15 @@
import random import random
import wagtail
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models, transaction from django.db import models, transaction
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.utils.encoding import python_2_unicode_compatible
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from modelcluster.models import ClusterableModel from modelcluster.models import ClusterableModel
import wagtail
from wagtail.admin.edit_handlers import ( from wagtail.admin.edit_handlers import (
FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel) FieldPanel, FieldRowPanel, InlinePanel, MultiFieldPanel)
from wagtail.core.models import Page from wagtail.core.models import Page
@ -21,12 +20,19 @@ from wagtail_personalisation.utils import count_active_days
from .forms import SegmentAdminForm from .forms import SegmentAdminForm
class RulePanel(InlinePanel):
def on_model_bound(self):
self.relation_name = self.relation_name.replace('_related', 's')
self.db_field = self.model._meta.get_field(self.relation_name)
manager = getattr(self.model, self.relation_name)
self.related = manager.rel
class SegmentQuerySet(models.QuerySet): class SegmentQuerySet(models.QuerySet):
def enabled(self): def enabled(self):
return self.filter(status=self.model.STATUS_ENABLED) return self.filter(status=self.model.STATUS_ENABLED)
@python_2_unicode_compatible
class Segment(ClusterableModel): class Segment(ClusterableModel):
"""The segment model.""" """The segment model."""
STATUS_ENABLED = 'enabled' STATUS_ENABLED = 'enabled'
@ -121,8 +127,8 @@ class Segment(ClusterableModel):
FieldPanel('randomisation_percent', classname='percent_field'), FieldPanel('randomisation_percent', classname='percent_field'),
], heading="Segment"), ], heading="Segment"),
MultiFieldPanel([ MultiFieldPanel([
InlinePanel( RulePanel(
"{}s".format(rule_model._meta.db_table), "{}_related".format(rule_model._meta.db_table),
label='{}{}'.format( label='{}{}'.format(
rule_model._meta.verbose_name, rule_model._meta.verbose_name,
' ({})'.format(_('Static compatible')) if rule_model.static else '' ' ({})'.format(_('Static compatible')) if rule_model.static else ''

View File

@ -1,8 +1,5 @@
from __future__ import absolute_import, unicode_literals
import logging import logging
import re import re
from datetime import datetime
from importlib import import_module from importlib import import_module
import pycountry import pycountry
@ -13,7 +10,7 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from user_agents import parse from user_agents import parse
@ -43,7 +40,6 @@ def get_geoip_module():
'warning if you use one of those.') 'warning if you use one of those.')
@python_2_unicode_compatible
class AbstractBaseRule(models.Model): class AbstractBaseRule(models.Model):
"""Base for creating rules to segment users with.""" """Base for creating rules to segment users with."""
icon = 'fa-circle-o' icon = 'fa-circle-o'
@ -59,7 +55,7 @@ class AbstractBaseRule(models.Model):
verbose_name = 'Abstract segmentation rule' verbose_name = 'Abstract segmentation rule'
def __str__(self): def __str__(self):
return force_text(self._meta.verbose_name) return str(self._meta.verbose_name)
def test_user(self): def test_user(self):
"""Test if the user matches this rule.""" """Test if the user matches this rule."""
@ -67,7 +63,7 @@ class AbstractBaseRule(models.Model):
def encoded_name(self): def encoded_name(self):
"""Return a string with a slug for the rule.""" """Return a string with a slug for the rule."""
return slugify(force_text(self).lower()) return slugify(str(self).lower())
def description(self): def description(self):
"""Return a description explaining the functionality of the rule. """Return a description explaining the functionality of the rule.
@ -113,7 +109,7 @@ class TimeRule(AbstractBaseRule):
verbose_name = _('Time Rule') verbose_name = _('Time Rule')
def test_user(self, request=None): def test_user(self, request=None):
return self.start_time <= datetime.now().time() <= self.end_time return self.start_time <= timezone.now().time() <= self.end_time
def description(self): def description(self):
return { return {
@ -157,7 +153,7 @@ class DayRule(AbstractBaseRule):
def test_user(self, request=None): def test_user(self, request=None):
return [self.mon, self.tue, self.wed, self.thu, return [self.mon, self.tue, self.wed, self.thu,
self.fri, self.sat, self.sun][datetime.today().weekday()] self.fri, self.sat, self.sun][timezone.now().date().weekday()]
def description(self): def description(self):
days = ( days = (

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import, unicode_literals
import csv import csv
from django import forms from django import forms
@ -87,7 +85,10 @@ class SegmentModelAdmin(ModelAdmin):
'page_count', 'variant_count', 'statistics') 'page_count', 'variant_count', 'statistics')
index_view_extra_js = ['js/commons.js', 'js/index.js'] index_view_extra_js = ['js/commons.js', 'js/index.js']
index_view_extra_css = ['css/index.css'] index_view_extra_css = ['css/index.css']
form_view_extra_js = ['js/commons.js', 'js/form.js'] form_view_extra_js = ['js/commons.js', 'js/form.js',
'js/segment_form_control.js',
'wagtailadmin/js/page-chooser-modal.js',
'wagtailadmin/js/page-chooser.js']
form_view_extra_css = ['css/form.css'] form_view_extra_css = ['css/form.css']
def index_view(self, request): def index_view(self, request):

View File

@ -1,9 +1,8 @@
from __future__ import absolute_import, unicode_literals
import logging import logging
from django.conf.urls import include, url from django.conf.urls import include, url
from django.db import transaction from django.db import transaction
from django.db.models import F
from django.http import Http404 from django.http import Http404
from django.shortcuts import redirect, render from django.shortcuts import redirect, render
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize
@ -19,6 +18,7 @@ from wagtail.core.models import Page
from wagtail_personalisation import admin_urls, models, utils from wagtail_personalisation import admin_urls, models, utils
from wagtail_personalisation.adapters import get_segment_adapter from wagtail_personalisation.adapters import get_segment_adapter
from wagtail_personalisation.models import PersonalisablePageMetadata
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -227,8 +227,7 @@ class PersonalisedPagesSummaryPanel(PagesSummaryItem):
order = 2100 order = 2100
def render(self): def render(self):
page_count = models.PersonalisablePageMetadata.objects.filter( page_count = models.PersonalisablePageMetadata.objects.filter(segment__isnull=True).count()
segment__isnull=True).count()
title = _("Personalised Page") title = _("Personalised Page")
return mark_safe(""" return mark_safe("""
<li class="icon icon-fa-file-o"> <li class="icon icon-fa-file-o">
@ -276,14 +275,22 @@ def delete_related_variants(request, page):
if request.method == 'POST': if request.method == 'POST':
parent_id = page.get_parent().id parent_id = page.get_parent().id
variants_metadata = variants_metadata.select_related('variant')
with transaction.atomic(): with transaction.atomic():
for metadata in variants_metadata.iterator(): # To ensure variants are deleted for all descendants, start with
# Call delete() on objects to trigger any signals or hooks. # the deepest ones, and explicitly delete variants and metadata
metadata.variant.delete() # for all of them, including the page itself. Otherwise protected
# Delete the page's main variant and the page itself. # foreign key constraints are violated. Only consider canonical
page.personalisation_metadata.delete() # pages.
page.delete() 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()
msg = _("Page '{0}' and its variants deleted.") msg = _("Page '{0}' and its variants deleted.")
messages.success( messages.success(
request, request,

View File

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

View File

@ -5,6 +5,7 @@ from django.utils.text import slugify
from wagtail_factories.factories import PageFactory from wagtail_factories.factories import PageFactory
from tests.site.pages import models from tests.site.pages import models
from wagtail_personalisation.models import PersonalisablePageMetadata
class ContentPageFactory(PageFactory): class ContentPageFactory(PageFactory):
@ -22,3 +23,9 @@ class RegularPageFactory(PageFactory):
class Meta: class Meta:
model = models.RegularPage model = models.RegularPage
class PersonalisablePageMetadataFactory(factory.DjangoModelFactory):
class Meta:
model = PersonalisablePageMetadata

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import, unicode_literals
import os import os
DATABASES = { DATABASES = {
@ -52,6 +50,7 @@ TEMPLATES = [
}, },
] ]
MIDDLEWARE = ( MIDDLEWARE = (
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',

View File

@ -1,5 +1,3 @@
from __future__ import absolute_import, unicode_literals
import datetime import datetime
import pytest import pytest
@ -8,7 +6,7 @@ from django.db.models import ProtectedError
from tests.factories.page import ContentPageFactory from tests.factories.page import ContentPageFactory
from tests.factories.segment import SegmentFactory from tests.factories.segment import SegmentFactory
from tests.site.pages import models from tests.site.pages import models
from wagtail_personalisation.models import PersonalisablePageMetadata from wagtail_personalisation.models import PersonalisablePageMetadata, Segment
from wagtail_personalisation.rules import TimeRule from wagtail_personalisation.rules import TimeRule
@ -73,3 +71,10 @@ def test_sitemap_generation_for_canonical_pages_is_enabled(segmented_page):
def test_sitemap_generation_for_variants_is_disabled(segmented_page): def test_sitemap_generation_for_variants_is_disabled(segmented_page):
assert not segmented_page.personalisation_metadata.is_canonical assert not segmented_page.personalisation_metadata.is_canonical
assert not segmented_page.get_sitemap_urls() assert not segmented_page.get_sitemap_urls()
@pytest.mark.django_db
def test_segment_edit_view(site, client, django_user_model):
test_segment = SegmentFactory()
new_panel = test_segment.panels[1].children[0].bind_to_model(Segment)
assert new_panel.related.name == "wagtail_personalisation_timerules"

View File

@ -1,5 +1,5 @@
from importlib.util import find_spec from importlib.util import find_spec
from unittest.mock import call, MagicMock, patch from unittest.mock import MagicMock, call, patch
import pytest import pytest
@ -7,7 +7,6 @@ from tests.factories.rule import OriginCountryRuleFactory
from tests.factories.segment import SegmentFactory from tests.factories.segment import SegmentFactory
from wagtail_personalisation.rules import get_geoip_module from wagtail_personalisation.rules import get_geoip_module
skip_if_geoip2_installed = pytest.mark.skipif( skip_if_geoip2_installed = pytest.mark.skipif(
find_spec('geoip2'), reason='requires GeoIP2 to be not installed' find_spec('geoip2'), reason='requires GeoIP2 to be not installed'
) )

View File

@ -487,7 +487,7 @@ def test_count_users_matching_static_rules(site, client, mocker, django_user_mod
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True) mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
assert form.count_matching_users([rule], True) is 2 assert form.count_matching_users([rule], True) == 2
@pytest.mark.django_db @pytest.mark.django_db
@ -500,7 +500,7 @@ def test_count_matching_users_excludes_staff(site, client, mocker, django_user_m
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True) mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
assert form.count_matching_users([rule], True) is 1 assert form.count_matching_users([rule], True) == 1
assert mock_test_user.call_count == 1 assert mock_test_user.call_count == 1
@ -514,7 +514,7 @@ def test_count_matching_users_excludes_inactive(site, client, mocker, django_use
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True) mock_test_user = mocker.patch('wagtail_personalisation.rules.VisitCountRule.test_user', return_value=True)
assert form.count_matching_users([rule], True) is 1 assert form.count_matching_users([rule], True) == 1
assert mock_test_user.call_count == 1 assert mock_test_user.call_count == 1
@ -532,7 +532,7 @@ def test_count_matching_users_only_counts_static_rules(site, client, mocker, dja
form = form_with_data(segment, rule) form = form_with_data(segment, rule)
mock_test_user = mocker.patch('wagtail_personalisation.rules.TimeRule.test_user') mock_test_user = mocker.patch('wagtail_personalisation.rules.TimeRule.test_user')
assert form.count_matching_users([rule], True) is 0 assert form.count_matching_users([rule], True) == 0
assert mock_test_user.call_count == 0 assert mock_test_user.call_count == 0
@ -551,7 +551,7 @@ def test_count_matching_users_handles_match_any(site, client, mocker, django_use
'wagtail_personalisation.rules.VisitCountRule.test_user', 'wagtail_personalisation.rules.VisitCountRule.test_user',
side_effect=[True, False, True, False]) side_effect=[True, False, True, False])
assert form.count_matching_users([first_rule, second_rule], True) is 2 assert form.count_matching_users([first_rule, second_rule], True) == 2
mock_test_user.call_count == 4 mock_test_user.call_count == 4
@ -570,5 +570,5 @@ def test_count_matching_users_handles_match_all(site, client, mocker, django_use
'wagtail_personalisation.rules.VisitCountRule.test_user', 'wagtail_personalisation.rules.VisitCountRule.test_user',
side_effect=[True, True, False, True]) side_effect=[True, True, False, True])
assert form.count_matching_users([first_rule, second_rule], False) is 1 assert form.count_matching_users([first_rule, second_rule], False) == 1
mock_test_user.call_count == 4 mock_test_user.call_count == 4

View File

@ -1,10 +1,11 @@
import pytest import pytest
from django.test import override_settings 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 ( from wagtail_personalisation.utils import (
can_delete_pages, get_client_ip, impersonate_other_page) can_delete_pages, exclude_variants, get_client_ip, impersonate_other_page)
@pytest.fixture @pytest.fixture
@ -64,3 +65,57 @@ def test_get_client_ip_custom_get_client_ip_function_does_not_exist(rf):
) )
def test_get_client_ip_custom_get_client_ip_used(rf): def test_get_client_ip_custom_get_client_ip_used(rf):
assert get_client_ip(rf.get('/')) == '123.123.123.123' assert get_client_ip(rf.get('/')) == '123.123.123.123'
def test_exclude_variants_with_pages_querysets():
'''
Test that excludes variant works for querysets
'''
for i in range(5):
page = ContentPageFactory(path="/" + str(i), depth=0, url_path="/", title="Hoi " + str(i))
page.save()
pages = WagtailPage.objects.all().specific().order_by('id')
result = exclude_variants(pages)
assert type(result) == type(pages)
assert set(result.values_list('pk', flat=True)) == set(pages.values_list('pk', flat=True))
def test_exclude_variants_with_pages_querysets_not_canonical():
'''
Test that excludes variant works for querysets with
personalisation_metadata canonical False
'''
for i in range(5):
page = ContentPageFactory(path="/" + str(i), depth=0, url_path="/", title="Hoi " + str(i))
page.save()
pages = WagtailPage.objects.all().specific().order_by('id')
# add variants
for page in pages:
variant = ContentPageFactory(title='variant %d' % page.pk)
page.personalisation_metadata = PersonalisablePageMetadataFactory(canonical_page=page, variant=variant)
page.save()
pages = WagtailPage.objects.all().specific()
result = exclude_variants(pages)
assert type(result) == type(pages)
assert result.count() < pages.count()
def test_exclude_variants_with_pages_querysets_meta_none():
'''
Test that excludes variant works for querysets with meta as none
'''
for i in range(5):
page = ContentPageFactory(path="/" + str(i), depth=0, url_path="/", title="Hoi " + str(i))
page.save()
pages = WagtailPage.objects.all().specific().order_by('id')
# add variants
for page in pages:
page.personalisation_metadata = PersonalisablePageMetadataFactory(canonical_page=page, variant=page)
page.save()
pages = WagtailPage.objects.all().specific()
result = exclude_variants(pages)
assert type(result) == type(pages)
assert set(result.values_list('pk', flat=True)) == set(pages.values_list('pk', flat=True))

View File

@ -6,7 +6,7 @@ from wagtail.core.models import Page
from wagtail_personalisation.models import Segment from wagtail_personalisation.models import Segment
from wagtail_personalisation.rules import VisitCountRule from wagtail_personalisation.rules import VisitCountRule
from wagtail_personalisation.views import ( from wagtail_personalisation.views import (
SegmentModelDeleteView, SegmentModelAdmin) SegmentModelAdmin, SegmentModelDeleteView)
@pytest.mark.django_db @pytest.mark.django_db

View File

@ -1,9 +1,8 @@
import pytest import pytest
from django.http import Http404 from django.http import Http404
from wagtail.core.models import Page from wagtail.core.models import Page
from tests.factories.page import ContentPageFactory
from tests.factories.segment import SegmentFactory from tests.factories.segment import SegmentFactory
from wagtail_personalisation import adapters, wagtail_hooks from wagtail_personalisation import adapters, wagtail_hooks
@ -124,3 +123,21 @@ def test_custom_delete_page_view_deletes_variants(rf, segmented_page, user):
# Make sure all the variant pages have been deleted. # Make sure all the variant pages have been deleted.
assert not len(variants.all()) assert not len(variants.all())
assert not len(variants_metadata.all()) assert not len(variants_metadata.all())
@pytest.mark.django_db
def test_custom_delete_page_view_deletes_variants_of_child_pages(rf, segmented_page, user):
"""
Regression test for deleting pages that have children with variants
"""
post_request = rf.post('/')
user.is_superuser = True
rf.user = user
canonical_page = segmented_page.personalisation_metadata.canonical_page
# Create a child with a variant
child_page = ContentPageFactory(parent=canonical_page, slug='personalised-child')
child_page.personalisation_metadata.copy_for_segment(segmented_page.personalisation_metadata.segment)
# A ProtectedError would be raised if the bug persists
wagtail_hooks.delete_related_variants(
post_request, canonical_page
)

12
tox.ini
View File

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py{36}-django{20}-wagtail{20,21}{,-geoip2},lint envlist = py{36}-django{111,20,21,22}-wagtail{20,21,22,23,24,25,26}{,-geoip2},lint
[testenv] [testenv]
basepython = python3.6 basepython = python3.6
@ -7,9 +7,17 @@ commands = coverage run --parallel -m pytest -rs {posargs}
extras = test extras = test
deps = deps =
django20: django>=2.0,<2.1 django20: django>=2.0,<2.1
django21: django>=2.1,<2.2
django22: django>=2.2,<2.3
wagtail20: wagtail>=2.0,<2.1 wagtail20: wagtail>=2.0,<2.1
wagtail21: wagtail>=2.1,<2.2 wagtail21: wagtail>=2.1,<2.2
wagtail22: wagtail>=2.2,<2.3
wagtail23: wagtail>=2.3,<2.4
wagtail24: wagtail>=2.4,<2.5
wagtail25: wagtail>=2.5,<2.6
wagtail26: wagtail>=2.6,<2.7
geoip2: geoip2 geoip2: geoip2
django111: django>=1.11,<1.12
[testenv:coverage-report] [testenv:coverage-report]
basepython = python3.6 basepython = python3.6
@ -21,7 +29,7 @@ commands =
[testenv:lint] [testenv:lint]
basepython = python3.6 basepython = python3.6
deps = flake8 deps = flake8==3.5.0
commands = commands =
flake8 src tests setup.py flake8 src tests setup.py
isort -q --recursive --diff src/ tests/ isort -q --recursive --diff src/ tests/

4777
yarn.lock

File diff suppressed because it is too large Load Diff