Skip to content

Commit 04f1b8d

Browse files
author
John Boxall
committed
+ Initial import.
----- ToDo: Move Session garbage to a Object.
0 parents  commit 04f1b8d

File tree

7 files changed

+178
-0
lines changed

7 files changed

+178
-0
lines changed

__init__.py

Whitespace-only changes.

admin.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.contrib import admin
2+
from ab.models import Experiment, Test
3+
4+
class TestInline(admin.TabularInline):
5+
model = Test
6+
7+
# class TestAdmin(admin.ModelAdmin):
8+
# list_display = ("name", "hits", "conversion",)
9+
10+
class ExperimentAdmin(admin.ModelAdmin):
11+
# list_display = ("name", "hits", "conversion",)
12+
inlines = (TestInline,)
13+
14+
15+
admin.site.register(Experiment, ExperimentAdmin)
16+
# admin.site.register(Test, TestAdmin)

loaders.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from django.template.loaders.filesystem import load_template_source as default_template_loader
2+
from django.template import TemplateDoesNotExist
3+
4+
from ab.middleware import get_current_request
5+
from ab.models import Experiment
6+
7+
def load_template_source(template_name, template_dirs=None,
8+
template_loader=default_template_loader):
9+
"""If an Experiment exists for this template use template_loader to load it."""
10+
try:
11+
# @@@ This (c|sh)ould be a cached call.
12+
experiment = Experiment.objects.get(template_name=template_name)
13+
except Experiment.DoesNotExist:
14+
raise TemplateDoesNotExist, template_name
15+
16+
request = get_current_request()
17+
test_template_name = experiment.get_test_template_for_request(request)
18+
19+
return default_template_loader(test_template_name,
20+
template_dirs=template_dirs)
21+
load_template_source.is_usable = True
22+
23+

middleware.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
try:
2+
from threading import local
3+
except ImportError:
4+
from django.utils._threading_local import local
5+
6+
from ab.models import Experiment, Test
7+
8+
9+
_thread_locals = local()
10+
def get_current_request():
11+
return getattr(_thread_locals, 'request', None)
12+
13+
"""
14+
Things to keep in mind:
15+
* Only record goal conversions when a Experiment is active
16+
* Can only record a conversion once
17+
18+
what about only running a test X times and then defaulting to the best performer?
19+
20+
21+
!!! the session junk should be abstracted to some kind of model thing. SessionBackend.is_active etc.
22+
23+
"""
24+
25+
26+
# @@@ How will caching effect all this???
27+
class ABMiddleware:
28+
def process_request(self, request):
29+
"""
30+
Puts the request object in local thread storage.
31+
Also checks whether we've reached a A/B test goal.
32+
"""
33+
_thread_locals.request = request
34+
35+
# We can only do this if a Experiment is active.
36+
37+
38+
print request.session.keys()
39+
40+
if "ab_active" in request.session:
41+
experiments = Experiment.objects.all()
42+
for experiment in experiments:
43+
44+
print request.path
45+
print experiment.goal
46+
47+
if request.path == experiment.goal:
48+
49+
print 'yes'
50+
51+
# @@@ Also
52+
53+
54+
key = "ab_%s" % experiment.template_name
55+
if key in request.session and "converted" not in request.session[key]:
56+
print request.session[key]
57+
test_id = request.session[key]["id"]
58+
test = Test.objects.get(pk=test_id)
59+
test.conversions = test.conversions + 1
60+
test.save()
61+
62+
request.session[key]["converted"] = 1
63+
request.session.modified = True
64+
print request.session[key]
65+
66+
67+
68+
69+
70+
71+
72+

models.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from django.db import models
2+
3+
4+
# @@@ How to remember tests are active???
5+
6+
7+
class Experiment(models.Model):
8+
"""
9+
10+
"""
11+
# @@@ unique=True ??? Does that make sense???
12+
name = models.CharField(max_length=255, unique=True)
13+
template_name = models.CharField(max_length=255, unique=True,
14+
help_text="Example: 'registration/signup.html'. The template to replaced.")
15+
goal = models.CharField(max_length=255, unique=True,
16+
help_text="Example: '/signup/complete/'. The path where the goal is converted.")
17+
18+
def __unicode__(self):
19+
return self.name
20+
21+
def get_session_key(self):
22+
return "ab_%s" % self.template_name
23+
24+
def get_test_template_for_request(self, request):
25+
"""
26+
Given a request return one of the templates from it's Tests.
27+
Tests are sticky - if a viewer saw a Test before, they should
28+
see the same test.
29+
"""
30+
key = self.get_session_key()
31+
if key in request.session:
32+
return request.session[key]["template"]
33+
34+
# Pick a Test to show.
35+
tests = self.test_set.all()
36+
# @@@ This hash will probably make the django pony cry.
37+
test = tests[request.session.accessed % len(tests)]
38+
39+
# Record this unique hit to the Test.
40+
test.hits = test.hits + 1
41+
test.save()
42+
43+
# Make the Test sticky.
44+
if key not in request.session:
45+
print 'here'
46+
request.session[key] = {"id": test.id, "template": test.template_name}
47+
48+
49+
request.session["ab_active"] = True
50+
51+
52+
return test.template_name
53+
54+
55+
class Test(models.Model):
56+
"""
57+
58+
"""
59+
experiment = models.ForeignKey(Experiment)
60+
template_name = models.CharField(max_length=255,
61+
help_text="Example: 'registration/signup_1.html'. The template to be tested.")
62+
hits = models.IntegerField(blank=True, default=0)
63+
conversions = models.IntegerField(blank=True, default=0)
64+
65+
def __unicode__(self):
66+
return self.template_name

tests.py

Whitespace-only changes.

views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your views here.

0 commit comments

Comments
 (0)