camping-abingo 1.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README +3 -0
- data/doc/classes/ABingoCampingPlugin.html +188 -0
- data/doc/classes/ABingoCampingPlugin.src/M000016.html +18 -0
- data/doc/classes/ABingoCampingPlugin.src/M000017.html +18 -0
- data/doc/classes/ABingoCampingPlugin.src/M000018.html +19 -0
- data/doc/classes/ABingoCampingPlugin.src/M000034.html +18 -0
- data/doc/classes/ABingoCampingPlugin.src/M000035.html +18 -0
- data/doc/classes/ABingoCampingPlugin.src/M000036.html +19 -0
- data/doc/classes/ABingoCampingPlugin/ABingo.html +121 -0
- data/doc/classes/ABingoCampingPlugin/Controllers.html +191 -0
- data/doc/classes/ABingoCampingPlugin/Controllers.src/M000022.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Controllers.src/M000023.html +26 -0
- data/doc/classes/ABingoCampingPlugin/Controllers.src/M000040.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Controllers.src/M000041.html +26 -0
- data/doc/classes/ABingoCampingPlugin/Filters.html +158 -0
- data/doc/classes/ABingoCampingPlugin/Filters.src/M000037.html +26 -0
- data/doc/classes/ABingoCampingPlugin/Filters.src/M000055.html +26 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.html +372 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000024.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000025.html +19 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000026.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000027.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000028.html +36 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000029.html +28 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000030.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000031.html +48 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000032.html +32 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000033.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000034.html +26 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000035.html +20 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000036.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000042.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000043.html +19 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000044.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000045.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000046.html +36 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000047.html +28 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000048.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000049.html +48 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000050.html +32 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000051.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000052.html +26 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000053.html +20 -0
- data/doc/classes/ABingoCampingPlugin/Helpers.src/M000054.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Models.html +208 -0
- data/doc/classes/ABingoCampingPlugin/Models.src/M000038.html +22 -0
- data/doc/classes/ABingoCampingPlugin/Models.src/M000039.html +39 -0
- data/doc/classes/ABingoCampingPlugin/Models.src/M000040.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models.src/M000056.html +22 -0
- data/doc/classes/ABingoCampingPlugin/Models.src/M000057.html +39 -0
- data/doc/classes/ABingoCampingPlugin/Models.src/M000058.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Alternative.html +181 -0
- data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000041.html +20 -0
- data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000042.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000043.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000059.html +20 -0
- data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000060.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000061.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.html +273 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000044.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000045.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000046.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000047.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000048.html +20 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000049.html +23 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000050.html +29 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000051.html +50 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000052.html +28 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000062.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000063.html +21 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000064.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000065.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000066.html +20 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000067.html +23 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000068.html +29 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000069.html +50 -0
- data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000070.html +28 -0
- data/doc/classes/ABingoCampingPlugin/Views.html +196 -0
- data/doc/classes/ABingoCampingPlugin/Views.src/M000019.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Views.src/M000020.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Views.src/M000021.html +30 -0
- data/doc/classes/ABingoCampingPlugin/Views.src/M000037.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Views.src/M000038.html +18 -0
- data/doc/classes/ABingoCampingPlugin/Views.src/M000039.html +30 -0
- data/doc/classes/Abingo.html +429 -0
- data/doc/classes/Abingo.src/M000001.html +18 -0
- data/doc/classes/Abingo.src/M000002.html +18 -0
- data/doc/classes/Abingo.src/M000003.html +18 -0
- data/doc/classes/Abingo.src/M000004.html +22 -0
- data/doc/classes/Abingo.src/M000005.html +69 -0
- data/doc/classes/Abingo.src/M000006.html +47 -0
- data/doc/classes/Abingo.src/M000007.html +31 -0
- data/doc/classes/Abingo.src/M000008.html +36 -0
- data/doc/classes/Abingo.src/M000009.html +18 -0
- data/doc/classes/Abingo.src/M000010.html +36 -0
- data/doc/classes/Abingo.src/M000011.html +22 -0
- data/doc/classes/Abingo.src/M000012.html +21 -0
- data/doc/classes/Abingo.src/M000013.html +18 -0
- data/doc/classes/Abingo.src/M000014.html +39 -0
- data/doc/classes/Abingo.src/M000015.html +25 -0
- data/doc/classes/ActiveSupport.html +107 -0
- data/doc/classes/ActiveSupport/Cache.html +111 -0
- data/doc/classes/ActiveSupport/Cache/MemoryStore.html +137 -0
- data/doc/classes/ActiveSupport/Cache/MemoryStore.src/M000016.html +18 -0
- data/doc/classes/CampingABingoTest.html +149 -0
- data/doc/classes/CampingABingoTest.src/M000017.html +24 -0
- data/doc/classes/CampingABingoTest/Controllers.html +116 -0
- data/doc/classes/CampingABingoTest/Controllers/Index.html +137 -0
- data/doc/classes/CampingABingoTest/Controllers/Index.src/M000024.html +18 -0
- data/doc/classes/CampingABingoTest/Controllers/Landing.html +137 -0
- data/doc/classes/CampingABingoTest/Controllers/Landing.src/M000026.html +18 -0
- data/doc/classes/CampingABingoTest/Controllers/SignIn.html +152 -0
- data/doc/classes/CampingABingoTest/Controllers/SignIn.src/M000030.html +18 -0
- data/doc/classes/CampingABingoTest/Controllers/SignIn.src/M000031.html +34 -0
- data/doc/classes/CampingABingoTest/Controllers/SignOut.html +137 -0
- data/doc/classes/CampingABingoTest/Controllers/SignOut.src/M000029.html +21 -0
- data/doc/classes/CampingABingoTest/Controllers/SignUp.html +152 -0
- data/doc/classes/CampingABingoTest/Controllers/SignUp.src/M000027.html +23 -0
- data/doc/classes/CampingABingoTest/Controllers/SignUp.src/M000028.html +33 -0
- data/doc/classes/CampingABingoTest/Controllers/Welcome.html +137 -0
- data/doc/classes/CampingABingoTest/Controllers/Welcome.src/M000025.html +18 -0
- data/doc/classes/CampingABingoTest/Helpers.html +112 -0
- data/doc/classes/CampingABingoTest/Models.html +119 -0
- data/doc/classes/CampingABingoTest/Models/CreateUserSchema.html +152 -0
- data/doc/classes/CampingABingoTest/Models/CreateUserSchema.src/M000032.html +26 -0
- data/doc/classes/CampingABingoTest/Models/CreateUserSchema.src/M000033.html +19 -0
- data/doc/classes/CampingABingoTest/Models/User.html +111 -0
- data/doc/classes/CampingABingoTest/Views.html +206 -0
- data/doc/classes/CampingABingoTest/Views.src/M000018.html +70 -0
- data/doc/classes/CampingABingoTest/Views.src/M000019.html +22 -0
- data/doc/classes/CampingABingoTest/Views.src/M000020.html +51 -0
- data/doc/classes/CampingABingoTest/Views.src/M000021.html +31 -0
- data/doc/classes/CampingABingoTest/Views.src/M000022.html +31 -0
- data/doc/classes/CampingABingoTest/Views.src/M000023.html +20 -0
- data/doc/created.rid +1 -0
- data/doc/files/examples/camping-abingo-test/camping-abingo-test_rb.html +101 -0
- data/doc/files/lib/camping-abingo_rb.html +457 -0
- data/doc/fr_class_index.html +52 -0
- data/doc/fr_file_index.html +28 -0
- data/doc/fr_method_index.html +96 -0
- data/doc/index.html +24 -0
- data/lib/camping-abingo.rb +1233 -0
- metadata +279 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
<?xml version="1.0" encoding="iso-8859-1"?>
|
3
|
+
<!DOCTYPE html
|
4
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
5
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
6
|
+
|
7
|
+
<!--
|
8
|
+
|
9
|
+
Classes
|
10
|
+
|
11
|
+
-->
|
12
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
13
|
+
<head>
|
14
|
+
<title>Classes</title>
|
15
|
+
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
16
|
+
<link rel="stylesheet" href="rdoc-style.css" type="text/css" />
|
17
|
+
<base target="docwin" />
|
18
|
+
</head>
|
19
|
+
<body>
|
20
|
+
<div id="index">
|
21
|
+
<h1 class="section-bar">Classes</h1>
|
22
|
+
<div id="index-entries">
|
23
|
+
<a href="classes/ABingoCampingPlugin.html">ABingoCampingPlugin</a><br />
|
24
|
+
<a href="classes/ABingoCampingPlugin/ABingo.html">ABingoCampingPlugin::ABingo</a><br />
|
25
|
+
<a href="classes/ABingoCampingPlugin/Controllers.html">ABingoCampingPlugin::Controllers</a><br />
|
26
|
+
<a href="classes/ABingoCampingPlugin/Filters.html">ABingoCampingPlugin::Filters</a><br />
|
27
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html">ABingoCampingPlugin::Helpers</a><br />
|
28
|
+
<a href="classes/ABingoCampingPlugin/Models.html">ABingoCampingPlugin::Models</a><br />
|
29
|
+
<a href="classes/ABingoCampingPlugin/Models/Alternative.html">ABingoCampingPlugin::Models::Alternative</a><br />
|
30
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html">ABingoCampingPlugin::Models::Experiment</a><br />
|
31
|
+
<a href="classes/ABingoCampingPlugin/Views.html">ABingoCampingPlugin::Views</a><br />
|
32
|
+
<a href="classes/Abingo.html">Abingo</a><br />
|
33
|
+
<a href="classes/ActiveSupport.html">ActiveSupport</a><br />
|
34
|
+
<a href="classes/ActiveSupport/Cache.html">ActiveSupport::Cache</a><br />
|
35
|
+
<a href="classes/ActiveSupport/Cache/MemoryStore.html">ActiveSupport::Cache::MemoryStore</a><br />
|
36
|
+
<a href="classes/CampingABingoTest.html">CampingABingoTest</a><br />
|
37
|
+
<a href="classes/CampingABingoTest/Controllers.html">CampingABingoTest::Controllers</a><br />
|
38
|
+
<a href="classes/CampingABingoTest/Controllers/Index.html">CampingABingoTest::Controllers::Index</a><br />
|
39
|
+
<a href="classes/CampingABingoTest/Controllers/Landing.html">CampingABingoTest::Controllers::Landing</a><br />
|
40
|
+
<a href="classes/CampingABingoTest/Controllers/SignIn.html">CampingABingoTest::Controllers::SignIn</a><br />
|
41
|
+
<a href="classes/CampingABingoTest/Controllers/SignOut.html">CampingABingoTest::Controllers::SignOut</a><br />
|
42
|
+
<a href="classes/CampingABingoTest/Controllers/SignUp.html">CampingABingoTest::Controllers::SignUp</a><br />
|
43
|
+
<a href="classes/CampingABingoTest/Controllers/Welcome.html">CampingABingoTest::Controllers::Welcome</a><br />
|
44
|
+
<a href="classes/CampingABingoTest/Helpers.html">CampingABingoTest::Helpers</a><br />
|
45
|
+
<a href="classes/CampingABingoTest/Models.html">CampingABingoTest::Models</a><br />
|
46
|
+
<a href="classes/CampingABingoTest/Models/CreateUserSchema.html">CampingABingoTest::Models::CreateUserSchema</a><br />
|
47
|
+
<a href="classes/CampingABingoTest/Models/User.html">CampingABingoTest::Models::User</a><br />
|
48
|
+
<a href="classes/CampingABingoTest/Views.html">CampingABingoTest::Views</a><br />
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</body>
|
52
|
+
</html>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
|
2
|
+
<?xml version="1.0" encoding="iso-8859-1"?>
|
3
|
+
<!DOCTYPE html
|
4
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
5
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
6
|
+
|
7
|
+
<!--
|
8
|
+
|
9
|
+
Files
|
10
|
+
|
11
|
+
-->
|
12
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
13
|
+
<head>
|
14
|
+
<title>Files</title>
|
15
|
+
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
16
|
+
<link rel="stylesheet" href="rdoc-style.css" type="text/css" />
|
17
|
+
<base target="docwin" />
|
18
|
+
</head>
|
19
|
+
<body>
|
20
|
+
<div id="index">
|
21
|
+
<h1 class="section-bar">Files</h1>
|
22
|
+
<div id="index-entries">
|
23
|
+
<a href="files/examples/camping-abingo-test/camping-abingo-test_rb.html">examples/camping-abingo-test/camping-abingo-test.rb</a><br />
|
24
|
+
<a href="files/lib/camping-abingo_rb.html">lib/camping-abingo.rb</a><br />
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
</body>
|
28
|
+
</html>
|
@@ -0,0 +1,96 @@
|
|
1
|
+
|
2
|
+
<?xml version="1.0" encoding="iso-8859-1"?>
|
3
|
+
<!DOCTYPE html
|
4
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
5
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
6
|
+
|
7
|
+
<!--
|
8
|
+
|
9
|
+
Methods
|
10
|
+
|
11
|
+
-->
|
12
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
13
|
+
<head>
|
14
|
+
<title>Methods</title>
|
15
|
+
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
16
|
+
<link rel="stylesheet" href="rdoc-style.css" type="text/css" />
|
17
|
+
<base target="docwin" />
|
18
|
+
</head>
|
19
|
+
<body>
|
20
|
+
<div id="index">
|
21
|
+
<h1 class="section-bar">Methods</h1>
|
22
|
+
<div id="index-entries">
|
23
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000050">ab_test (ABingoCampingPlugin::Helpers)</a><br />
|
24
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000054">abingo_administrator_user_id (ABingoCampingPlugin::Helpers)</a><br />
|
25
|
+
<a href="classes/ABingoCampingPlugin/Views.html#M000037">abingo_view_helpers (ABingoCampingPlugin::Views)</a><br />
|
26
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000068">alternatives_for_test (ABingoCampingPlugin::Models::Experiment)</a><br />
|
27
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000043">app_module (ABingoCampingPlugin::Helpers)</a><br />
|
28
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000053">authenticate_abingo_administrator (ABingoCampingPlugin::Helpers)</a><br />
|
29
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000063">before_destroy (ABingoCampingPlugin::Models::Experiment)</a><br />
|
30
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000066">best_alternative (ABingoCampingPlugin::Models::Experiment)</a><br />
|
31
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000051">bingo! (ABingoCampingPlugin::Helpers)</a><br />
|
32
|
+
<a href="classes/Abingo.html#M000006">bingo! (Abingo)</a><br />
|
33
|
+
<a href="classes/Abingo.html#M000001">cache (Abingo)</a><br />
|
34
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000062">cache_keys (ABingoCampingPlugin::Models::Experiment)</a><br />
|
35
|
+
<a href="classes/ABingoCampingPlugin/Models/Alternative.html#M000059">calculate_lookup (ABingoCampingPlugin::Models::Alternative)</a><br />
|
36
|
+
<a href="classes/ABingoCampingPlugin/Controllers.html#M000040">common_abingo_controllers (ABingoCampingPlugin::Controllers)</a><br />
|
37
|
+
<a href="classes/ABingoCampingPlugin/Views.html#M000038">common_abingo_views (ABingoCampingPlugin::Views)</a><br />
|
38
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000044">conversion_rate (ABingoCampingPlugin::Helpers)</a><br />
|
39
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000065">conversions (ABingoCampingPlugin::Models::Experiment)</a><br />
|
40
|
+
<a href="classes/CampingABingoTest.html#M000017">create (CampingABingoTest)</a><br />
|
41
|
+
<a href="classes/ABingoCampingPlugin.html#M000036">create (ABingoCampingPlugin)</a><br />
|
42
|
+
<a href="classes/ActiveSupport/Cache/MemoryStore.html#M000016">data (ActiveSupport::Cache::MemoryStore)</a><br />
|
43
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000049">describe_result_in_words (ABingoCampingPlugin::Helpers)</a><br />
|
44
|
+
<a href="classes/ABingoCampingPlugin/Models.html#M000058">down (ABingoCampingPlugin::Models)</a><br />
|
45
|
+
<a href="classes/CampingABingoTest/Models/CreateUserSchema.html#M000033">down (CampingABingoTest::Models::CreateUserSchema)</a><br />
|
46
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000070">end_experiment! (ABingoCampingPlugin::Models::Experiment)</a><br />
|
47
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000067">exists? (ABingoCampingPlugin::Models::Experiment)</a><br />
|
48
|
+
<a href="classes/Abingo.html#M000015">expires_in (Abingo)</a><br />
|
49
|
+
<a href="classes/Abingo.html#M000012">find_alternative_for_user (Abingo)</a><br />
|
50
|
+
<a href="classes/Abingo.html#M000004">flip (Abingo)</a><br />
|
51
|
+
<a href="classes/CampingABingoTest/Controllers/Landing.html#M000026">get (CampingABingoTest::Controllers::Landing)</a><br />
|
52
|
+
<a href="classes/CampingABingoTest/Controllers/SignIn.html#M000030">get (CampingABingoTest::Controllers::SignIn)</a><br />
|
53
|
+
<a href="classes/CampingABingoTest/Controllers/SignUp.html#M000027">get (CampingABingoTest::Controllers::SignUp)</a><br />
|
54
|
+
<a href="classes/CampingABingoTest/Controllers/Index.html#M000024">get (CampingABingoTest::Controllers::Index)</a><br />
|
55
|
+
<a href="classes/CampingABingoTest/Controllers/SignOut.html#M000029">get (CampingABingoTest::Controllers::SignOut)</a><br />
|
56
|
+
<a href="classes/CampingABingoTest/Controllers/Welcome.html#M000025">get (CampingABingoTest::Controllers::Welcome)</a><br />
|
57
|
+
<a href="classes/Abingo.html#M000008">human! (Abingo)</a><br />
|
58
|
+
<a href="classes/Abingo.html#M000003">identity (Abingo)</a><br />
|
59
|
+
<a href="classes/Abingo.html#M000002">identity= (Abingo)</a><br />
|
60
|
+
<a href="classes/ABingoCampingPlugin/Controllers.html#M000041">include_abingo_controllers (ABingoCampingPlugin::Controllers)</a><br />
|
61
|
+
<a href="classes/ABingoCampingPlugin/Views.html#M000039">include_abingo_views (ABingoCampingPlugin::Views)</a><br />
|
62
|
+
<a href="classes/ABingoCampingPlugin/Models.html#M000056">included (ABingoCampingPlugin::Models)</a><br />
|
63
|
+
<a href="classes/ABingoCampingPlugin/Filters.html#M000055">included (ABingoCampingPlugin::Filters)</a><br />
|
64
|
+
<a href="classes/CampingABingoTest/Views.html#M000019">index (CampingABingoTest::Views)</a><br />
|
65
|
+
<a href="classes/Abingo.html#M000009">is_human? (Abingo)</a><br />
|
66
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000048">is_statistically_significant? (ABingoCampingPlugin::Helpers)</a><br />
|
67
|
+
<a href="classes/CampingABingoTest/Views.html#M000020">landing (CampingABingoTest::Views)</a><br />
|
68
|
+
<a href="classes/CampingABingoTest/Views.html#M000018">layout (CampingABingoTest::Views)</a><br />
|
69
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000042">log_debug (ABingoCampingPlugin::Helpers)</a><br />
|
70
|
+
<a href="classes/ABingoCampingPlugin.html#M000034">logger (ABingoCampingPlugin)</a><br />
|
71
|
+
<a href="classes/ABingoCampingPlugin.html#M000035">logger= (ABingoCampingPlugin)</a><br />
|
72
|
+
<a href="classes/Abingo.html#M000013">modulo_choice (Abingo)</a><br />
|
73
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000047">p_value (ABingoCampingPlugin::Helpers)</a><br />
|
74
|
+
<a href="classes/Abingo.html#M000010">parse_alternatives (Abingo)</a><br />
|
75
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000064">participants (ABingoCampingPlugin::Models::Experiment)</a><br />
|
76
|
+
<a href="classes/Abingo.html#M000007">participating_tests (Abingo)</a><br />
|
77
|
+
<a href="classes/CampingABingoTest/Controllers/SignIn.html#M000031">post (CampingABingoTest::Controllers::SignIn)</a><br />
|
78
|
+
<a href="classes/CampingABingoTest/Controllers/SignUp.html#M000028">post (CampingABingoTest::Controllers::SignUp)</a><br />
|
79
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000045">pretty_conversion_rate (ABingoCampingPlugin::Helpers)</a><br />
|
80
|
+
<a href="classes/Abingo.html#M000011">retrieve_alternatives (Abingo)</a><br />
|
81
|
+
<a href="classes/ABingoCampingPlugin/Models/Alternative.html#M000060">score_conversion (ABingoCampingPlugin::Models::Alternative)</a><br />
|
82
|
+
<a href="classes/Abingo.html#M000014">score_conversion! (Abingo)</a><br />
|
83
|
+
<a href="classes/ABingoCampingPlugin/Models/Alternative.html#M000061">score_participation (ABingoCampingPlugin::Models::Alternative)</a><br />
|
84
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000052">set_abingo_identity (ABingoCampingPlugin::Helpers)</a><br />
|
85
|
+
<a href="classes/CampingABingoTest/Views.html#M000021">signin (CampingABingoTest::Views)</a><br />
|
86
|
+
<a href="classes/CampingABingoTest/Views.html#M000022">signup (CampingABingoTest::Views)</a><br />
|
87
|
+
<a href="classes/ABingoCampingPlugin/Models/Experiment.html#M000069">start_experiment! (ABingoCampingPlugin::Models::Experiment)</a><br />
|
88
|
+
<a href="classes/Abingo.html#M000005">test (Abingo)</a><br />
|
89
|
+
<a href="classes/ABingoCampingPlugin/Models.html#M000057">up (ABingoCampingPlugin::Models)</a><br />
|
90
|
+
<a href="classes/CampingABingoTest/Models/CreateUserSchema.html#M000032">up (CampingABingoTest::Models::CreateUserSchema)</a><br />
|
91
|
+
<a href="classes/CampingABingoTest/Views.html#M000023">welcome (CampingABingoTest::Views)</a><br />
|
92
|
+
<a href="classes/ABingoCampingPlugin/Helpers.html#M000046">zscore (ABingoCampingPlugin::Helpers)</a><br />
|
93
|
+
</div>
|
94
|
+
</div>
|
95
|
+
</body>
|
96
|
+
</html>
|
data/doc/index.html
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
<?xml version="1.0" encoding="iso-8859-1"?>
|
2
|
+
<!DOCTYPE html
|
3
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
|
4
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">
|
5
|
+
|
6
|
+
<!--
|
7
|
+
|
8
|
+
RDoc Documentation
|
9
|
+
|
10
|
+
-->
|
11
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
12
|
+
<head>
|
13
|
+
<title>RDoc Documentation</title>
|
14
|
+
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
|
15
|
+
</head>
|
16
|
+
<frameset rows="20%, 80%">
|
17
|
+
<frameset cols="25%,35%,45%">
|
18
|
+
<frame src="fr_file_index.html" title="Files" name="Files" />
|
19
|
+
<frame src="fr_class_index.html" name="Classes" />
|
20
|
+
<frame src="fr_method_index.html" name="Methods" />
|
21
|
+
</frameset>
|
22
|
+
<frame src="files/examples/camping-abingo-test/camping-abingo-test_rb.html" name="docwin" />
|
23
|
+
</frameset>
|
24
|
+
</html>
|
@@ -0,0 +1,1233 @@
|
|
1
|
+
=begin rdoc
|
2
|
+
|
3
|
+
[Abingo|identity|flip();test();bingo!(){bg:red}]
|
4
|
+
[Experiment|test_name;status|start_experiment!();end_experiment!(){bg:green}]
|
5
|
+
[Alternative|content;lookup;weight;participants;conversions|score_conversion();score_participation(){bg:yellow}]
|
6
|
+
[User|id;username{bg:blue}]
|
7
|
+
[Abingo]uses -.->[Experiment]
|
8
|
+
[Abingo]-id>[User]
|
9
|
+
[Experiment]++1-alternatives >*[Alternative]
|
10
|
+
|
11
|
+
|
12
|
+
Author:: Philippe F. Monnet (mailto:pfmonnet@gmail.com)
|
13
|
+
Copyright:: Copyright (c) 2010 Philippe F. Monnet - ABingo Camping plugin
|
14
|
+
Copyright:: Copyright (c) 2009 Patrick McKenzie - A subset of the Rails ABingo plugin reused in ABingo Camping
|
15
|
+
License:: Distributes under the same terms as Ruby
|
16
|
+
Version:: 1.0.3
|
17
|
+
|
18
|
+
:main: Camping-ABingo
|
19
|
+
|
20
|
+
=Installing Camping-ABingo
|
21
|
+
A lightweight ABingo plugin for Ruby Camping.
|
22
|
+
To install the library and its prerequisisites, type the following command(s):
|
23
|
+
|
24
|
+
$ gem install camping-abingo
|
25
|
+
|
26
|
+
=Adding ABingo Provider Support To Your App
|
27
|
+
|
28
|
+
|
29
|
+
===Add new gem and require statements
|
30
|
+
Add the following statements towards the top of your source file (before the Camping.goes statement):
|
31
|
+
|
32
|
+
gem 'camping' , '>= 2.0'
|
33
|
+
gem 'filtering_camping' , '>= 1.0'
|
34
|
+
|
35
|
+
%w(rubygems active_record erb fileutils json markaby md5 redcloth
|
36
|
+
camping camping/session filtering_camping camping-abingo
|
37
|
+
).each { |lib| require lib }
|
38
|
+
|
39
|
+
===Customizing the main module
|
40
|
+
|
41
|
+
First we'll make sure to include the Camping::Session and CampingFilters modules, and to extend the app module with ABingoCampingPlugin, like so:
|
42
|
+
|
43
|
+
module CampingABingoTest
|
44
|
+
include Camping::Session
|
45
|
+
include CampingFilters
|
46
|
+
|
47
|
+
extend ABingoCampingPlugin
|
48
|
+
include ABingoCampingPlugin::Filters
|
49
|
+
|
50
|
+
# ...
|
51
|
+
end
|
52
|
+
|
53
|
+
This gives us the ability to leverage a logger for the camping-abingo plugin.
|
54
|
+
|
55
|
+
app_logger = Logger.new(File.dirname(__FILE__) + '/camping-abingo-test.log')
|
56
|
+
app_logger.level = Logger::DEBUG
|
57
|
+
Camping::Models::Base.logger = app_logger
|
58
|
+
ABingoCampingPlugin.logger = app_logger
|
59
|
+
|
60
|
+
Now let's customize the create method by adding a call to ABingoCampingPlugin.create, so we can give the plugin to run any needed initialization
|
61
|
+
(such as running the ABingo-specific ActiveRecord migration).
|
62
|
+
|
63
|
+
def CampingABingoTest.create
|
64
|
+
ABingoCampingPlugin.create
|
65
|
+
end
|
66
|
+
|
67
|
+
Also if you plan on using the ABingo dashboard to view the statistics, you need to define the user id associated with the administrator:
|
68
|
+
|
69
|
+
Abingo.options[:abingo_administrator_user_id] = 1 #change this id to match the account id you need
|
70
|
+
|
71
|
+
Ok, at this point we have a minimally configured application module. Our next step is to move on to the Models module.
|
72
|
+
|
73
|
+
===Plugging in the ABingo models
|
74
|
+
|
75
|
+
First, we'll include the include ABingoCampingPlugin::Models module so we can get all the ABingo-specific models. Then we'll define a User model. The User will need to keep track of the applications it provided access to. It will also manage the tokens associated with these applications. Our model will look like this:
|
76
|
+
|
77
|
+
class User < Base;
|
78
|
+
has_many :client_applications
|
79
|
+
has_many :tokens,
|
80
|
+
:class_name=>"OauthToken",
|
81
|
+
:order=>"authorized_at desc",
|
82
|
+
:include=>[:client_application]
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
Now we need a CreateUserSchema migration class to define our database tables for User, and ABingo models. In the up and down methods we will plugin a call to the corresponding method from the ABingoCampingPlugin::Models module to create the tables for the Experiment and Alternative models.
|
87
|
+
|
88
|
+
class CreateUserSchema < V 1.0
|
89
|
+
def self.up
|
90
|
+
create_table :CampingABingoTest_users, :force => true do |t|
|
91
|
+
t.integer :id, :null => false
|
92
|
+
t.string :username
|
93
|
+
t.string :password
|
94
|
+
end
|
95
|
+
|
96
|
+
User.create :username => 'admin', :password => 'camping'
|
97
|
+
|
98
|
+
ABingoCampingPlugin::Models.up
|
99
|
+
end
|
100
|
+
|
101
|
+
def self.down
|
102
|
+
ABingoCampingPlugin::Models.down
|
103
|
+
|
104
|
+
drop_table :CampingABingoTest_users
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
At this point we can go back to the main module and add the code to configure the ActiveRecord connection and invoke our new schema migration if the User table does not exist yet. This code will be added to the create method:
|
109
|
+
|
110
|
+
module CampingABingoTest
|
111
|
+
# ...
|
112
|
+
|
113
|
+
def CampingABingoTest.create
|
114
|
+
dbconfig = YAML.load(File.read('config/database.yml'))
|
115
|
+
Camping::Models::Base.establish_connection dbconfig['development']
|
116
|
+
|
117
|
+
ABingoCampingPlugin.create
|
118
|
+
Abingo.cache.logger = Camping::Models::Base.logger
|
119
|
+
Abingo.options[:abingo_administrator_user_id] = 1
|
120
|
+
|
121
|
+
CampingABingoTest::Models.create_schema :assume => (CampingABingoTest::Models::User.table_exists? ? 1.1 : 0.0)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
You probably noticed that the database configuration is loaded from a database.yml file. So let's create a subfolder named config and a file named database.yml, then let's configure the yaml file as follows:
|
126
|
+
|
127
|
+
development:
|
128
|
+
adapter: sqlite3
|
129
|
+
database: camping-abingo-test.db
|
130
|
+
|
131
|
+
Now if we restart the application, our migration should be executed.
|
132
|
+
|
133
|
+
===Creating a common helpers module
|
134
|
+
|
135
|
+
The Helpers module is used in Camping to provide common utilities to both the Controllers and Views modules. Enhancing our Helpers module is very easy, we need to add both and extend and an include of the ABingoCampingPlugin::Helpers module so we can enhance both instance and class sides:
|
136
|
+
|
137
|
+
module CampingABingoTest::Helpers
|
138
|
+
extend ABingoCampingPlugin::Helpers
|
139
|
+
include ABingoCampingPlugin::Helpers
|
140
|
+
end
|
141
|
+
|
142
|
+
===Plugging in the ABingo controllers
|
143
|
+
|
144
|
+
We will need to extend our app Controllers module with the ABingoCampingPlugin::Controllers module using the extend statement. Then just before the end of the Controllers module, we'll add a call to the include_abingo_controllers method. This is how camping-abingo will inject and plugin the common ABingo controllers and helpers. It is important that this call always remain the last statement of the module, even when you add new controller classes. So the module should look like so:
|
145
|
+
|
146
|
+
module CampingABingoTest::Controllers
|
147
|
+
extend ABingoCampingPlugin::Controllers
|
148
|
+
|
149
|
+
# ...
|
150
|
+
|
151
|
+
include_abingo_controllers
|
152
|
+
end #Controllers
|
153
|
+
|
154
|
+
Before we continue fleshing out the logic of our controllers, let's finish hooking up the Views module.
|
155
|
+
|
156
|
+
===Plugging in the ABingo common views
|
157
|
+
|
158
|
+
We will need to extend our app Views module with the ABingoCampingPlugin::Views module using the extend statement. Then just before the end of the Views module, we'll add a call to the include_abingo_views method. This is how camping-abingo will inject and plugin the common ABingo views. It is important that this call always remain the last statement of the module, even when you add new view methods. So the module should look like so:
|
159
|
+
|
160
|
+
module CampingABingoTest::Views
|
161
|
+
extend ABingoCampingPlugin::Views
|
162
|
+
|
163
|
+
# ...
|
164
|
+
|
165
|
+
include_abingo_views
|
166
|
+
end
|
167
|
+
|
168
|
+
|
169
|
+
==Testing And Troubleshooting
|
170
|
+
|
171
|
+
At this stage, we have a basic Camping ABingo Tester, now let's test it!
|
172
|
+
Run:
|
173
|
+
camping --port 3301 camping-abingo-test.rb
|
174
|
+
|
175
|
+
The migrations for both our test app and the plugin will be run. So at this point you should see 3 tables:
|
176
|
+
- abingo_alternatives
|
177
|
+
- abingo_experiments
|
178
|
+
- CampingABingoTest_users
|
179
|
+
|
180
|
+
=== Test App
|
181
|
+
|
182
|
+
I suggest you test the app using FireFox with the Firebug and Firecookie activated. This will make troubleshooting much easier.
|
183
|
+
|
184
|
+
Navigate to:
|
185
|
+
http://localhost:3301/
|
186
|
+
|
187
|
+
Notice in the Debugging Information panel of the page that Abingo assigned a state variable named abingo_identity with a random value.
|
188
|
+
Click on the "XYZ SAAS Application Landing page variations" link. This page contains 2 variable content areas:
|
189
|
+
- The "special_promo" div content
|
190
|
+
- The "signup_btn" button text
|
191
|
+
|
192
|
+
Abingo will randomly select an alternative for each area.
|
193
|
+
Once you click on the sign up button, ABingo will track the conversion.
|
194
|
+
|
195
|
+
Now, using Firecookie, delete the campingabingotest.state cookie. And go back to the main page. Notice that the abingo_identity has changed.
|
196
|
+
You can repeat the scenario and probably will get different results.
|
197
|
+
|
198
|
+
=== Dashboard
|
199
|
+
|
200
|
+
If you had created a test account and logged in, the sign-out and sign back in as the administrator of the test app (admin/camping)/
|
201
|
+
You should now see an "ABingo Dashboard" option on the top navigation. Click on it or navigate to:
|
202
|
+
http://localhost:3301/abingo/dashboard
|
203
|
+
|
204
|
+
You should see two experiments:
|
205
|
+
- Special Promo
|
206
|
+
- Call To Action
|
207
|
+
|
208
|
+
For each experiment you can see the various alternatives and their associated number of participants and conversions.There is also a summary of the experiment.
|
209
|
+
If you want to terminate an experiment click on the corresponding link in the far right column.
|
210
|
+
|
211
|
+
===Examples Source Code
|
212
|
+
|
213
|
+
Under the examples/camping-abingo-test you will find the full source for the CampingABingoTest app.
|
214
|
+
|
215
|
+
=More information
|
216
|
+
Check for updates :
|
217
|
+
- http://blog.monnet-usa.com
|
218
|
+
- https://github.com/techarch/camping-abingo
|
219
|
+
=end
|
220
|
+
|
221
|
+
require 'active_record'
|
222
|
+
|
223
|
+
# Main Abingo class
|
224
|
+
#
|
225
|
+
class Abingo
|
226
|
+
@@VERSION = "1.0.3"
|
227
|
+
@@MAJOR_VERSION = "1.0"
|
228
|
+
cattr_reader :VERSION
|
229
|
+
cattr_reader :MAJOR_VERSION
|
230
|
+
|
231
|
+
#Not strictly necessary, but eh, as long as I'm here.
|
232
|
+
cattr_accessor :salt
|
233
|
+
@@salt = "Not really necessary."
|
234
|
+
|
235
|
+
@@options ||= {}
|
236
|
+
cattr_accessor :options
|
237
|
+
|
238
|
+
#Defined options:
|
239
|
+
# :enable_specification => if true, allow params[test_name] to override the calculated value for a test.
|
240
|
+
# :enable_override_in_session => if true, allows session[test_name] to override the calculated value for a test.
|
241
|
+
# :expires_in => if not nil, passes expire_in to creation of per-user cache keys. Useful for Redis, to prevent expired sessions
|
242
|
+
# from running wild and consuming all of your memory.
|
243
|
+
# :count_humans_only => Count only participation and conversions from humans. Humans can be identified by calling Abingo.mark_human!
|
244
|
+
# This can be done in e.g. Javascript code, which bots will typically not execute. See FAQ for details.
|
245
|
+
# :expires_in_for_bots => if not nil, passes expire_in to creation of per-user cache keys, but only for bots.
|
246
|
+
# Only matters if :count_humans_only is on.
|
247
|
+
# :abingo_administrator_user_id => the user account id of the ABingo administrator on the site
|
248
|
+
# This is used as a primitive mechanism to control access to the ABingo dashboard
|
249
|
+
|
250
|
+
#ABingo stores whether a particular user has participated in a particular
|
251
|
+
#experiment yet, and if so whether they converted, in the cache.
|
252
|
+
#
|
253
|
+
#It is STRONGLY recommended that you use a MemcacheStore for this.
|
254
|
+
#If you'd like to persist this through a system restart or the like, you can
|
255
|
+
#look into memcachedb, which speaks the memcached protocol. From the perspective
|
256
|
+
#of Rails it is just another MemcachedStore.
|
257
|
+
#
|
258
|
+
#You can overwrite Abingo's cache instance, if you would like it to not share
|
259
|
+
#your generic Rails cache.
|
260
|
+
cattr_writer :cache
|
261
|
+
|
262
|
+
def self.cache
|
263
|
+
@@cache || ActiveSupport::Cache.lookup_store(:memory_store) # Camping-specific change to explicitly request a cache store
|
264
|
+
end
|
265
|
+
|
266
|
+
#This method gives a unique identity to a user. It can be absolutely anything
|
267
|
+
#you want, as long as it is consistent.
|
268
|
+
#
|
269
|
+
#We use the identity to determine, deterministically, which alternative a user sees.
|
270
|
+
#This means that if you use Abingo.identify_user on someone at login, they will
|
271
|
+
#always see the same alternative for a particular test which is past the login
|
272
|
+
#screen. For details and usage notes, see the docs.
|
273
|
+
def self.identity=(new_identity)
|
274
|
+
@@identity = new_identity.to_s
|
275
|
+
end
|
276
|
+
|
277
|
+
def self.identity
|
278
|
+
@@identity ||= rand(10 ** 10).to_i.to_s
|
279
|
+
end
|
280
|
+
|
281
|
+
#A simple convenience method for doing an A/B test. Returns true or false.
|
282
|
+
#If you pass it a block, it will bind the choice to the variable given to the block.
|
283
|
+
def self.flip(test_name)
|
284
|
+
if block_given?
|
285
|
+
yield(self.test(test_name, [true, false]))
|
286
|
+
else
|
287
|
+
self.test(test_name, [true, false])
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
#This is the meat of A/Bingo.
|
292
|
+
#options accepts
|
293
|
+
# :multiple_participation (true or false)
|
294
|
+
# :conversion name of conversion to listen for (alias: conversion_name)
|
295
|
+
def self.test(test_name, alternatives, options = {})
|
296
|
+
ABingoCampingPlugin.logger.debug "test #{test_name} alternatives: #{alternatives.inspect} options:#{options.inspect} for #{Abingo.identity}"
|
297
|
+
short_circuit = Abingo.cache.read("Abingo::Experiment::short_circuit(#{test_name})".gsub(" ", "_"))
|
298
|
+
unless short_circuit.nil?
|
299
|
+
return short_circuit #Test has been stopped, pick canonical alternative.
|
300
|
+
end
|
301
|
+
|
302
|
+
unless ABingoCampingPlugin::Models::Experiment.exists?(test_name) # Camping-specific
|
303
|
+
lock_key = "Abingo::lock_for_creation(#{test_name.gsub(" ", "_")})"
|
304
|
+
creation_required = true
|
305
|
+
|
306
|
+
#this prevents (most) repeated creations of experiments in high concurrency environments.
|
307
|
+
if Abingo.cache.exist?(lock_key)
|
308
|
+
creation_required = false
|
309
|
+
while Abingo.cache.exist?(lock_key)
|
310
|
+
sleep(0.1)
|
311
|
+
end
|
312
|
+
creation_required = ABingoCampingPlugin::Models::Experiment.exists?(test_name) # Camping-specific
|
313
|
+
end
|
314
|
+
|
315
|
+
if creation_required
|
316
|
+
Abingo.cache.write(lock_key, 1, :expires_in => 5.seconds)
|
317
|
+
conversion_name = options[:conversion] || options[:conversion_name]
|
318
|
+
ABingoCampingPlugin::Models::Experiment.start_experiment!(test_name, self.parse_alternatives(alternatives), conversion_name) # Camping-specific
|
319
|
+
Abingo.cache.delete(lock_key)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
choice = self.find_alternative_for_user(test_name, alternatives)
|
324
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
|
325
|
+
|
326
|
+
#Set this user to participate in this experiment, and increment participants count.
|
327
|
+
if options[:multiple_participation] || !(participating_tests.include?(test_name))
|
328
|
+
unless participating_tests.include?(test_name)
|
329
|
+
participating_tests = participating_tests + [test_name]
|
330
|
+
expires_in = Abingo.expires_in
|
331
|
+
if expires_in
|
332
|
+
Abingo.cache.write("Abingo::participating_tests::#{Abingo.identity}", participating_tests, {:expires_in => expires_in})
|
333
|
+
else
|
334
|
+
Abingo.cache.write("Abingo::participating_tests::#{Abingo.identity}", participating_tests)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
#If we're only counting known humans, then postpone scoring participation until after we know the user is human.
|
338
|
+
if (!@@options[:count_humans_only] || Abingo.is_human?)
|
339
|
+
ABingoCampingPlugin::Models::Alternative.score_participation(test_name) # Camping-specific
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
if block_given?
|
344
|
+
yield(choice)
|
345
|
+
else
|
346
|
+
choice
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
#Scores conversions for tests.
|
351
|
+
#test_name_or_array supports three types of input:
|
352
|
+
#
|
353
|
+
#A conversion name: scores a conversion for any test the user is participating in which
|
354
|
+
# is listening to the specified conversion.
|
355
|
+
#
|
356
|
+
#A test name: scores a conversion for the named test if the user is participating in it.
|
357
|
+
#
|
358
|
+
#An array of either of the above: for each element of the array, process as above.
|
359
|
+
#
|
360
|
+
#nil: score a conversion for every test the u
|
361
|
+
def Abingo.bingo!(name = nil, options = {})
|
362
|
+
if name.kind_of? Array
|
363
|
+
name.map do |single_test|
|
364
|
+
self.bingo!(single_test, options)
|
365
|
+
end
|
366
|
+
else
|
367
|
+
if name.nil?
|
368
|
+
#Score all participating tests
|
369
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
|
370
|
+
participating_tests.each do |participating_test|
|
371
|
+
self.bingo!(participating_test, options)
|
372
|
+
end
|
373
|
+
else #Could be a test name or conversion name.
|
374
|
+
conversion_name = name.gsub(" ", "_")
|
375
|
+
tests_listening_to_conversion = Abingo.cache.read("Abingo::tests_listening_to_conversion#{conversion_name}")
|
376
|
+
if tests_listening_to_conversion
|
377
|
+
if tests_listening_to_conversion.size > 1
|
378
|
+
tests_listening_to_conversion.map do |individual_test|
|
379
|
+
self.score_conversion!(individual_test.to_s)
|
380
|
+
end
|
381
|
+
elsif tests_listening_to_conversion.size == 1
|
382
|
+
test_name_str = tests_listening_to_conversion.first.to_s
|
383
|
+
self.score_conversion!(test_name_str)
|
384
|
+
end
|
385
|
+
else
|
386
|
+
#No tests listening for this conversion. Assume it is just a test name.
|
387
|
+
test_name_str = name.to_s
|
388
|
+
self.score_conversion!(test_name_str)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
|
394
|
+
def self.participating_tests(only_current = true)
|
395
|
+
identity = Abingo.identity
|
396
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{identity}") || []
|
397
|
+
tests_and_alternatives = participating_tests.inject({}) do |acc, test_name|
|
398
|
+
alternatives_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_")
|
399
|
+
alternatives = Abingo.cache.read(alternatives_key)
|
400
|
+
acc[test_name] = Abingo.find_alternative_for_user(test_name, alternatives)
|
401
|
+
acc
|
402
|
+
end
|
403
|
+
if (only_current)
|
404
|
+
tests_and_alternatives.reject! do |key, value|
|
405
|
+
self.cache.read("Abingo::Experiment::short_circuit(#{key})")
|
406
|
+
end
|
407
|
+
end
|
408
|
+
tests_and_alternatives
|
409
|
+
end
|
410
|
+
|
411
|
+
#Marks that this user is human.
|
412
|
+
def self.human!
|
413
|
+
Abingo.cache.fetch("Abingo::is_human(#{Abingo.identity})", {:expires_in => Abingo.expires_in(true)}) do
|
414
|
+
#Now that we know the user is human, score participation for all their tests. (Further participation will *not* be lazy evaluated.)
|
415
|
+
|
416
|
+
#Score all tests which have been deferred.
|
417
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
|
418
|
+
|
419
|
+
#Refresh cache expiry for this user to match that of known humans.
|
420
|
+
if (@@options[:expires_in_for_bots] && !participating_tests.blank?)
|
421
|
+
Abingo.cache.write("Abingo::participating_tests::#{Abingo.identity}", participating_tests, {:expires_in => Abingo.expires_in(true)})
|
422
|
+
end
|
423
|
+
|
424
|
+
participating_tests.each do |test_name|
|
425
|
+
Alternative.score_participation(test_name)
|
426
|
+
if conversions = Abingo.cache.read("Abingo::conversions(#{Abingo.identity},#{test_name}")
|
427
|
+
conversions.times { Alternative.score_conversion(test_name) }
|
428
|
+
end
|
429
|
+
end
|
430
|
+
true #Marks this user as human in the cache.
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
protected
|
435
|
+
|
436
|
+
def self.is_human?
|
437
|
+
!!Abingo.cache.read("Abingo::is_human(#{Abingo.identity})")
|
438
|
+
end
|
439
|
+
|
440
|
+
#For programmer convenience, we allow you to specify what the alternatives for
|
441
|
+
#an experiment are in a few ways. Thus, we need to actually be able to handle
|
442
|
+
#all of them. We fire this parser very infrequently (once per test, typically)
|
443
|
+
#so it can be as complicated as we want.
|
444
|
+
# Integer => a number 1 through N
|
445
|
+
# Range => a number within the range
|
446
|
+
# Array => an element of the array.
|
447
|
+
# Hash => assumes a hash of something to int. We pick one of the
|
448
|
+
# somethings, weighted accorded to the ints provided. e.g.
|
449
|
+
# {:a => 2, :b => 3} produces :a 40% of the time, :b 60%.
|
450
|
+
#
|
451
|
+
#Alternatives are always represented internally as an array.
|
452
|
+
def self.parse_alternatives(alternatives)
|
453
|
+
if alternatives.kind_of? Array
|
454
|
+
return alternatives
|
455
|
+
elsif alternatives.kind_of? Integer
|
456
|
+
return (1..alternatives).to_a
|
457
|
+
elsif alternatives.kind_of? Range
|
458
|
+
return alternatives.to_a
|
459
|
+
elsif alternatives.kind_of? Hash
|
460
|
+
alternatives_array = []
|
461
|
+
alternatives.each do |key, value|
|
462
|
+
if value.kind_of? Integer
|
463
|
+
alternatives_array += [key] * value
|
464
|
+
else
|
465
|
+
raise "You gave a hash with #{key} => #{value} as an element. The value must be an integral weight."
|
466
|
+
end
|
467
|
+
end
|
468
|
+
return alternatives_array
|
469
|
+
else
|
470
|
+
raise "I don't know how to turn [#{alternatives}] into an array of alternatives."
|
471
|
+
end
|
472
|
+
end
|
473
|
+
|
474
|
+
def self.retrieve_alternatives(test_name, alternatives)
|
475
|
+
cache_key = "Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_")
|
476
|
+
alternative_array = self.cache.fetch(cache_key) do
|
477
|
+
self.parse_alternatives(alternatives)
|
478
|
+
end
|
479
|
+
alternative_array
|
480
|
+
end
|
481
|
+
|
482
|
+
def self.find_alternative_for_user(test_name, alternatives)
|
483
|
+
alternatives_array = retrieve_alternatives(test_name, alternatives)
|
484
|
+
selected_alternative = alternatives_array[self.modulo_choice(test_name, alternatives_array.size)]
|
485
|
+
ABingoCampingPlugin.logger.debug "find_alternative_for_user(#{test_name}, #{alternatives}, #{self.identity}) > #{selected_alternative}"
|
486
|
+
selected_alternative
|
487
|
+
end
|
488
|
+
|
489
|
+
#Quickly determines what alternative to show a given user. Given a test name
|
490
|
+
#and their identity, we hash them together (which, for MD5, provably introduces
|
491
|
+
#enough entropy that we don't care) otherwise
|
492
|
+
def self.modulo_choice(test_name, choices_count)
|
493
|
+
Digest::MD5.hexdigest(Abingo.salt.to_s + test_name + self.identity.to_s).to_i(16) % choices_count
|
494
|
+
end
|
495
|
+
|
496
|
+
def self.score_conversion!(test_name)
|
497
|
+
test_name.gsub!(" ", "_")
|
498
|
+
participating_tests = Abingo.cache.read("Abingo::participating_tests::#{Abingo.identity}") || []
|
499
|
+
ABingoCampingPlugin.logger.debug "score_conversion (#{test_name}) participating_tests=#{participating_tests.inspect} flag=#{participating_tests.include?(test_name)} options=#{options.inspect}"
|
500
|
+
|
501
|
+
if options[:assume_participation] || participating_tests.include?(test_name)
|
502
|
+
cache_key = "Abingo::conversions(#{Abingo.identity},#{test_name}"
|
503
|
+
ABingoCampingPlugin.logger.debug "score_conversion cache_key=#{cache_key}"
|
504
|
+
if options[:multiple_conversions] || !Abingo.cache.read(cache_key)
|
505
|
+
ABingoCampingPlugin.logger.debug "score_conversion is human=#{!options[:count_humans_only] || Abingo.is_human?}"
|
506
|
+
if !options[:count_humans_only] || Abingo.is_human?
|
507
|
+
ABingoCampingPlugin::Models::Alternative.score_conversion(test_name) # Camping-specific
|
508
|
+
end
|
509
|
+
|
510
|
+
if Abingo.cache.exist?(cache_key)
|
511
|
+
ABingoCampingPlugin.logger.debug "score_conversion increment #{cache_key}"
|
512
|
+
Abingo.cache.increment(cache_key)
|
513
|
+
else
|
514
|
+
ABingoCampingPlugin.logger.debug "score_conversion write #{cache_key}"
|
515
|
+
Abingo.cache.write(cache_key, 1)
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
def self.expires_in(known_human = false)
|
522
|
+
expires_in = nil
|
523
|
+
if (@@options[:expires_in])
|
524
|
+
expires_in = @@options[:expires_in]
|
525
|
+
end
|
526
|
+
if (@@options[:count_humans_only] && @@options[:expires_in_for_bots] && !(known_human || Abingo.is_human?))
|
527
|
+
expires_in = @@options[:expires_in_for_bots]
|
528
|
+
end
|
529
|
+
expires_in
|
530
|
+
end
|
531
|
+
|
532
|
+
|
533
|
+
end # Abingo class
|
534
|
+
|
535
|
+
# Main module for the ABingo Camping Plugin
|
536
|
+
#
|
537
|
+
module ABingoCampingPlugin
|
538
|
+
@@logger = nil
|
539
|
+
|
540
|
+
# Logger for the ABingoCampingPlugin - can be assigned the main logger for the main web app
|
541
|
+
def self.logger
|
542
|
+
@@logger
|
543
|
+
end
|
544
|
+
|
545
|
+
def self.logger=(a_logger)
|
546
|
+
@@logger = a_logger
|
547
|
+
end
|
548
|
+
|
549
|
+
# Provides a hook to initialize the plugin in the context of the main web app module
|
550
|
+
def self.create
|
551
|
+
#Abingo.cache = ActiveSupport::Cache.lookup_store(:file_store, File.dirname(__FILE__) + "/cache")
|
552
|
+
Abingo.cache = ActiveSupport::Cache.lookup_store(:memory_store)
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
# Helpers module for ABingo Camping Plugin.
|
557
|
+
# The module will be plugged in to the main app Helpers module.
|
558
|
+
# Its methods will be added to Controllers and Views.
|
559
|
+
# Example:
|
560
|
+
# module CampingABingoTest::Helpers
|
561
|
+
# extend ABingoCampingPlugin::Helpers
|
562
|
+
# include ABingoCampingPlugin::Helpers
|
563
|
+
# end
|
564
|
+
#
|
565
|
+
|
566
|
+
module ABingoCampingPlugin::Helpers
|
567
|
+
|
568
|
+
# Logs a specific message if in debug mode
|
569
|
+
def log_debug(msg)
|
570
|
+
ABingoCampingPlugin.logger.debug(msg) if ABingoCampingPlugin.logger && ABingoCampingPlugin.logger.debug?
|
571
|
+
end
|
572
|
+
|
573
|
+
# Reverse engineers the main app module
|
574
|
+
def app_module
|
575
|
+
app_module_name = self.class.to_s.split("::").first
|
576
|
+
app_module = app_module_name.constantize
|
577
|
+
end
|
578
|
+
|
579
|
+
# --- ConversionRate ------------------------------
|
580
|
+
def conversion_rate
|
581
|
+
1.0 * conversions / participants
|
582
|
+
end
|
583
|
+
|
584
|
+
def pretty_conversion_rate
|
585
|
+
sprintf("%4.2f%%", conversion_rate * 100)
|
586
|
+
end
|
587
|
+
|
588
|
+
# --- Statistics --------------------------
|
589
|
+
HANDY_Z_SCORE_CHEATSHEET = [[0.10, 1.29], [0.05, 1.65], [0.01, 2.33], [0.001, 3.08]]
|
590
|
+
|
591
|
+
PERCENTAGES = {0.10 => '90%', 0.05 => '95%', 0.01 => '99%', 0.001 => '99.9%'}
|
592
|
+
|
593
|
+
DESCRIPTION_IN_WORDS = {0.10 => 'fairly confident', 0.05 => 'confident',
|
594
|
+
0.01 => 'very confident', 0.001 => 'extremely confident'}
|
595
|
+
def zscore
|
596
|
+
if alternatives.size != 2
|
597
|
+
raise "Sorry, can't currently automatically calculate statistics for A/B tests with > 2 alternatives."
|
598
|
+
end
|
599
|
+
|
600
|
+
if (alternatives[0].participants == 0) || (alternatives[1].participants == 0)
|
601
|
+
raise "Can't calculate the z score if either of the alternatives lacks participants."
|
602
|
+
end
|
603
|
+
|
604
|
+
cr1 = alternatives[0].conversion_rate
|
605
|
+
cr2 = alternatives[1].conversion_rate
|
606
|
+
|
607
|
+
n1 = alternatives[0].participants
|
608
|
+
n2 = alternatives[1].participants
|
609
|
+
|
610
|
+
numerator = cr1 - cr2
|
611
|
+
frac1 = cr1 * (1 - cr1) / n1
|
612
|
+
frac2 = cr2 * (1 - cr2) / n2
|
613
|
+
|
614
|
+
numerator / ((frac1 + frac2) ** 0.5)
|
615
|
+
end
|
616
|
+
|
617
|
+
def p_value
|
618
|
+
index = 0
|
619
|
+
z = zscore
|
620
|
+
z = z.abs
|
621
|
+
found_p = nil
|
622
|
+
while index < HANDY_Z_SCORE_CHEATSHEET.size do
|
623
|
+
if (z > HANDY_Z_SCORE_CHEATSHEET[index][1])
|
624
|
+
found_p = HANDY_Z_SCORE_CHEATSHEET[index][0]
|
625
|
+
end
|
626
|
+
index += 1
|
627
|
+
end
|
628
|
+
found_p
|
629
|
+
end
|
630
|
+
|
631
|
+
def is_statistically_significant?(p = 0.05)
|
632
|
+
p_value <= p
|
633
|
+
end
|
634
|
+
|
635
|
+
def describe_result_in_words
|
636
|
+
begin
|
637
|
+
z = zscore
|
638
|
+
rescue
|
639
|
+
return "Could not execute the significance test because one or more of the alternatives has not been seen yet."
|
640
|
+
end
|
641
|
+
p = p_value
|
642
|
+
|
643
|
+
words = ""
|
644
|
+
if (alternatives[0].participants < 10) || (alternatives[1].participants < 10)
|
645
|
+
words += "Take these results with a grain of salt since your samples are so small: "
|
646
|
+
end
|
647
|
+
|
648
|
+
alts = alternatives - [best_alternative]
|
649
|
+
worst_alternative = alts.first
|
650
|
+
|
651
|
+
words += "The best alternative you have is: [#{best_alternative.content}], which had "
|
652
|
+
words += "#{best_alternative.conversions} conversions from #{best_alternative.participants} participants "
|
653
|
+
words += "(#{best_alternative.pretty_conversion_rate}). The other alternative was [#{worst_alternative.content}], "
|
654
|
+
words += "which had #{worst_alternative.conversions} conversions from #{worst_alternative.participants} participants "
|
655
|
+
words += "(#{worst_alternative.pretty_conversion_rate}). "
|
656
|
+
|
657
|
+
if (p.nil?)
|
658
|
+
words += "However, this difference is not statistically significant."
|
659
|
+
else
|
660
|
+
words += "This difference is #{PERCENTAGES[p]} likely to be statistically significant, which means you can be "
|
661
|
+
words += "#{DESCRIPTION_IN_WORDS[p]} that it is the result of your alternatives actually mattering, rather than "
|
662
|
+
words += "being due to random chance. However, this statistical test can't measure how likely the currently "
|
663
|
+
words += "observed magnitude of the difference is to be accurate or not. It only says \"better\", not \"better "
|
664
|
+
words += "by so much\"."
|
665
|
+
end
|
666
|
+
words
|
667
|
+
end
|
668
|
+
|
669
|
+
# Controller Helpers
|
670
|
+
|
671
|
+
def ab_test(test_name, alternatives = nil, options = {})
|
672
|
+
if (Abingo.options[:enable_specification] && !@input.test_name.nil?) # Camping-specific
|
673
|
+
choice = @input.test_name # Camping-specific
|
674
|
+
elsif (Abingo.options[:enable_override_in_session] && !@state.test_name.nil?) # Camping-specific
|
675
|
+
choice = @state.test_name # Camping-specific
|
676
|
+
elsif (alternatives.nil?)
|
677
|
+
choice = Abingo.flip(test_name)
|
678
|
+
else
|
679
|
+
choice = Abingo.test(test_name, alternatives, options)
|
680
|
+
end
|
681
|
+
|
682
|
+
if block_given?
|
683
|
+
yield(choice)
|
684
|
+
else
|
685
|
+
choice
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
def bingo!(test_name, options = {})
|
690
|
+
Abingo.bingo!(test_name, options)
|
691
|
+
end
|
692
|
+
|
693
|
+
# Filter Helpers
|
694
|
+
def set_abingo_identity # Camping-specific
|
695
|
+
if @user
|
696
|
+
Abingo.identity = @user.id
|
697
|
+
else
|
698
|
+
if @state.abingo_identity
|
699
|
+
Abingo.identity = @state.abingo_identity
|
700
|
+
else
|
701
|
+
@state.abingo_identity = Abingo.identity = rand(10 ** 10).to_i
|
702
|
+
end
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
def authenticate_abingo_administrator
|
707
|
+
return true if !@state.nil? && !@state.user_id.nil? && @state.user_id == abingo_administrator_user_id
|
708
|
+
redirect('/abingo/restricted_access')
|
709
|
+
return false
|
710
|
+
end
|
711
|
+
|
712
|
+
def abingo_administrator_user_id
|
713
|
+
Abingo.options[:abingo_administrator_user_id] || 1
|
714
|
+
end
|
715
|
+
|
716
|
+
end #ABingoCampingPlugin::Helpers
|
717
|
+
|
718
|
+
# Filters module for OAuth Camping Plugin.
|
719
|
+
# The module will be plugged in to the main app Helpers module.
|
720
|
+
# Example:
|
721
|
+
# module CampingOAuthProvider
|
722
|
+
# include Camping::Session
|
723
|
+
# include CampingFilters
|
724
|
+
# extend OAuthCampingPlugin
|
725
|
+
# include OAuthCampingPlugin::Filters
|
726
|
+
#
|
727
|
+
# # ...
|
728
|
+
# end
|
729
|
+
#
|
730
|
+
module ABingoCampingPlugin::Filters
|
731
|
+
# Adds a before filters for the common controllers:
|
732
|
+
# - ABingoDashboard
|
733
|
+
# Also adds a before filter on all controllers to ensure the user is set
|
734
|
+
def self.included(mod)
|
735
|
+
mod.module_eval do
|
736
|
+
before :all do
|
737
|
+
set_abingo_identity
|
738
|
+
end
|
739
|
+
|
740
|
+
before :ABingoDashboard do
|
741
|
+
authenticate_abingo_administrator
|
742
|
+
end
|
743
|
+
end
|
744
|
+
end
|
745
|
+
end # ABingoCampingPlugin::Filters
|
746
|
+
|
747
|
+
|
748
|
+
# ABingo module for ABingo Camping Plugin.
|
749
|
+
# The module will be plugged into all controllers either:
|
750
|
+
# - directly such as in the standard common ABingo controllers (e.g. ABingoProvideRequestToken)
|
751
|
+
# - or indirectly via the include_abingo_controllers of the ABingoCampingPlugin::Controllers module
|
752
|
+
# The module provides accessors, helper, authentication, signing, and authorization methods specific to ABingo
|
753
|
+
#
|
754
|
+
module ABingoCampingPlugin::ABingo
|
755
|
+
|
756
|
+
#protected
|
757
|
+
|
758
|
+
|
759
|
+
end
|
760
|
+
|
761
|
+
# Models module for the ABingo Camping Plugin.
|
762
|
+
# The module will be plugged in to the main app models module.
|
763
|
+
# Example:
|
764
|
+
# module CampingABingoTest::Models
|
765
|
+
# include ABingoCampingPlugin::Models
|
766
|
+
#
|
767
|
+
# class User < Base;
|
768
|
+
# has_many :client_applications
|
769
|
+
# has_many :tokens, :class_name=>"OauthToken",:order=>"authorized_at desc",:include=>[:client_application]
|
770
|
+
#
|
771
|
+
# end
|
772
|
+
# # ...
|
773
|
+
# end
|
774
|
+
#
|
775
|
+
# This module requires the abingo-plugin gem to be installed as it will load the following models
|
776
|
+
# - Experiment
|
777
|
+
# - Alternative
|
778
|
+
#
|
779
|
+
module ABingoCampingPlugin::Models
|
780
|
+
|
781
|
+
# --- Experiment ---------------------
|
782
|
+
class Experiment < Camping::Models::Base
|
783
|
+
include ABingoCampingPlugin::Helpers
|
784
|
+
|
785
|
+
has_many :alternatives, :dependent => :destroy, :class_name => "Alternative"
|
786
|
+
validates_uniqueness_of :test_name
|
787
|
+
|
788
|
+
def cache_keys
|
789
|
+
[" Abingo::Experiment::exists(#{test_name})".gsub(" ", "_"),
|
790
|
+
"Abingo::Experiment::#{test_name}::alternatives".gsub(" ","_"),
|
791
|
+
"Abingo::Experiment::short_circuit(#{test_name})".gsub(" ", "_")
|
792
|
+
]
|
793
|
+
end
|
794
|
+
|
795
|
+
def before_destroy
|
796
|
+
cache_keys.each do |key|
|
797
|
+
Abingo.cache.delete key
|
798
|
+
end
|
799
|
+
true
|
800
|
+
end
|
801
|
+
|
802
|
+
def participants
|
803
|
+
alternatives.sum("participants")
|
804
|
+
end
|
805
|
+
|
806
|
+
def conversions
|
807
|
+
alternatives.sum("conversions")
|
808
|
+
end
|
809
|
+
|
810
|
+
def best_alternative
|
811
|
+
alternatives.max do |a,b|
|
812
|
+
a.conversion_rate <=> b.conversion_rate
|
813
|
+
end
|
814
|
+
end
|
815
|
+
|
816
|
+
def self.exists?(test_name)
|
817
|
+
cache_key = "Abingo::Experiment::exists(#{test_name})".gsub(" ", "_")
|
818
|
+
ret = Abingo.cache.fetch(cache_key) do
|
819
|
+
count = ABingoCampingPlugin::Models::Experiment.count(:conditions => {:test_name => test_name})
|
820
|
+
count > 0 ? count : nil
|
821
|
+
end
|
822
|
+
(!ret.nil?)
|
823
|
+
end
|
824
|
+
|
825
|
+
def self.alternatives_for_test(test_name)
|
826
|
+
cache_key = "Abingo::#{test_name}::alternatives".gsub(" ","_")
|
827
|
+
Abingo.cache.fetch(cache_key) do
|
828
|
+
experiment = ABingoCampingPlugin::Models::Experiment.find_by_test_name(test_name)
|
829
|
+
alternatives_array = Abingo.cache.fetch(cache_key) do
|
830
|
+
tmp_array = experiment.alternatives.map do |alt|
|
831
|
+
[alt.content, alt.weight]
|
832
|
+
end
|
833
|
+
tmp_hash = tmp_array.inject({}) {|hash, couplet| hash[couplet[0]] = couplet[1]; hash}
|
834
|
+
Abingo.parse_alternatives(tmp_hash)
|
835
|
+
end
|
836
|
+
alternatives_array
|
837
|
+
end
|
838
|
+
end
|
839
|
+
|
840
|
+
def self.start_experiment!(test_name, alternatives_array, conversion_name = nil)
|
841
|
+
ABingoCampingPlugin.logger.debug "Experiment> start_experiment(test_name=#{test_name}, alternatives_array=#{alternatives_array.inspect}, conversion_name=#{conversion_name})"
|
842
|
+
|
843
|
+
conversion_name ||= test_name
|
844
|
+
conversion_name.gsub!(" ", "_")
|
845
|
+
cloned_alternatives_array = alternatives_array.clone
|
846
|
+
ActiveRecord::Base.transaction do
|
847
|
+
experiment = ABingoCampingPlugin::Models::Experiment.find_or_create_by_test_name(test_name)
|
848
|
+
experiment.alternatives.destroy_all #Blows away alternatives for pre-existing experiments.
|
849
|
+
while (cloned_alternatives_array.size > 0)
|
850
|
+
alt = cloned_alternatives_array[0]
|
851
|
+
|
852
|
+
if alt.is_a?(TrueClass) || alt.is_a?(FalseClass)
|
853
|
+
alt_to_store = alt.to_s
|
854
|
+
else
|
855
|
+
alt_to_store = alt
|
856
|
+
end
|
857
|
+
|
858
|
+
weight = cloned_alternatives_array.size - (cloned_alternatives_array - [alt]).size
|
859
|
+
experiment.alternatives.build(:content => alt_to_store, :weight => weight,
|
860
|
+
:lookup => ABingoCampingPlugin::Models::Alternative.calculate_lookup(test_name, alt_to_store))
|
861
|
+
cloned_alternatives_array -= [alt]
|
862
|
+
end
|
863
|
+
experiment.status = "Live"
|
864
|
+
experiment.save(false) #Calling the validation here causes problems b/c of transaction.
|
865
|
+
Abingo.cache.write("Abingo::Experiment::exists(#{test_name})".gsub(" ", "_"), 1)
|
866
|
+
|
867
|
+
#This might have issues in very, very high concurrency environments...
|
868
|
+
|
869
|
+
tests_listening_to_conversion = Abingo.cache.read("Abingo::tests_listening_to_conversion#{conversion_name}") || []
|
870
|
+
tests_listening_to_conversion << test_name unless tests_listening_to_conversion.include? test_name
|
871
|
+
Abingo.cache.write("Abingo::tests_listening_to_conversion#{conversion_name}", tests_listening_to_conversion)
|
872
|
+
experiment
|
873
|
+
end
|
874
|
+
end
|
875
|
+
|
876
|
+
def end_experiment!(final_alternative, conversion_name = nil)
|
877
|
+
ABingoCampingPlugin.logger.debug "Experiment> end_experiment(final_alternative=#{final_alternative}, conversion_name=#{conversion_name})"
|
878
|
+
|
879
|
+
conversion_name ||= test_name
|
880
|
+
ActiveRecord::Base.transaction do
|
881
|
+
alternatives.each do |alternative|
|
882
|
+
alternative.lookup = "Experiment completed. #{alternative.id}"
|
883
|
+
alternative.save!
|
884
|
+
end
|
885
|
+
update_attribute(:status, "Finished")
|
886
|
+
Abingo.cache.write("Abingo::Experiment::short_circuit(#{test_name})".gsub(" ", "_"), final_alternative)
|
887
|
+
end
|
888
|
+
end
|
889
|
+
|
890
|
+
end
|
891
|
+
|
892
|
+
# --- Alternative ---------------------
|
893
|
+
class Alternative < Camping::Models::Base
|
894
|
+
include ABingoCampingPlugin::Helpers
|
895
|
+
|
896
|
+
belongs_to :experiment, :class_name => "Experiment"
|
897
|
+
serialize :content
|
898
|
+
|
899
|
+
def self.calculate_lookup(test_name, alternative_name)
|
900
|
+
digest = Digest::MD5.hexdigest(Abingo.salt + test_name + alternative_name.to_s)
|
901
|
+
ABingoCampingPlugin.logger.debug "calculate_lookup #{test_name} , #{alternative_name} > #{digest}"
|
902
|
+
digest
|
903
|
+
end
|
904
|
+
|
905
|
+
def self.score_conversion(test_name)
|
906
|
+
viewed_alternative = Abingo.find_alternative_for_user(test_name,
|
907
|
+
ABingoCampingPlugin::Models::Experiment.alternatives_for_test(test_name))
|
908
|
+
ABingoCampingPlugin.logger.debug "Alternative> score_conversion #{test_name} > #{viewed_alternative}"
|
909
|
+
self.update_all("conversions = conversions + 1", :lookup => self.calculate_lookup(test_name, viewed_alternative))
|
910
|
+
end
|
911
|
+
|
912
|
+
def self.score_participation(test_name)
|
913
|
+
viewed_alternative = Abingo.find_alternative_for_user(test_name,
|
914
|
+
ABingoCampingPlugin::Models::Experiment.alternatives_for_test(test_name))
|
915
|
+
ABingoCampingPlugin.logger.debug "Alternative>score_participation #{test_name} > #{viewed_alternative}"
|
916
|
+
self.update_all("participants = participants + 1", :lookup => self.calculate_lookup(test_name, viewed_alternative))
|
917
|
+
end
|
918
|
+
|
919
|
+
end #Abingo::Alternative
|
920
|
+
|
921
|
+
# --- Migrations --------------------
|
922
|
+
|
923
|
+
# Loads the 5 standard ABingo models defined in the abingo-plugin gem
|
924
|
+
def self.included(mod)
|
925
|
+
# @techarch : Reset the table names back to pre-Camping
|
926
|
+
mod.module_eval do
|
927
|
+
mod::Experiment.class_eval { set_table_name "abingo_experiments" }
|
928
|
+
mod::Alternative.class_eval { set_table_name "abingo_alternatives" }
|
929
|
+
end
|
930
|
+
end
|
931
|
+
|
932
|
+
# Up-migrates the schema definition for the 5 ABingo models
|
933
|
+
def self.up
|
934
|
+
ActiveRecord::Schema.define do
|
935
|
+
create_table "abingo_experiments", :force => true do |t|
|
936
|
+
t.string "test_name"
|
937
|
+
t.string "status"
|
938
|
+
t.timestamps
|
939
|
+
end
|
940
|
+
|
941
|
+
add_index "abingo_experiments", "test_name"
|
942
|
+
#add_index "experiments", "created_on"
|
943
|
+
|
944
|
+
create_table "abingo_alternatives", :force => true do |t|
|
945
|
+
t.integer :experiment_id
|
946
|
+
t.string :content
|
947
|
+
t.string :lookup, :limit => 32
|
948
|
+
t.integer :weight, :default => 1
|
949
|
+
t.integer :participants, :default => 0
|
950
|
+
t.integer :conversions, :default => 0
|
951
|
+
end
|
952
|
+
|
953
|
+
add_index "abingo_alternatives", "experiment_id"
|
954
|
+
add_index "abingo_alternatives", "lookup" #Critical for speed, since we'll primarily be updating by that.
|
955
|
+
end
|
956
|
+
end
|
957
|
+
|
958
|
+
# Down-migrates the schema definition for the 2 ABingo models
|
959
|
+
def self.down
|
960
|
+
ActiveRecord::Schema.define do
|
961
|
+
drop_table :abingo_experiments
|
962
|
+
drop_table :abingo_alternatives
|
963
|
+
end
|
964
|
+
end
|
965
|
+
|
966
|
+
end
|
967
|
+
|
968
|
+
# Controllers module for the ABingo Camping Plugin.
|
969
|
+
# The module will be plugged in to the main app controllers module using:
|
970
|
+
# - extend to add class methods to the app controllers module
|
971
|
+
# - include_abingo_controllers to dynamically plugin the ABingo and Helpers modules inside each controller class
|
972
|
+
# (this is why the call must be the last statement in the controllers module)
|
973
|
+
#
|
974
|
+
# Example:
|
975
|
+
# module CampingABingoTest::Controllers
|
976
|
+
# extend ABingoCampingPlugin::Controllers
|
977
|
+
#
|
978
|
+
# # ...
|
979
|
+
#
|
980
|
+
# include_abingo_controllers
|
981
|
+
# end
|
982
|
+
#
|
983
|
+
module ABingoCampingPlugin::Controllers
|
984
|
+
|
985
|
+
# Returns the source code for all common ABingo controllers
|
986
|
+
def self.common_abingo_controllers
|
987
|
+
<<-CLASS_DEFS
|
988
|
+
class ABingoMarkHuman < R '/abingo/mark_human'
|
989
|
+
include ABingoCampingPlugin::ABingo
|
990
|
+
include ABingoCampingPlugin::Helpers
|
991
|
+
|
992
|
+
def post
|
993
|
+
textual_result = "1"
|
994
|
+
begin
|
995
|
+
a = @input.a.to_i
|
996
|
+
b = @input.b.to_i
|
997
|
+
c = @input.c.to_i
|
998
|
+
if (@env['REQUEST_METHOD'] == 'POST' && (a + b == c))
|
999
|
+
Abingo.human!
|
1000
|
+
else
|
1001
|
+
textual_result = "0"
|
1002
|
+
end
|
1003
|
+
rescue #If a bot doesn't pass a, b, or c, to_i will fail. This scarfs up the exception, to save it from polluting our logs.
|
1004
|
+
textual_result = "0"
|
1005
|
+
end
|
1006
|
+
|
1007
|
+
return textual_result
|
1008
|
+
end
|
1009
|
+
end
|
1010
|
+
|
1011
|
+
class ABingoDashboard < R '/abingo/dashboard'
|
1012
|
+
include ABingoCampingPlugin::ABingo
|
1013
|
+
include ABingoCampingPlugin::Helpers
|
1014
|
+
|
1015
|
+
def get
|
1016
|
+
@experiments = ABingoCampingPlugin::Models::Experiment.all
|
1017
|
+
render :abingo_dashboard
|
1018
|
+
end
|
1019
|
+
end
|
1020
|
+
|
1021
|
+
class ABingoTerminateExperiment < R '/abingo/terminate'
|
1022
|
+
include ABingoCampingPlugin::ABingo
|
1023
|
+
include ABingoCampingPlugin::Helpers
|
1024
|
+
|
1025
|
+
def post
|
1026
|
+
return(:abingo_dashboard) unless @input.alternative_id
|
1027
|
+
|
1028
|
+
@alternative = ABingoCampingPlugin::Models::Alternative.find(@input.alternative_id)
|
1029
|
+
@experiment = ABingoCampingPlugin::Models::Experiment.find(@alternative.experiment_id)
|
1030
|
+
experiment_name = @experiment.test_name
|
1031
|
+
|
1032
|
+
if (@experiment.status != "Completed")
|
1033
|
+
@experiment.end_experiment!(@alternative.content)
|
1034
|
+
@abingo_notice = "Experiment '" + experiment_name + "' has been marked as ended. All users will now see the chosen alternative."
|
1035
|
+
else
|
1036
|
+
@abingo_notice = "Experiment '" + experiment_name + "' is already ended."
|
1037
|
+
end
|
1038
|
+
|
1039
|
+
render :abingo_termination_notice
|
1040
|
+
end
|
1041
|
+
end
|
1042
|
+
|
1043
|
+
class ABingoRestrictedAccess < R '/abingo/restricted_access'
|
1044
|
+
def get
|
1045
|
+
render :abingo_dashboard_restricted_access
|
1046
|
+
end
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
CLASS_DEFS
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
# Includes the ABingo and Helpers modules inside each controller class using class_eval
|
1053
|
+
# (this is why the call must be the last statement in the controllers module)
|
1054
|
+
def include_abingo_controllers
|
1055
|
+
module_eval ABingoCampingPlugin::Controllers.common_abingo_controllers
|
1056
|
+
|
1057
|
+
# Add ABing to each controller
|
1058
|
+
r.each do |x|
|
1059
|
+
x.class_eval do
|
1060
|
+
include ABingoCampingPlugin::ABingo
|
1061
|
+
include ABingoCampingPlugin::Helpers
|
1062
|
+
end
|
1063
|
+
end
|
1064
|
+
end
|
1065
|
+
end
|
1066
|
+
|
1067
|
+
# Views module for the ABingo Camping Plugin.
|
1068
|
+
# The module will be plugged in to the main app views module using:
|
1069
|
+
# - extend to add class methods to the app views module
|
1070
|
+
# - include_abingo_views to dynamically plugin the common ABingo views (e.g. authorize_view)
|
1071
|
+
#
|
1072
|
+
# Example:
|
1073
|
+
# module CampingABingoTest::Views
|
1074
|
+
# extend ABingoCampingPlugin::Views
|
1075
|
+
#
|
1076
|
+
# # ...
|
1077
|
+
#
|
1078
|
+
# include_abingo_views
|
1079
|
+
# end
|
1080
|
+
#
|
1081
|
+
module ABingoCampingPlugin::Views
|
1082
|
+
def self.abingo_view_helpers
|
1083
|
+
<<-VIEW_HELPERS
|
1084
|
+
|
1085
|
+
def ab_test(test_name, alternatives = nil, options = {}, &block)
|
1086
|
+
|
1087
|
+
if (Abingo.options[:enable_specification] && !params[test_name].nil?)
|
1088
|
+
choice = params[test_name]
|
1089
|
+
elsif (Abingo.options[:enable_override_in_session] && !session[test_name].nil?)
|
1090
|
+
choice = session[test_name]
|
1091
|
+
elsif (alternatives.nil?)
|
1092
|
+
choice = Abingo.flip(test_name)
|
1093
|
+
else
|
1094
|
+
choice = Abingo.test(test_name, alternatives, options)
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
if block
|
1098
|
+
content_tag = capture(choice, &block)
|
1099
|
+
block_called_from_erb?(block) ? concat(content_tag) : content_tag
|
1100
|
+
else
|
1101
|
+
choice
|
1102
|
+
end
|
1103
|
+
end
|
1104
|
+
|
1105
|
+
def bingo!(test_name, options = {})
|
1106
|
+
Abingo.bingo!(test_name, options)
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
#This causes an AJAX post against the URL. That URL should call Abingo.human!
|
1110
|
+
#This guarantees that anyone calling Abingo.human! is capable of at least minimal Javascript execution, and thus is (probably) not a robot.
|
1111
|
+
def include_humanizing_javascript(url = "/abingo/mark_human", style = :jquery) # Camping-specific
|
1112
|
+
ajax_call_script = nil
|
1113
|
+
if (style == :prototype)
|
1114
|
+
ajax_call_script = "var a=Math.floor(Math.random()*11); var b=Math.floor(Math.random()*11);var x=new Ajax.Request('" + url + "', {parameters:{a: a, b: b, c: a+b}})"
|
1115
|
+
elsif (style == :jquery)
|
1116
|
+
ajax_call_script = "var a=Math.floor(Math.random()*11); var b=Math.floor(Math.random()*11);var x=jQuery.post('" + url + "', {a: a, b: b, c: a+b})"
|
1117
|
+
end
|
1118
|
+
#ajax_call_script.nil? ? "" : "<script type='text/javascript'>" + ajax_call_script + "</script>"
|
1119
|
+
|
1120
|
+
return unless !ajax_call_script.nil?
|
1121
|
+
|
1122
|
+
script :type=> 'text/javascript' do
|
1123
|
+
%Q|ajax_call_script|
|
1124
|
+
end
|
1125
|
+
|
1126
|
+
end
|
1127
|
+
|
1128
|
+
VIEW_HELPERS
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
# Returns the source code for all common ABingo views such as error views (e.g. authorize_failure)
|
1132
|
+
def self.common_abingo_views
|
1133
|
+
<<-VIEW_DEFS
|
1134
|
+
|
1135
|
+
def abingo_dashboard
|
1136
|
+
div.abingo_dashboard! do
|
1137
|
+
h1 "ABingo All Experiments"
|
1138
|
+
|
1139
|
+
@experiments.each do | experiment |
|
1140
|
+
abingo_experiment(experiment)
|
1141
|
+
end
|
1142
|
+
end
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
def abingo_experiment(experiment)
|
1146
|
+
short_circuit = ""
|
1147
|
+
Abingo.cache.read("ABingoCampingPlugin::Models::Experiment::short_circuit("+experiment.test_name+")".gsub(" ", ""))
|
1148
|
+
|
1149
|
+
h2 { span { experiment.id.to_s + ' - ' + experiment.test_name.titleize }
|
1150
|
+
span("[Completed]") if experiment.status != "Live"
|
1151
|
+
}
|
1152
|
+
|
1153
|
+
table.abingo_experiment_table :style=>"" do
|
1154
|
+
tr { th {"Name"};
|
1155
|
+
th {"Participants"};
|
1156
|
+
th {"Conversions"};
|
1157
|
+
th {"Notes" }
|
1158
|
+
}
|
1159
|
+
|
1160
|
+
tr.abingo_experiment_row { td {"Experiment Total: "};
|
1161
|
+
td { experiment.participants.to_s };
|
1162
|
+
td { experiment.conversions.to_s + '(' + experiment.pretty_conversion_rate + ')' };
|
1163
|
+
td { };
|
1164
|
+
}
|
1165
|
+
|
1166
|
+
experiment.alternatives.each do | alternative |
|
1167
|
+
tr.abingo_alternative_row { td { h(alternative.content) };
|
1168
|
+
td { alternative.participants.to_s };
|
1169
|
+
td { alternative.conversions.to_s + '(' + alternative.pretty_conversion_rate + ')' };
|
1170
|
+
td {
|
1171
|
+
unless experiment.status != "Live"
|
1172
|
+
|
1173
|
+
onclickfn = <<-JAVASCRIPT
|
1174
|
+
if (confirm('Are you sure you want to terminate this experiment? This is not reversible.')) {
|
1175
|
+
var f = document.createElement('form');
|
1176
|
+
f.style.display = 'none';
|
1177
|
+
this.parentNode.appendChild(f);
|
1178
|
+
f.method = 'POST';
|
1179
|
+
f.action = this.href;
|
1180
|
+
f.submit();
|
1181
|
+
};
|
1182
|
+
return false;
|
1183
|
+
JAVASCRIPT
|
1184
|
+
|
1185
|
+
a "Terminate this Alternative", :onclick=>onclickfn, :href=> "/abingo/terminate?alternative_id=" + alternative.id.to_s
|
1186
|
+
else
|
1187
|
+
if alternative.content == short_circuit
|
1188
|
+
span "(All users seeing this.)"
|
1189
|
+
end
|
1190
|
+
end
|
1191
|
+
};
|
1192
|
+
}
|
1193
|
+
end
|
1194
|
+
|
1195
|
+
tr.abingo_experiment_row { td :colspan=>'24' do
|
1196
|
+
span("Significance test results: " + experiment.describe_result_in_words)
|
1197
|
+
end
|
1198
|
+
}
|
1199
|
+
|
1200
|
+
end
|
1201
|
+
end
|
1202
|
+
|
1203
|
+
def abingo_termination_notice
|
1204
|
+
h1 "ABingo Dashboard - Termination Notice"
|
1205
|
+
div { @abingo_notice }
|
1206
|
+
end
|
1207
|
+
|
1208
|
+
def abingo_dashboard_restricted_access
|
1209
|
+
h1 "ABingo Dashboard - Restricted Access"
|
1210
|
+
div "Only the administrator can access the ABingo Dashboard."
|
1211
|
+
end
|
1212
|
+
|
1213
|
+
VIEW_DEFS
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
# Includes all common ABingo views inside the views module using module_eval
|
1217
|
+
# (this is why the call must be the last statement in the views module)
|
1218
|
+
def include_abingo_views
|
1219
|
+
module_eval ABingoCampingPlugin::Views.abingo_view_helpers
|
1220
|
+
module_eval ABingoCampingPlugin::Views.common_abingo_views
|
1221
|
+
|
1222
|
+
module_eval do
|
1223
|
+
app_module_name = self.to_s.split("::").first
|
1224
|
+
mab_class_name = "#{app_module_name}::Mab"
|
1225
|
+
mab_class = mab_class_name.constantize
|
1226
|
+
|
1227
|
+
# unless mab_class.public_instance_methods.include? 'register'
|
1228
|
+
# module_eval ABingoCampingPlugin::Views.register_view
|
1229
|
+
# end
|
1230
|
+
end
|
1231
|
+
|
1232
|
+
end
|
1233
|
+
end
|