Permalink
Browse files

+ Moved a bunch of functionality to ab.abs

  • Loading branch information...
1 parent 04f1b8d commit ddc152165e8d2f2eeffde4f763eecf13c87748ae @johnboxall committed May 18, 2009
Showing with 117 additions and 102 deletions.
  1. 0 README
  2. +90 −0 abs.py
  3. +3 −11 loaders.py
  4. +15 −54 middleware.py
  5. +0 −37 models.py
  6. +9 −0 tests.py
View
0 README
No changes.
View
90 abs.py
@@ -0,0 +1,90 @@
+from django.template import TemplateDoesNotExist
+
+from ab.models import Experiment, Test
+
+# @@@ The interface to this is shazbot. Rethink is in order.
+class AB(object):
+ """
+ Uses request session to track Experiment state.
+ - Whether an Experiment/Test is active
+ - Whether an Experiment/Test has been converted
+ """
+
+ def __init__(self, request):
+ self.request = request
+
+ def is_active(self):
+ """True if at least one Experiment is running on this request."""
+ return "ab_active" in self.request.session
+
+ def is_converted(self, exp):
+ """
+ True if request location is the Goal of Experiment and this request
+ hasn't already been converted.
+ """
+ return self.is_experiment_active(exp) and not self.is_experiment_converted(exp) \
+ and self.request.path == exp.goal
+
+ def is_experiment_active(self, exp):
+ """True if this Experiment is active."""
+ return self.get_experiment_key(exp) in self.request.session
+
+ def is_experiment_converted(self, exp):
+ """True if this Experiment has been converted."""
+ return "converted" in self.request.session[self.get_experiment_key(exp)]
+
+ def get_test(self, exp):
+ """Returns a random Test for this Experiment"""
+ tests = exp.test_set.all()
+ return tests[self.request.session.session_key.__hash__() % len(tests)]
+
+ def get_experiment_key(self, exp):
+ return "ab_exp_%s" % exp.name
+
+ def get_experiment(self, template_name):
+ try:
+ return Experiment.objects.get(template_name=template_name)
+ except Experiment.DoesNotExist:
+ raise TemplateDoesNotExist, template_name
+
+ def run(self, template_name):
+ """
+ Searches for an Experiment running on template_name. If none are found
+ raises a TemplateDoesNotExist otherwise activates a Test for that
+ Experiment unless one is already running and returns the Test
+ template_name.
+ """
+ exp = self.get_experiment(template_name)
+
+ # If this Experiment is active, return the template to show.
+ key = self.get_experiment_key(exp)
+ if self.is_experiment_active(exp):
+ return self.request.session[key]["template"]
+
+ # Otherwise Experiment isn't active so start one of its Tests.
+ test = self.get_test(exp)
+ self.activate(test, key)
+
+ return test.template_name
+
+ def activate(self, test, key):
+ # Record this hit.
+ test.hits = test.hits + 1
+ test.save()
+
+ # Activate this experiment/test on the request.
+ self.request.session[key] = {"id": test.id, "template": test.template_name}
+
+ # Mark that there is at least one A/B test running.
+ self.request.session["ab_active"] = True
+
+ def convert(self, exp):
+ """Update the test active on the request for this experiment."""
+ key = self.get_experiment_key(exp)
+ test_id = self.request.session[key]["id"]
+ test = Test.objects.get(pk=test_id)
+ test.conversions = test.conversions + 1
+ test.save()
+
+ self.request.session[key]["converted"] = 1
+ self.request.session.modified = True
View
14 loaders.py
@@ -1,20 +1,12 @@
from django.template.loaders.filesystem import load_template_source as default_template_loader
-from django.template import TemplateDoesNotExist
-
from ab.middleware import get_current_request
-from ab.models import Experiment
+
def load_template_source(template_name, template_dirs=None,
template_loader=default_template_loader):
- """If an Experiment exists for this template use template_loader to load it."""
- try:
- # @@@ This (c|sh)ould be a cached call.
- experiment = Experiment.objects.get(template_name=template_name)
- except Experiment.DoesNotExist:
- raise TemplateDoesNotExist, template_name
-
+ """If an Experiment exists for this template use template_loader to load it."""
request = get_current_request()
- test_template_name = experiment.get_test_template_for_request(request)
+ test_template_name = request.ab.run(template_name)
return default_template_loader(test_template_name,
template_dirs=template_dirs)
View
69 middleware.py
@@ -3,70 +3,31 @@
except ImportError:
from django.utils._threading_local import local
-from ab.models import Experiment, Test
+from ab.abs import AB
+from ab.models import Experiment
_thread_locals = local()
def get_current_request():
return getattr(_thread_locals, 'request', None)
-"""
-Things to keep in mind:
-* Only record goal conversions when a Experiment is active
-* Can only record a conversion once
-what about only running a test X times and then defaulting to the best performer?
-
-
-!!! the session junk should be abstracted to some kind of model thing. SessionBackend.is_active etc.
-
-"""
-
-
-# @@@ How will caching effect all this???
+# @@@ This won't work with caching. Need to create an AB aware cache middleware.
class ABMiddleware:
def process_request(self, request):
"""
- Puts the request object in local thread storage.
- Also checks whether we've reached a A/B test goal.
+ Puts the request object in local thread storage so we can access it in
+ the template loader. If an Experiment is active then check whether we've
+ reached it's goal.
"""
_thread_locals.request = request
- # We can only do this if a Experiment is active.
-
-
- print request.session.keys()
-
- if "ab_active" in request.session:
- experiments = Experiment.objects.all()
- for experiment in experiments:
-
- print request.path
- print experiment.goal
-
- if request.path == experiment.goal:
-
- print 'yes'
-
- # @@@ Also
-
-
- key = "ab_%s" % experiment.template_name
- if key in request.session and "converted" not in request.session[key]:
- print request.session[key]
- test_id = request.session[key]["id"]
- test = Test.objects.get(pk=test_id)
- test.conversions = test.conversions + 1
- test.save()
-
- request.session[key]["converted"] = 1
- request.session.modified = True
- print request.session[key]
-
-
-
-
-
-
-
-
+ request.ab = AB(request)
+ # request.ab.run()
+ # If at least one Experiment is running then check if we're at a Goal
+ # @@@ All this logic seems like it could be moved into the AB class. (but does it belong there?)
+ if request.ab.is_active():
+ exps = Experiment.objects.all()
+ for exp in exps:
+ if request.ab.is_converted(exp):
+ request.ab.convert(exp)
View
37 models.py
@@ -1,9 +1,5 @@
from django.db import models
-
-# @@@ How to remember tests are active???
-
-
class Experiment(models.Model):
"""
@@ -18,39 +14,6 @@ class Experiment(models.Model):
def __unicode__(self):
return self.name
- def get_session_key(self):
- return "ab_%s" % self.template_name
-
- def get_test_template_for_request(self, request):
- """
- Given a request return one of the templates from it's Tests.
- Tests are sticky - if a viewer saw a Test before, they should
- see the same test.
- """
- key = self.get_session_key()
- if key in request.session:
- return request.session[key]["template"]
-
- # Pick a Test to show.
- tests = self.test_set.all()
- # @@@ This hash will probably make the django pony cry.
- test = tests[request.session.accessed % len(tests)]
-
- # Record this unique hit to the Test.
- test.hits = test.hits + 1
- test.save()
-
- # Make the Test sticky.
- if key not in request.session:
- print 'here'
- request.session[key] = {"id": test.id, "template": test.template_name}
-
-
- request.session["ab_active"] = True
-
-
- return test.template_name
-
class Test(models.Model):
"""
View
9 tests.py
@@ -0,0 +1,9 @@
+
+
+from django.contrib.flatpages
+
+
+"""
+If flatpages is installed this is easy to test... if not... not so much.
+
+"""

0 comments on commit ddc1521

Please sign in to comment.