Compare commits
9 Commits
0.12.0
...
feature/se
Author | SHA1 | Date | |
---|---|---|---|
acd273c06c | |||
4b3af020fd | |||
05afea8d68 | |||
c31415b484 | |||
4a596d62f2 | |||
3c1c0c3306 | |||
937c06cf32 | |||
d7fac2607b | |||
be672f6fde |
@ -5,15 +5,11 @@ language: python
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: TOXENV=lint
|
env: lint
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: TOXENV=py36-django20-wagtail20
|
env: TOXENV=py36-django20-wagtail20
|
||||||
- python: 3.6
|
|
||||||
env: TOXENV=py36-django20-wagtail20-geoip2
|
|
||||||
- python: 3.6
|
- python: 3.6
|
||||||
env: TOXENV=py36-django20-wagtail21
|
env: TOXENV=py36-django20-wagtail21
|
||||||
- python: 3.6
|
|
||||||
env: TOXENV=py36-django20-wagtail21-geoip2
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- pip install tox codecov
|
- pip install tox codecov
|
||||||
|
18
CHANGES
18
CHANGES
@ -1,21 +1,3 @@
|
|||||||
0.12.0
|
|
||||||
==================
|
|
||||||
- Merged forks of Torchbox and Praekelt
|
|
||||||
- Wagtail 2 compatibility
|
|
||||||
- Makefile adjustments for portability
|
|
||||||
- Adds simple segment forcing for superusers
|
|
||||||
- Fix excluding pages without variant
|
|
||||||
- Fix bug on visiting a segment page in the admin
|
|
||||||
- Use Wagtail's logic in the page count in the dash
|
|
||||||
- Prevent corrected summary item from counting the root page
|
|
||||||
- Delete variants of a page that is being deleted
|
|
||||||
- Add end user and developer documentation
|
|
||||||
- Add an option to show a personalised block to everyone
|
|
||||||
- Add origin country rule (#190)
|
|
||||||
- Return 404 if variant page is accessed directly (#188)
|
|
||||||
- Do not generate sitemap entries for variants (#187)
|
|
||||||
- Remove restrictive wagtail dependency version constraint (#192)
|
|
||||||
|
|
||||||
0.11.3
|
0.11.3
|
||||||
==================
|
==================
|
||||||
- Bugfix: Handle errors when testing an invalid visit count rule
|
- Bugfix: Handle errors when testing an invalid visit count rule
|
||||||
|
@ -131,47 +131,3 @@ Is logged in Whether the user is logged in or logged out.
|
|||||||
================== ==========================================================
|
================== ==========================================================
|
||||||
|
|
||||||
``wagtail_personalisation.rules.UserIsLoggedInRule``
|
``wagtail_personalisation.rules.UserIsLoggedInRule``
|
||||||
|
|
||||||
|
|
||||||
Origin country rule
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
The origin country rule allows you to match visitors based on the origin
|
|
||||||
country of their request. This rule requires to have set up a way to detect
|
|
||||||
countries beforehand.
|
|
||||||
|
|
||||||
================== ==========================================================
|
|
||||||
Option Description
|
|
||||||
================== ==========================================================
|
|
||||||
Country What country user's request comes from.
|
|
||||||
================== ==========================================================
|
|
||||||
|
|
||||||
You must have one of the following configurations set up in order to
|
|
||||||
make it work.
|
|
||||||
|
|
||||||
- Cloudflare IP Geolocation - ``cf-ipcountry`` HTTP header set with a value of
|
|
||||||
the alpha-2 country format.
|
|
||||||
- CloudFront Geo-Targeting - ``cloudfront-viewer-country`` header set with a
|
|
||||||
value of the alpha-2 country format.
|
|
||||||
- The last fallback is to use GeoIP2 module that is included with Django. This
|
|
||||||
requires setting up an IP database beforehand, see the Django's
|
|
||||||
`GeoIP2 instructions <https://docs.djangoproject.com/en/stable/ref/contrib/gis/geoip2/>`_
|
|
||||||
for more information. It will use IP of the request, using HTTP header
|
|
||||||
the ``x-forwarded-for`` HTTP header and ``REMOTE_ADDR`` server value as a
|
|
||||||
fallback. If you want to use a custom logic when obtaining IP address, please
|
|
||||||
set the ``WAGTAIL_PERSONALISATION_IP_FUNCTION`` setting to the function that takes a
|
|
||||||
request as an argument, e.g.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
# settings.py
|
|
||||||
|
|
||||||
WAGTAIL_PERSONALISATION_IP_FUNCTION = 'yourproject.utils.get_client_ip'
|
|
||||||
|
|
||||||
|
|
||||||
# yourproject/utils.py
|
|
||||||
|
|
||||||
def get_client_ip(request):
|
|
||||||
return request['HTTP_CF_CONNECTING_IP']
|
|
||||||
|
|
||||||
``wagtail_personalisation.rules.OriginCountryRule``
|
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
"enable_date": "2017-06-02T10:58:39.389Z",
|
"enable_date": "2017-06-02T10:58:39.389Z",
|
||||||
"disable_date": "2017-06-02T10:34:51.722Z",
|
"disable_date": "2017-06-02T10:34:51.722Z",
|
||||||
"visit_count": 0,
|
"visit_count": 0,
|
||||||
"status": "enabled",
|
"enabled": true,
|
||||||
"persistent": false,
|
"persistent": false,
|
||||||
"match_any": false
|
"match_any": false
|
||||||
}
|
}
|
||||||
@ -39,7 +39,7 @@
|
|||||||
"enable_date": "2017-06-02T10:57:44.497Z",
|
"enable_date": "2017-06-02T10:57:44.497Z",
|
||||||
"disable_date": "2017-06-02T10:57:39.984Z",
|
"disable_date": "2017-06-02T10:57:39.984Z",
|
||||||
"visit_count": 1,
|
"visit_count": 1,
|
||||||
"status": "enabled",
|
"enabled": true,
|
||||||
"persistent": false,
|
"persistent": false,
|
||||||
"match_any": false
|
"match_any": false
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ python_paths = .
|
|||||||
[flake8]
|
[flake8]
|
||||||
ignore = E731
|
ignore = E731
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
exclude =
|
exclude =
|
||||||
src/**/migrations/*.py
|
src/**/migrations/*.py
|
||||||
|
|
||||||
[wheel]
|
[wheel]
|
||||||
|
3
setup.py
3
setup.py
@ -2,10 +2,9 @@ import re
|
|||||||
from setuptools import find_packages, setup
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
install_requires = [
|
install_requires = [
|
||||||
'wagtail>=2.0',
|
'wagtail>=2.0,<2.2',
|
||||||
'user-agents>=1.1.0',
|
'user-agents>=1.1.0',
|
||||||
'wagtailfontawesome>=1.1.3',
|
'wagtailfontawesome>=1.1.3',
|
||||||
'pycountry',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
tests_require = [
|
tests_require = [
|
||||||
|
@ -8,7 +8,6 @@ from wagtail_personalisation.models import Segment
|
|||||||
|
|
||||||
|
|
||||||
def list_segment_choices():
|
def list_segment_choices():
|
||||||
yield -1, ("Show to everyone")
|
|
||||||
for pk, name in Segment.objects.values_list('pk', 'name'):
|
for pk, name in Segment.objects.values_list('pk', 'name'):
|
||||||
yield pk, name
|
yield pk, name
|
||||||
|
|
||||||
@ -36,19 +35,10 @@ class PersonalisedStructBlock(blocks.StructBlock):
|
|||||||
adapter = get_segment_adapter(request)
|
adapter = get_segment_adapter(request)
|
||||||
user_segments = adapter.get_segments()
|
user_segments = adapter.get_segments()
|
||||||
|
|
||||||
try:
|
if value['segment']:
|
||||||
segment_id = int(value['segment'])
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return ''
|
|
||||||
|
|
||||||
if segment_id > 0:
|
|
||||||
for segment in user_segments:
|
for segment in user_segments:
|
||||||
if segment.id == segment_id:
|
if segment.id == int(value['segment']):
|
||||||
return super(PersonalisedStructBlock, self).render(
|
return super(PersonalisedStructBlock, self).render(
|
||||||
value, context)
|
value, context)
|
||||||
|
|
||||||
if segment_id == -1:
|
return ""
|
||||||
return super(PersonalisedStructBlock, self).render(
|
|
||||||
value, context)
|
|
||||||
|
|
||||||
return ''
|
|
||||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,45 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.2 on 2017-08-10 13:48
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
Segment = apps.get_model('wagtail_personalisation', 'Segment')
|
||||||
|
|
||||||
|
for segment in Segment.objects.all():
|
||||||
|
segment.enabled = segment.status == 'enabled'
|
||||||
|
segment.save()
|
||||||
|
|
||||||
|
|
||||||
|
def backward(apps, schema_editor):
|
||||||
|
Segment = apps.get_model('wagtail_personalisation', 'Segment')
|
||||||
|
|
||||||
|
for segment in Segment.objects.all():
|
||||||
|
if segment.enabled:
|
||||||
|
segment.status = 'enabled'
|
||||||
|
else:
|
||||||
|
segment.status = 'disabled'
|
||||||
|
|
||||||
|
segment.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wagtail_personalisation', '0023_personalisablepagemetadata_variant_cascade'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='segment',
|
||||||
|
name='enabled',
|
||||||
|
field=models.BooleanField(default=True, help_text='Should the segment be active?'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forward, reverse_code=backward),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='segment',
|
||||||
|
name='status',
|
||||||
|
),
|
||||||
|
]
|
@ -10,7 +10,6 @@ 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
|
||||||
@ -23,20 +22,12 @@ from .forms import SegmentAdminForm
|
|||||||
|
|
||||||
class SegmentQuerySet(models.QuerySet):
|
class SegmentQuerySet(models.QuerySet):
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
return self.filter(status=self.model.STATUS_ENABLED)
|
return self.filter(enabled=True)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class Segment(ClusterableModel):
|
class Segment(ClusterableModel):
|
||||||
"""The segment model."""
|
"""The segment model."""
|
||||||
STATUS_ENABLED = 'enabled'
|
|
||||||
STATUS_DISABLED = 'disabled'
|
|
||||||
|
|
||||||
STATUS_CHOICES = (
|
|
||||||
(STATUS_ENABLED, _('Enabled')),
|
|
||||||
(STATUS_DISABLED, _('Disabled')),
|
|
||||||
)
|
|
||||||
|
|
||||||
TYPE_DYNAMIC = 'dynamic'
|
TYPE_DYNAMIC = 'dynamic'
|
||||||
TYPE_STATIC = 'static'
|
TYPE_STATIC = 'static'
|
||||||
|
|
||||||
@ -51,8 +42,8 @@ class Segment(ClusterableModel):
|
|||||||
enable_date = models.DateTimeField(null=True, editable=False)
|
enable_date = models.DateTimeField(null=True, editable=False)
|
||||||
disable_date = models.DateTimeField(null=True, editable=False)
|
disable_date = models.DateTimeField(null=True, editable=False)
|
||||||
visit_count = models.PositiveIntegerField(default=0, editable=False)
|
visit_count = models.PositiveIntegerField(default=0, editable=False)
|
||||||
status = models.CharField(
|
enabled = models.BooleanField(
|
||||||
max_length=20, choices=STATUS_CHOICES, default=STATUS_ENABLED)
|
default=True, help_text=_("Should the segment be active?"))
|
||||||
persistent = models.BooleanField(
|
persistent = models.BooleanField(
|
||||||
default=False, help_text=_("Should the segment persist between visits?"))
|
default=False, help_text=_("Should the segment persist between visits?"))
|
||||||
match_any = models.BooleanField(
|
match_any = models.BooleanField(
|
||||||
@ -112,7 +103,7 @@ class Segment(ClusterableModel):
|
|||||||
MultiFieldPanel([
|
MultiFieldPanel([
|
||||||
FieldPanel('name', classname="title"),
|
FieldPanel('name', classname="title"),
|
||||||
FieldRowPanel([
|
FieldRowPanel([
|
||||||
FieldPanel('status'),
|
FieldPanel('enabled'),
|
||||||
FieldPanel('persistent'),
|
FieldPanel('persistent'),
|
||||||
]),
|
]),
|
||||||
FieldPanel('match_any'),
|
FieldPanel('match_any'),
|
||||||
@ -179,9 +170,7 @@ class Segment(ClusterableModel):
|
|||||||
return segment_rules
|
return segment_rules
|
||||||
|
|
||||||
def toggle(self, save=True):
|
def toggle(self, save=True):
|
||||||
self.status = (
|
self.enabled = not self.enabled
|
||||||
self.STATUS_ENABLED if self.status == self.STATUS_DISABLED
|
|
||||||
else self.STATUS_DISABLED)
|
|
||||||
if save:
|
if save:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -303,15 +292,3 @@ class PersonalisablePageMixin:
|
|||||||
metadata = PersonalisablePageMetadata.objects.create(
|
metadata = PersonalisablePageMetadata.objects.create(
|
||||||
canonical_page=self, variant=self)
|
canonical_page=self, variant=self)
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
def get_sitemap_urls(self, request=None):
|
|
||||||
# Do not generate sitemap entries for variants.
|
|
||||||
if not self.personalisation_metadata.is_canonical:
|
|
||||||
return []
|
|
||||||
if wagtail.VERSION >= (2, 2):
|
|
||||||
# Since Wagtail 2.2 you can pass request to the get_sitemap_urls
|
|
||||||
# method.
|
|
||||||
return super(PersonalisablePageMixin, self).get_sitemap_urls(
|
|
||||||
request=request
|
|
||||||
)
|
|
||||||
return super(PersonalisablePageMixin, self).get_sitemap_urls()
|
|
||||||
|
@ -7,16 +7,16 @@ from wagtail_personalisation.models import Segment
|
|||||||
def check_status_change(sender, instance, *args, **kwargs):
|
def check_status_change(sender, instance, *args, **kwargs):
|
||||||
"""Check if the status has changed. Alter dates accordingly."""
|
"""Check if the status has changed. Alter dates accordingly."""
|
||||||
try:
|
try:
|
||||||
original_status = sender.objects.get(pk=instance.id).status
|
original_status = sender.objects.get(pk=instance.id).enabled
|
||||||
except sender.DoesNotExist:
|
except sender.DoesNotExist:
|
||||||
original_status = ""
|
original_status = None
|
||||||
|
|
||||||
if original_status != instance.status:
|
if original_status != instance.enabled:
|
||||||
if instance.status == instance.STATUS_ENABLED:
|
if instance.enabled is True:
|
||||||
instance.enable_date = timezone.now()
|
instance.enable_date = timezone.now()
|
||||||
instance.visit_count = 0
|
instance.visit_count = 0
|
||||||
return instance
|
return instance
|
||||||
if instance.status == instance.STATUS_DISABLED:
|
if instance.enabled is False:
|
||||||
instance.disable_date = timezone.now()
|
instance.disable_date = timezone.now()
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
import logging
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
import pycountry
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.sessions.models import Session
|
from django.contrib.sessions.models import Session
|
||||||
@ -20,28 +18,8 @@ from user_agents import parse
|
|||||||
from wagtail.admin.edit_handlers import (
|
from wagtail.admin.edit_handlers import (
|
||||||
FieldPanel, FieldRowPanel, PageChooserPanel)
|
FieldPanel, FieldRowPanel, PageChooserPanel)
|
||||||
|
|
||||||
from wagtail_personalisation.utils import get_client_ip
|
|
||||||
|
|
||||||
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_geoip_module():
|
|
||||||
try:
|
|
||||||
from django.contrib.gis.geoip2 import GeoIP2
|
|
||||||
return GeoIP2
|
|
||||||
except ImportError:
|
|
||||||
logger.exception(
|
|
||||||
'GeoIP module is disabled. To use GeoIP for the origin\n'
|
|
||||||
'country personaliastion rule please set it up as per '
|
|
||||||
'documentation:\n'
|
|
||||||
'https://docs.djangoproject.com/en/stable/ref/contrib/gis/'
|
|
||||||
'geoip2/.\n'
|
|
||||||
'Wagtail-personalisation also works with Cloudflare and\n'
|
|
||||||
'CloudFront country detection, so you should not see this\n'
|
|
||||||
'warning if you use one of those.')
|
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class AbstractBaseRule(models.Model):
|
class AbstractBaseRule(models.Model):
|
||||||
@ -430,65 +408,3 @@ class UserIsLoggedInRule(AbstractBaseRule):
|
|||||||
'title': _('These visitors are'),
|
'title': _('These visitors are'),
|
||||||
'value': _('Logged in') if self.is_logged_in else _('Not logged in'),
|
'value': _('Logged in') if self.is_logged_in else _('Not logged in'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
COUNTRY_CHOICES = [(country.alpha_2.lower(), country.name)
|
|
||||||
for country in pycountry.countries]
|
|
||||||
|
|
||||||
|
|
||||||
class OriginCountryRule(AbstractBaseRule):
|
|
||||||
"""
|
|
||||||
Test user against the country or origin of their request.
|
|
||||||
|
|
||||||
Using this rule requires setting up GeoIP2 on Django or using
|
|
||||||
CloudFlare or CloudFront geolocation detection.
|
|
||||||
"""
|
|
||||||
country = models.CharField(
|
|
||||||
max_length=2, choices=COUNTRY_CHOICES,
|
|
||||||
help_text=_("Select origin country of the request that this rule will "
|
|
||||||
"match against. This rule will only work if you use "
|
|
||||||
"Cloudflare or CloudFront IP geolocation or if GeoIP2 "
|
|
||||||
"module is configured.")
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = _("origin country rule")
|
|
||||||
|
|
||||||
def get_cloudflare_country(self, request):
|
|
||||||
"""
|
|
||||||
Get country code that has been detected by Cloudflare.
|
|
||||||
|
|
||||||
Guide to the functionality:
|
|
||||||
https://support.cloudflare.com/hc/en-us/articles/200168236-What-does-Cloudflare-IP-Geolocation-do-
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return request.META['HTTP_CF_IPCOUNTRY'].lower()
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_cloudfront_country(self, request):
|
|
||||||
try:
|
|
||||||
return request.META['HTTP_CLOUDFRONT_VIEWER_COUNTRY'].lower()
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_geoip_country(self, request):
|
|
||||||
GeoIP2 = get_geoip_module()
|
|
||||||
if GeoIP2 is None:
|
|
||||||
return False
|
|
||||||
return GeoIP2().country_code(get_client_ip(request)).lower()
|
|
||||||
|
|
||||||
def get_country(self, request):
|
|
||||||
# Prioritise CloudFlare and CloudFront country detection over GeoIP.
|
|
||||||
functions = (
|
|
||||||
self.get_cloudflare_country,
|
|
||||||
self.get_cloudfront_country,
|
|
||||||
self.get_geoip_country,
|
|
||||||
)
|
|
||||||
for function in functions:
|
|
||||||
result = function(request)
|
|
||||||
if result:
|
|
||||||
return result
|
|
||||||
|
|
||||||
def test_user(self, request=None):
|
|
||||||
return (self.get_country(request) or '') == self.country.lower()
|
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
<div class="nice-padding block_container">
|
<div class="nice-padding block_container">
|
||||||
{% if all_count %}
|
{% if all_count %}
|
||||||
{% for segment in object_list %}
|
{% for segment in object_list %}
|
||||||
<div class="block block--{{ segment.status }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
|
<div class="block block--{{ segment.enabled|yesno:"enabled,disabled" }}" onclick="location.href = '{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}'">
|
||||||
<h2>{{ segment }}</h2>
|
<h2>{{ segment }}</h2>
|
||||||
|
|
||||||
<div class="inspect_container">
|
<div class="inspect_container">
|
||||||
@ -98,10 +98,10 @@
|
|||||||
|
|
||||||
{% if user_can_create %}
|
{% if user_can_create %}
|
||||||
<ul class="block_actions">
|
<ul class="block_actions">
|
||||||
{% if segment.status == segment.STATUS_DISABLED %}
|
{% if segment.enabled %}
|
||||||
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
|
|
||||||
{% elif segment.status == segment.STATUS_ENABLED %}
|
|
||||||
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Disable this segment" %}">disable</a></li>
|
||||||
|
{% else %}
|
||||||
|
<li><a href="{% url 'segment:toggle' segment.pk %}" title="{% trans "Enable this segment" %}">enable</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><a href="{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
<li><a href="{% url 'wagtail_personalisation_segment_modeladmin_edit' segment.pk %}" title="{% trans "Configure this segment" %}">configure this</a></li>
|
||||||
{% if segment.is_static %}
|
{% if segment.is_static %}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.template.base import FilterExpression, kwarg_re
|
from django.template.base import FilterExpression, kwarg_re
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.module_loading import import_string
|
|
||||||
|
|
||||||
|
|
||||||
def impersonate_other_page(page, other_page):
|
def impersonate_other_page(page, other_page):
|
||||||
@ -118,17 +116,3 @@ def can_delete_pages(pages, user):
|
|||||||
if not variant.permissions_for_user(user).can_delete():
|
if not variant.permissions_for_user(user).can_delete():
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_client_ip(request):
|
|
||||||
try:
|
|
||||||
func = import_string(settings.WAGTAIL_PERSONALISATION_IP_FUNCTION)
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return func(request)
|
|
||||||
try:
|
|
||||||
x_forwarded_for = request.META['HTTP_X_FORWARDED_FOR']
|
|
||||||
return x_forwarded_for.split(',')[-1].strip()
|
|
||||||
except KeyError:
|
|
||||||
return request.META['REMOTE_ADDR']
|
|
||||||
|
@ -83,7 +83,7 @@ class SegmentModelAdmin(ModelAdmin):
|
|||||||
delete_view_class = SegmentModelDeleteView
|
delete_view_class = SegmentModelDeleteView
|
||||||
menu_icon = 'fa-snowflake-o'
|
menu_icon = 'fa-snowflake-o'
|
||||||
add_to_settings_menu = False
|
add_to_settings_menu = False
|
||||||
list_display = ('name', 'persistent', 'match_any', 'status',
|
list_display = ('name', 'persistent', 'match_any', 'enabled',
|
||||||
'page_count', 'variant_count', 'statistics')
|
'page_count', 'variant_count', 'statistics')
|
||||||
index_view_extra_js = ['js/commons.js', 'js/index.js']
|
index_view_extra_js = ['js/commons.js', 'js/index.js']
|
||||||
index_view_extra_css = ['css/index.css']
|
index_view_extra_css = ['css/index.css']
|
||||||
|
@ -4,7 +4,6 @@ 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.http import Http404
|
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template.defaultfilters import pluralize
|
from django.template.defaultfilters import pluralize
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
@ -105,13 +104,9 @@ def serve_variant(page, request, serve_args, serve_kwargs):
|
|||||||
adapter = get_segment_adapter(request)
|
adapter = get_segment_adapter(request)
|
||||||
user_segments = adapter.get_segments()
|
user_segments = adapter.get_segments()
|
||||||
|
|
||||||
metadata = page.personalisation_metadata
|
|
||||||
|
|
||||||
# If page is not canonical, don't serve it.
|
|
||||||
if not metadata.is_canonical:
|
|
||||||
raise Http404
|
|
||||||
|
|
||||||
if user_segments:
|
if user_segments:
|
||||||
|
metadata = page.personalisation_metadata
|
||||||
|
|
||||||
# TODO: This is never more then one page? (fix query count)
|
# TODO: This is never more then one page? (fix query count)
|
||||||
metadata = metadata.metadata_for_segments(user_segments)
|
metadata = metadata.metadata_for_segments(user_segments)
|
||||||
if metadata:
|
if metadata:
|
||||||
|
@ -46,8 +46,3 @@ class VisitCountRuleFactory(factory.DjangoModelFactory):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = rules.VisitCountRule
|
model = rules.VisitCountRule
|
||||||
|
|
||||||
|
|
||||||
class OriginCountryRuleFactory(factory.DjangoModelFactory):
|
|
||||||
class Meta:
|
|
||||||
model = rules.OriginCountryRule
|
|
||||||
|
@ -7,7 +7,7 @@ from wagtail_personalisation import models
|
|||||||
|
|
||||||
class SegmentFactory(factory.DjangoModelFactory):
|
class SegmentFactory(factory.DjangoModelFactory):
|
||||||
name = 'TestSegment'
|
name = 'TestSegment'
|
||||||
status = models.Segment.STATUS_ENABLED
|
enabled = True
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Segment
|
model = models.Segment
|
||||||
|
@ -64,7 +64,7 @@ def test_refresh_removes_disabled(rf):
|
|||||||
adapter.set_segments([segment_1, segment_2])
|
adapter.set_segments([segment_1, segment_2])
|
||||||
|
|
||||||
adapter = adapters.SessionSegmentsAdapter(request)
|
adapter = adapters.SessionSegmentsAdapter(request)
|
||||||
segment_1.status = segment_1.STATUS_DISABLED
|
segment_1.enabled = False
|
||||||
segment_1.save()
|
segment_1.save()
|
||||||
adapter.refresh()
|
adapter.refresh()
|
||||||
|
|
||||||
|
@ -15,14 +15,14 @@ from wagtail_personalisation.rules import TimeRule
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_segment_create():
|
def test_segment_create():
|
||||||
factoried_segment = SegmentFactory()
|
factoried_segment = SegmentFactory()
|
||||||
segment = Segment(name='TestSegment', status='enabled')
|
segment = Segment(name='TestSegment', enabled=True)
|
||||||
TimeRule(
|
TimeRule(
|
||||||
start_time=datetime.time(8, 0, 0),
|
start_time=datetime.time(8, 0, 0),
|
||||||
end_time=datetime.time(23, 0, 0),
|
end_time=datetime.time(23, 0, 0),
|
||||||
segment=segment)
|
segment=segment)
|
||||||
|
|
||||||
assert factoried_segment.name == segment.name
|
assert factoried_segment.name == segment.name
|
||||||
assert factoried_segment.status == segment.status
|
assert factoried_segment.enabled == segment.enabled
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -60,16 +60,3 @@ def test_page_protection_when_deleting_segment(segmented_page):
|
|||||||
assert len(segment.get_used_pages())
|
assert len(segment.get_used_pages())
|
||||||
with pytest.raises(ProtectedError):
|
with pytest.raises(ProtectedError):
|
||||||
segment.delete()
|
segment.delete()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_sitemap_generation_for_canonical_pages_is_enabled(segmented_page):
|
|
||||||
canonical = segmented_page.personalisation_metadata.canonical_page
|
|
||||||
assert canonical.personalisation_metadata.is_canonical
|
|
||||||
assert canonical.get_sitemap_urls()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_sitemap_generation_for_variants_is_disabled(segmented_page):
|
|
||||||
assert not segmented_page.personalisation_metadata.is_canonical
|
|
||||||
assert not segmented_page.get_sitemap_urls()
|
|
||||||
|
@ -1,203 +0,0 @@
|
|||||||
from importlib.util import find_spec
|
|
||||||
from unittest.mock import call, MagicMock, 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
|
|
@ -12,7 +12,7 @@ from wagtail_personalisation.rules import TimeRule, VisitCountRule
|
|||||||
|
|
||||||
|
|
||||||
def form_with_data(segment, *rules):
|
def form_with_data(segment, *rules):
|
||||||
model_fields = ['type', 'status', 'count', 'name', 'match_any', 'randomisation_percent']
|
model_fields = ['type', 'enabled', 'count', 'name', 'match_any', 'randomisation_percent']
|
||||||
|
|
||||||
class TestSegmentAdminForm(SegmentAdminForm):
|
class TestSegmentAdminForm(SegmentAdminForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.test import override_settings
|
|
||||||
|
|
||||||
from tests.factories.page import ContentPageFactory
|
from tests.factories.page import ContentPageFactory
|
||||||
from wagtail_personalisation.utils import (
|
from wagtail_personalisation.utils import (
|
||||||
can_delete_pages, get_client_ip, impersonate_other_page)
|
can_delete_pages, impersonate_other_page)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -38,29 +36,3 @@ def test_can_delete_pages_with_superuser(rf, user, segmented_page):
|
|||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_cannot_delete_pages_with_standard_user(user, segmented_page):
|
def test_cannot_delete_pages_with_standard_user(user, segmented_page):
|
||||||
assert not can_delete_pages([segmented_page], user)
|
assert not can_delete_pages([segmented_page], user)
|
||||||
|
|
||||||
|
|
||||||
def test_get_client_ip_with_remote_addr(rf):
|
|
||||||
request = rf.get('/', REMOTE_ADDR='173.231.235.87')
|
|
||||||
assert get_client_ip(request) == '173.231.235.87'
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_client_ip_with_x_forwarded_for(rf):
|
|
||||||
request = rf.get('/', HTTP_X_FORWARDED_FOR='173.231.235.87',
|
|
||||||
REMOTE_ADDR='10.0.23.24')
|
|
||||||
assert get_client_ip(request) == '173.231.235.87'
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
|
||||||
WAGTAIL_PERSONALISATION_IP_FUNCTION='some.non.existent.path'
|
|
||||||
)
|
|
||||||
def test_get_client_ip_custom_get_client_ip_function_does_not_exist(rf):
|
|
||||||
with pytest.raises(ImportError):
|
|
||||||
get_client_ip(rf.get('/'))
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
|
||||||
WAGTAIL_PERSONALISATION_IP_FUNCTION='tests.utils.get_custom_ip'
|
|
||||||
)
|
|
||||||
def test_get_client_ip_custom_get_client_ip_used(rf):
|
|
||||||
assert get_client_ip(rf.get('/')) == '123.123.123.123'
|
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from django.http import Http404
|
|
||||||
|
|
||||||
from wagtail.core.models import Page
|
from wagtail.core.models import Page
|
||||||
|
|
||||||
from tests.factories.segment import SegmentFactory
|
from tests.factories.segment import SegmentFactory
|
||||||
@ -19,15 +16,6 @@ def test_serve_variant_no_variant(site, rf):
|
|||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
def test_variant_accessed_directly_returns_404(segmented_page, rf):
|
|
||||||
request = rf.get('/')
|
|
||||||
args = tuple()
|
|
||||||
kwargs = {}
|
|
||||||
with pytest.raises(Http404):
|
|
||||||
wagtail_hooks.serve_variant(segmented_page, request, args, kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_serve_variant_with_variant_no_segment(site, rf, segmented_page):
|
def test_serve_variant_with_variant_no_segment(site, rf, segmented_page):
|
||||||
request = rf.get('/')
|
request = rf.get('/')
|
||||||
|
@ -5,7 +5,3 @@ def render_template(value, **context):
|
|||||||
template = engines['django'].from_string(value)
|
template = engines['django'].from_string(value)
|
||||||
request = context.pop('request', None)
|
request = context.pop('request', None)
|
||||||
return template.render(context, request)
|
return template.render(context, request)
|
||||||
|
|
||||||
|
|
||||||
def get_custom_ip(request):
|
|
||||||
return '123.123.123.123'
|
|
||||||
|
5
tox.ini
5
tox.ini
@ -1,15 +1,14 @@
|
|||||||
[tox]
|
[tox]
|
||||||
envlist = py{36}-django{20}-wagtail{20,21}{,-geoip2},lint
|
envlist = py{36}-django{20}-wagtail{20,21},lint
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
basepython = python3.6
|
basepython = python3.6
|
||||||
commands = coverage run --parallel -m pytest -rs {posargs}
|
commands = coverage run --parallel -m pytest {posargs}
|
||||||
extras = test
|
extras = test
|
||||||
deps =
|
deps =
|
||||||
django20: django>=2.0,<2.1
|
django20: django>=2.0,<2.1
|
||||||
wagtail20: wagtail>=2.0,<2.1
|
wagtail20: wagtail>=2.0,<2.1
|
||||||
wagtail21: wagtail>=2.1,<2.2
|
wagtail21: wagtail>=2.1,<2.2
|
||||||
geoip2: geoip2
|
|
||||||
|
|
||||||
[testenv:coverage-report]
|
[testenv:coverage-report]
|
||||||
basepython = python3.6
|
basepython = python3.6
|
||||||
|
Reference in New Issue
Block a user