Skip to content
Browse files

Record number of days visited

  • Loading branch information...
1 parent 63ac756 commit eb700e1a22f2810160b48832b32104aaf3f90ee9 @theospears theospears committed Jan 10, 2013
View
93 ...migrations/0003_auto__del_field_enrollment_goals__add_field_enrollment_last_seen__chg_.py
@@ -0,0 +1,93 @@
+# encoding: utf-8
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+
+ # Deleting field 'Enrollment.goals'
+ db.delete_column('experiments_enrollment', 'goals')
+
+ # Adding field 'Enrollment.last_seen'
+ db.add_column('experiments_enrollment', 'last_seen', self.gf('django.db.models.fields.DateTimeField')(null=True), keep_default=False)
+
+ # Changing field 'Enrollment.enrollment_date'
+ db.alter_column('experiments_enrollment', 'enrollment_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True))
+
+
+ def backwards(self, orm):
+
+ # Adding field 'Enrollment.goals'
+ db.add_column('experiments_enrollment', 'goals', self.gf('jsonfield.fields.JSONField')(default='{}', null=True, blank=True), keep_default=False)
+
+ # Deleting field 'Enrollment.last_seen'
+ db.delete_column('experiments_enrollment', 'last_seen')
+
+ # Changing field 'Enrollment.enrollment_date'
+ db.alter_column('experiments_enrollment', 'enrollment_date', self.gf('django.db.models.fields.DateField')(auto_now_add=True, blank=True))
+
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 1, 10, 17, 57, 32, 153242)'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2013, 1, 10, 17, 57, 32, 153087)'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'experiments.enrollment': {
+ 'Meta': {'unique_together': "(('user', 'experiment'),)", 'object_name': 'Enrollment'},
+ 'alternative': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'enrollment_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'experiment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['experiments.Experiment']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'})
+ },
+ 'experiments.experiment': {
+ 'Meta': {'object_name': 'Experiment'},
+ 'alternatives': ('jsonfield.fields.JSONField', [], {'default': "'{}'", 'blank': 'True'}),
+ 'description': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'end_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'primary_key': 'True'}),
+ 'relevant_chi2_goals': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'relevant_mwu_goals': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'start_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
+ 'state': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'switch_key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'})
+ }
+ }
+
+ complete_apps = ['experiments']
View
3 experiments/models.py
@@ -165,7 +165,8 @@ class Enrollment(models.Model):
""" A participant in a split testing experiment """
user = models.ForeignKey(User, null=True)
experiment = models.ForeignKey(Experiment)
- enrollment_date = models.DateField(db_index=True, auto_now_add=True)
+ enrollment_date = models.DateTimeField(auto_now_add=True)
+ last_seen = models.DateTimeField(null=True)
alternative = models.CharField(max_length=50)
class Meta:
View
6 experiments/templatetags/experiments.py
@@ -52,3 +52,9 @@ def experiment(parser, token):
"{% experiment experiment_name alternative %}")
return ExperimentNode(node_list, experiment_name, alternative)
+
+@register.simple_tag(takes_context=True)
+def visit(context):
+ request = context.get('request', None)
+ participant(request).visit()
+ return ""
View
21 experiments/tests/webuser.py
@@ -6,7 +6,7 @@
from django.contrib.sessions.backends.db import SessionStore as DatabaseSession
from experiments.models import Experiment, ENABLED_STATE, CONTROL_GROUP
-from experiments.utils import participant
+from experiments.utils import participant, VISIT_COUNT_GOAL
request_factory = RequestFactory()
TEST_ALTERNATIVE = 'blue'
@@ -64,6 +64,25 @@ def test_counts_increment_immediately_once_confirmed_human(self):
experiment_user.set_alternative(EXPERIMENT_NAME, TEST_ALTERNATIVE)
self.assertEqual(self.participants(TEST_ALTERNATIVE), 1, "Did not count participant after confirm human")
+ def test_visit_increases_goal(self):
+ experiment_user = participant(self.request)
+ self.confirm_human(experiment_user)
+ experiment_user.set_alternative(EXPERIMENT_NAME, TEST_ALTERNATIVE)
+
+ experiment_user.visit()
+
+ self.assertEqual(self.experiment.goal_distribution(TEST_ALTERNATIVE, VISIT_COUNT_GOAL), {1: 1})
+
+ def test_visit_twice_increases_once(self):
+ experiment_user = participant(self.request)
+ self.confirm_human(experiment_user)
+ experiment_user.set_alternative(EXPERIMENT_NAME, TEST_ALTERNATIVE)
+
+ experiment_user.visit()
+ experiment_user.visit()
+
+ self.assertEqual(self.experiment.goal_distribution(TEST_ALTERNATIVE, VISIT_COUNT_GOAL), {1: 1})
+
class WebUserAnonymousTestCase(WebUserTests, TestCase):
def setUp(self):
View
49 experiments/utils.py
@@ -5,10 +5,15 @@
from experiments.models import Enrollment, CONTROL_GROUP
from experiments.manager import experiment_manager
+from experiments.dateutils import now
+
from collections import namedtuple
import re
import warnings
+from datetime import timedelta
+
+VISIT_COUNT_GOAL = '_retention_visits'
# Known bots user agents to drop from experiments
@@ -50,7 +55,7 @@ def _get_participant(request, session, user):
else:
return DummyUser()
-EnrollmentData = namedtuple('EnrollmentData', ['experiment', 'alternative'])
+EnrollmentData = namedtuple('EnrollmentData', ['experiment', 'alternative', 'enrollment_date', 'last_seen'])
class WebUser(object):
"""Represents a user (either authenticated or session based) which can take part in experiments"""
@@ -122,6 +127,14 @@ def incorporate(self, other_user):
enrollment.experiment.increment_goal_count(enrollment.alternative, goal_name, self._participant_identifier(), count)
other_user._cancel_enrollment(enrollment.experiment)
+ def visit(self):
+ """Record that the user has visited the site for the purposes of retention tracking"""
+ for enrollment in self._get_all_enrollments():
+ if enrollment.experiment.is_displaying_alternatives():
+ if not enrollment.last_seen or now() - enrollment.last_seen >= timedelta(1):
+ self._experiment_goal(enrollment.experiment, enrollment.alternative, VISIT_COUNT_GOAL, 1)
+ self._set_last_seen(enrollment.experiment, now())
+
def _get_enrollment(self, experiment):
"""Get the name of the alternative this user is enrolled in for the specified experiment
@@ -161,6 +174,10 @@ def _experiment_goal(self, experiment, alternative, goal_name, count):
"Record a goal against a particular experiment and alternative"
raise NotImplementedError
+ def _set_last_seen(self, experiment, last_seen):
+ "Set the last time the user was seen associated with this experiment"
+ raise NotImplementedError
+
def _gargoyle_key(self):
return None
@@ -187,6 +204,8 @@ def _get_goal_counts(self, experiment, alternative):
return {}
def _experiment_goal(self, experiment, alternative, goal_name, count):
pass
+ def _set_last_seen(self, experiment, last_seen):
+ pass
class AuthenticatedUser(WebUser):
@@ -226,7 +245,7 @@ def _get_all_enrollments(self):
enrollments = Enrollment.objects.filter(user=self.user).select_related("experiment")
if enrollments:
for enrollment in enrollments:
- yield EnrollmentData(enrollment.experiment, enrollment.alternative)
+ yield EnrollmentData(enrollment.experiment, enrollment.alternative, enrollment.enrollment_date, enrollment.last_seen)
def _cancel_enrollment(self, experiment):
try:
@@ -240,9 +259,21 @@ def _cancel_enrollment(self, experiment):
def _experiment_goal(self, experiment, alternative, goal_name, count):
experiment.increment_goal_count(alternative, goal_name, self._participant_identifier(), count)
+ def _set_last_seen(self, experiment, last_seen):
+ Enrollment.objects.filter(user=self.user, experiment=experiment).update(last_seen=last_seen)
+
def _gargoyle_key(self):
return self.request or self.user
+def _session_enrollment_latest_version(data):
+ try:
+ alternative, unused, enrollment_date, last_seen = data
+ except ValueError: # Data from previous version
+ alternative, unused = data
+ enrollment_date = None
+ last_seen = None
+ return alternative, unused, enrollment_date, last_seen
+
class SessionUser(WebUser):
def __init__(self, session, request=None):
@@ -253,13 +284,13 @@ def __init__(self, session, request=None):
def _get_enrollment(self, experiment):
enrollments = self.session.get('experiments_enrollments', None)
if enrollments and experiment.name in enrollments:
- alternative, goals = enrollments[experiment.name]
+ alternative, _, _, _ = _session_enrollment_latest_version(enrollments[experiment.name])
return alternative
return None
def _set_enrollment(self, experiment, alternative):
enrollments = self.session.get('experiments_enrollments', {})
- enrollments[experiment.name] = (alternative, [])
+ enrollments[experiment.name] = (alternative, None, now(), None)
self.session['experiments_enrollments'] = enrollments
if self._is_verified_human():
experiment.increment_participant_count(alternative, self._participant_identifier())
@@ -298,10 +329,10 @@ def _get_all_enrollments(self):
enrollments = self.session.get('experiments_enrollments', None)
if enrollments:
for experiment_name, data in enrollments.items():
- alternative, _ = data
+ alternative, _, enrollment_date, last_seen = _session_enrollment_latest_version(data)
experiment = experiment_manager.get(experiment_name, None)
if experiment:
- yield EnrollmentData(experiment, alternative)
+ yield EnrollmentData(experiment, alternative, enrollment_date, last_seen)
def _cancel_enrollment(self, experiment):
alternative = self._get_enrollment(experiment)
@@ -318,6 +349,12 @@ def _experiment_goal(self, experiment, alternative, goal_name, count):
goals.append( (experiment.name, alternative, goal_name, count) )
self.session['experiments_goals'] = goals
+ def _set_last_seen(self, experiment, last_seen):
+ enrollments = self.session.get('experiments_enrollments', {})
+ alternative, unused, enrollment_date, _ = _session_enrollment_latest_version(enrollments[experiment.name])
+ enrollments[experiment.name] = (alternative, unused, enrollment_date, last_seen)
+ self.session['experiments_enrollments'] = enrollments
+
def _gargoyle_key(self):
return self.request

0 comments on commit eb700e1

Please sign in to comment.
Something went wrong with that request. Please try again.