Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
4111947
Support wagtail 5 and django 4.2
TopDevPros Jul 22, 2023
7135b36
Revert heading to 'Installation'
gasman Aug 9, 2023
f3a3ca7
Remove / fix docstrings
gasman Aug 9, 2023
16e63c5
Remove unnecessary fallbacks
gasman Aug 9, 2023
488fbdc
revert comment as per https://github.com/torchbox/wagtail-experiments…
gasman Aug 9, 2023
2072633
Downcase verbose_names
gasman Aug 9, 2023
eb842b3
set version number to 0.3 in setup.py
gasman Aug 9, 2023
436418c
revert change to fixture
gasman Aug 9, 2023
43e4c0a
Remove unwanted copy of simple_page template
gasman Aug 9, 2023
2550c21
Remove unnecessary fallback caused by incorrectly capitalised verbose…
gasman Aug 9, 2023
78712d7
Revert changes to impersonate_other_page that stop it from replacing …
gasman Aug 9, 2023
0abffa9
Add Github Actions config
gasman Aug 10, 2023
8b4aef9
run tests via runtests.py
gasman Aug 10, 2023
c86ce81
Remove travis CI config
gasman Aug 10, 2023
87d5f67
set 3.8 as minimum python version
gasman Aug 10, 2023
34dcea3
Add nightly build test scripts
gasman Aug 10, 2023
ff40d73
deliberate test failure to test nightly build reporting
gasman Aug 10, 2023
a2ddfb3
Revert "deliberate test failure to test nightly build reporting"
gasman Aug 10, 2023
4a2ea36
Add build to gitignore
gasman Aug 10, 2023
6a20507
Fill in release date for 0.3
gasman Aug 10, 2023
ad77595
Use external modeladmin package where available
gasman Nov 6, 2023
0b688a2
Test against Wagtail 5.2 and Python 3.12
gasman Nov 6, 2023
2dc1427
Remove tox.ini as it is clearly unused and unmaintained
gasman Nov 6, 2023
52da076
Update Github Actions to support installing wagtail-modeladmin package
gasman Nov 6, 2023
e5700ca
Version bump to 0.3.1
gasman Nov 6, 2023
e09803d
Install wagtail-modeladmin for nightly tests
gasman Nov 7, 2023
08ae8a8
allow passing parameters to runtests.py
gasman Feb 12, 2024
b323b63
Add meaningful error message if wagtail-modeladmin is not installed o…
gasman Feb 12, 2024
e1be43c
Revise tests to show failures on Wagtail 6
gasman Feb 12, 2024
d0108a4
Reinstate ordering on Alternative and add migration for downcasing ve…
gasman Feb 12, 2024
88719fa
Rework alternatives model to store explicit revision IDs
gasman Feb 12, 2024
182bac5
Test against Wagtail 6.0, Django 5.0
gasman Feb 12, 2024
73c600b
Drop support for Wagtail <5.2 and Django <4.2
gasman Feb 13, 2024
30f3684
Version bump to 0.4
gasman Feb 13, 2024
0c2904c
Allow for standalone URL to be use as goals
dawnwages Apr 21, 2020
039121e
goal_url issue
dawnwages Apr 21, 2020
0a303d3
II-210: URLS included in experiment
dawnwages Apr 22, 2020
261d59c
#210: fix migrations to match other wagtail-experiments
dawnwages Apr 22, 2020
782983c
#210: path and url
dawnwages Apr 23, 2020
6b85dff
#568: record_participant and record_completion print to console
dawnwages Apr 24, 2020
335590a
Testing pdb statements
dawnwages Apr 24, 2020
aed799a
#210: clean up debugging
dawnwages Apr 29, 2020
4fe7ac6
#210: Path and query
dawnwages Apr 23, 2020
9e5cf64
#210: Path and query
dawnwages Apr 23, 2020
f585b2a
fix path
dawnwages Apr 29, 2020
cc2fcd5
fix
dawnwages Apr 29, 2020
3f02464
update middleware code
DelGall Apr 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/report_nightly_build_failure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@

"""
Called by GitHub Action when the nightly build fails.
This reports an error to the #nightly-build-failures Slack channel.
"""
import os
import requests

if "SLACK_WEBHOOK_URL" in os.environ:
# https://docs.github.com/en/free-pro-team@latest/actions/reference/environment-variables#default-environment-variables
repository = os.environ["GITHUB_REPOSITORY"]
run_id = os.environ["GITHUB_RUN_ID"]
url = f"https://github.com/{repository}/actions/runs/{run_id}"

print("Reporting to #nightly-build-failures slack channel")
response = requests.post(
os.environ["SLACK_WEBHOOK_URL"],
json={
"text": f"A Nightly build failed. See {url}",
},
)

print(f"Slack responded with: {response}")

else:
print(
"Unable to report to #nightly-build-failures slack channel because SLACK_WEBHOOK_URL is not set"
)
36 changes: 36 additions & 0 deletions .github/workflows/nightly-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Nightly Wagtail test

on:
schedule:
- cron: "20 1 * * *"

workflow_dispatch:

jobs:
nightly-test:
# Cannot check the existence of secrets, so limiting to repository name to prevent all forks to run nightly.
# See: https://github.com/actions/runner/issues/520
if: ${{ github.repository == 'torchbox/wagtail-experiments' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.12
uses: actions/setup-python@v2
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "git+https://github.com/wagtail/wagtail.git@main#egg=wagtail"
pip install wagtail-modeladmin
pip install -e .[testing]
- name: Test
id: test
continue-on-error: true
run: ./runtests.py
- name: Send Slack notification on failure
if: steps.test.outcome == 'failure'
run: |
python .github/report_nightly_build_failure.py
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
71 changes: 71 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@

name: CI

on:
push:
branches: [ main ]
pull_request:

# Current configuration:
# - django 4.2, python 3.8, wagtail 5.2, sqlite
# - django 5.0, python 3.10, wagtail 6.0, postgres
# - django 5.0, python 3.12, wagtail main, sqlite (allow failures)
jobs:
test:
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
matrix:
include:
- python: "3.8"
django: "Django>=4.2,<4.3"
wagtail: "wagtail>=5.2,<5.3"
database: "sqlite3"
modeladmin: true
experimental: false
- python: "3.10"
django: "Django>=5.0,<5.1"
wagtail: "wagtail>=6.0,<6.1"
database: "postgresql"
modeladmin: true
experimental: false

- python: "3.12"
django: "Django>=5.0,<5.1"
wagtail: "git+https://github.com/wagtail/wagtail.git@main#egg=wagtail"
database: "sqlite3"
modeladmin: true
experimental: true

services:
postgres:
image: postgres:14
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install "psycopg2>=2.9.3"
pip install "${{ matrix.django }}"
pip install "${{ matrix.wagtail }}"
pip install -e .[testing]
- name: Install wagtail-modeladmin
if: ${{ matrix.modeladmin }}
run: pip install wagtail-modeladmin
- name: Test
run: ./runtests.py
env:
DATABASE_ENGINE: django.db.backends.${{ matrix.database }}
DATABASE_HOST: localhost
DATABASE_USER: postgres
DATABASE_PASS: postgres
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.pyc
/.tox/
/dist/
/build/
/wagtail_experiments.egg-info/
39 changes: 0 additions & 39 deletions .travis.yml

This file was deleted.

25 changes: 25 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
Changelog
=========

0.4 (2024-02-13)
~~~~~~~~~~~~~~~~

* Added support for Wagtail 6.0 (prevent draft edits made after an experiment goes live from showing on the front-end)
* Added support for Django 5.0
* Dropped support for Wagtail <5.2 and Django <4.2
* Fix: Prevent potential incorrect ordering of alternatives on PostgreSQL
* Fix: Add missing migration for changes to meta options


0.3.1 (2023-11-06)
~~~~~~~~~~~~~~~~~~

* Use external wagtail-modeladmin package where available
* Added support for Wagtail 5.1 - 5.2 and provisional support for Wagtail 6.0


0.3 (2023-08-10)
~~~~~~~~~~~~~~~~

* Added support for Wagtail 4.1 thru 5.0
* Added support for Django 3.2 thru 4.2
* Added docstrings to all 'experiment' functions and classes.
* Support internationalization for models

0.2 (28.11.2018)
~~~~~~~~~~~~~~~~

Expand Down
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
include LICENSE
include README.rst
include CHANGELOG.txt
recursive-include experiments/static *
recursive-include experiments/templates *
15 changes: 7 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
Wagtail Experiments
===================

.. image:: https://api.travis-ci.org/torchbox/wagtail-experiments.svg?branch=master
:target: https://travis-ci.org/torchbox/wagtail-experiments

**A/B testing for Wagtail**

This module supports the creation of A/B testing experiments within a Wagtail site. Several alternative versions of a page are set up, and on visiting a designated control page, a user is presented with one of those variations, selected at random (using a simplified version of the `PlanOut algorithm <https://facebook.github.io/planout/>`_). The number of visitors receiving each variation is logged, along with the number that subsequently go on to complete the experiment by visiting a designated goal page.
Expand All @@ -14,22 +11,24 @@ This module supports the creation of A/B testing experiments within a Wagtail si
Installation
------------

wagtail-experiments is compatible with Wagtail 1.7 to 2.3, and Django 1.8 to 2.1. To install::
wagtail-experiments is compatible with Wagtail 5.2 to 6.0, and Django 4.2 to 5.0. It depends on the Wagtail ModelAdmin module, which is available as an external package as of Wagtail 5.0; we recommend using this rather than the bundled `wagtail.contrib.modeladmin` module to avoid deprecation warnings. The external package is required as of Wagtail 6.0.

To install::

pip install wagtail-experiments
pip install wagtail-experiments wagtail-modeladmin

and ensure that the apps ``wagtail.contrib.modeladmin`` and ``experiments`` are included in your project's ``INSTALLED_APPS``:
and ensure that the apps ``wagtail_modeladmin`` and ``experiments`` are included in your project's ``INSTALLED_APPS``:

.. code-block:: python

INSTALLED_APPS = [
# ...
'wagtail.contrib.modeladmin',
'wagtail_modeladmin',
'experiments',
# ...
]

Then migrate::
Then migrate::

./manage.py migrate

Expand Down
9 changes: 9 additions & 0 deletions createmigrations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python

import sys
import os

from django.core.management import execute_from_command_line

os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.settings'
execute_from_command_line([sys.argv[0], 'makemigrations'])
10 changes: 4 additions & 6 deletions experiments/admin_urls.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from __future__ import absolute_import, unicode_literals

from django.conf.urls import url
from django.urls import re_path
from experiments import views

app_name = 'experiments'

urlpatterns = [
url(r'^experiment/report/(\d+)/$', views.experiment_report, name='report'),
url(r'^experiment/select_winner/(\d+)/(\d+)/$', views.select_winner, name='select_winner'),
url(r'^experiment/report/preview/(\d+)/(\d+)/$', views.preview_for_report, name='preview_for_report'),
re_path(r'^experiment/report/(\d+)/$', views.experiment_report, name='report'),
re_path(r'^experiment/select_winner/(\d+)/(\d+)/$', views.select_winner, name='select_winner'),
re_path(r'^experiment/report/preview/(\d+)/(\d+)/$', views.preview_for_report, name='preview_for_report'),
]
44 changes: 39 additions & 5 deletions experiments/backends/db.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
from __future__ import absolute_import, unicode_literals

import datetime

from django.db.models import F, Sum

from experiments.models import ExperimentHistory


#TODO: potentially the same session? clearing cookies
def record_participant(experiment, user_id, variation, request):
'''
If the user hasn't already participated in this experiment,
then save the variation the user viewed.

Args:
experiment: instance of experiments.models.Experiment
user_id: the id for this user.
variation: variation user viewed.
request: django HttpRequest.

Return:
Nothing
'''

# abort if this user has participated already
experiments_started = request.session.get('experiments_started', [])
if experiment.id in experiments_started:
Expand All @@ -23,8 +34,21 @@ def record_participant(experiment, user_id, variation, request):
# increment the participant_count
ExperimentHistory.objects.filter(pk=history.pk).update(participant_count=F('participant_count') + 1)


def record_completion(experiment, user_id, variation, request):
'''
If the user has started this experiment, but not completed it yet,
then mark the user's participation as completed.

Args:
experiment: instance of experiments.models.Experiment
user_id: the id for this user.
variation: variation user viewed.
request: django HttpRequest.

Return:
Nothing
'''

# abort if this user never started the experiment
if experiment.id not in request.session.get('experiments_started', []):
return
Expand All @@ -46,6 +70,16 @@ def record_completion(experiment, user_id, variation, request):


def get_report(experiment):
'''
Generate a report about the experiment's results.

Args:
experiment: instance of experiments.models.Experiment

Return:
A report of experiment results as a dictionary.
'''

result = {}
result.setdefault('variations', [])

Expand Down
22 changes: 22 additions & 0 deletions experiments/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from .models import Experiment
from .utils import get_user_id


class GoalURLMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
current_url = request.path
experiments = Experiment.objects.filter(
goal_url__contains=current_url,
status='live'
)
if experiments.exists():
# let's complete all experiment that match this URL
user_id = get_user_id(request)
for exp in experiments:
exp.record_completion_for_user(user_id, request)

response = self.get_response(request)
return response
Loading