camping-abingo 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. data/README +3 -0
  2. data/doc/classes/ABingoCampingPlugin.html +188 -0
  3. data/doc/classes/ABingoCampingPlugin.src/M000016.html +18 -0
  4. data/doc/classes/ABingoCampingPlugin.src/M000017.html +18 -0
  5. data/doc/classes/ABingoCampingPlugin.src/M000018.html +19 -0
  6. data/doc/classes/ABingoCampingPlugin.src/M000034.html +18 -0
  7. data/doc/classes/ABingoCampingPlugin.src/M000035.html +18 -0
  8. data/doc/classes/ABingoCampingPlugin.src/M000036.html +19 -0
  9. data/doc/classes/ABingoCampingPlugin/ABingo.html +121 -0
  10. data/doc/classes/ABingoCampingPlugin/Controllers.html +191 -0
  11. data/doc/classes/ABingoCampingPlugin/Controllers.src/M000022.html +18 -0
  12. data/doc/classes/ABingoCampingPlugin/Controllers.src/M000023.html +26 -0
  13. data/doc/classes/ABingoCampingPlugin/Controllers.src/M000040.html +18 -0
  14. data/doc/classes/ABingoCampingPlugin/Controllers.src/M000041.html +26 -0
  15. data/doc/classes/ABingoCampingPlugin/Filters.html +158 -0
  16. data/doc/classes/ABingoCampingPlugin/Filters.src/M000037.html +26 -0
  17. data/doc/classes/ABingoCampingPlugin/Filters.src/M000055.html +26 -0
  18. data/doc/classes/ABingoCampingPlugin/Helpers.html +372 -0
  19. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000024.html +18 -0
  20. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000025.html +19 -0
  21. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000026.html +18 -0
  22. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000027.html +18 -0
  23. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000028.html +36 -0
  24. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000029.html +28 -0
  25. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000030.html +18 -0
  26. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000031.html +48 -0
  27. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000032.html +32 -0
  28. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000033.html +18 -0
  29. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000034.html +26 -0
  30. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000035.html +20 -0
  31. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000036.html +18 -0
  32. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000042.html +18 -0
  33. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000043.html +19 -0
  34. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000044.html +18 -0
  35. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000045.html +18 -0
  36. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000046.html +36 -0
  37. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000047.html +28 -0
  38. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000048.html +18 -0
  39. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000049.html +48 -0
  40. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000050.html +32 -0
  41. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000051.html +18 -0
  42. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000052.html +26 -0
  43. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000053.html +20 -0
  44. data/doc/classes/ABingoCampingPlugin/Helpers.src/M000054.html +18 -0
  45. data/doc/classes/ABingoCampingPlugin/Models.html +208 -0
  46. data/doc/classes/ABingoCampingPlugin/Models.src/M000038.html +22 -0
  47. data/doc/classes/ABingoCampingPlugin/Models.src/M000039.html +39 -0
  48. data/doc/classes/ABingoCampingPlugin/Models.src/M000040.html +21 -0
  49. data/doc/classes/ABingoCampingPlugin/Models.src/M000056.html +22 -0
  50. data/doc/classes/ABingoCampingPlugin/Models.src/M000057.html +39 -0
  51. data/doc/classes/ABingoCampingPlugin/Models.src/M000058.html +21 -0
  52. data/doc/classes/ABingoCampingPlugin/Models/Alternative.html +181 -0
  53. data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000041.html +20 -0
  54. data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000042.html +21 -0
  55. data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000043.html +21 -0
  56. data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000059.html +20 -0
  57. data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000060.html +21 -0
  58. data/doc/classes/ABingoCampingPlugin/Models/Alternative.src/M000061.html +21 -0
  59. data/doc/classes/ABingoCampingPlugin/Models/Experiment.html +273 -0
  60. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000044.html +21 -0
  61. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000045.html +21 -0
  62. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000046.html +18 -0
  63. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000047.html +18 -0
  64. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000048.html +20 -0
  65. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000049.html +23 -0
  66. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000050.html +29 -0
  67. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000051.html +50 -0
  68. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000052.html +28 -0
  69. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000062.html +21 -0
  70. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000063.html +21 -0
  71. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000064.html +18 -0
  72. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000065.html +18 -0
  73. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000066.html +20 -0
  74. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000067.html +23 -0
  75. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000068.html +29 -0
  76. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000069.html +50 -0
  77. data/doc/classes/ABingoCampingPlugin/Models/Experiment.src/M000070.html +28 -0
  78. data/doc/classes/ABingoCampingPlugin/Views.html +196 -0
  79. data/doc/classes/ABingoCampingPlugin/Views.src/M000019.html +18 -0
  80. data/doc/classes/ABingoCampingPlugin/Views.src/M000020.html +18 -0
  81. data/doc/classes/ABingoCampingPlugin/Views.src/M000021.html +30 -0
  82. data/doc/classes/ABingoCampingPlugin/Views.src/M000037.html +18 -0
  83. data/doc/classes/ABingoCampingPlugin/Views.src/M000038.html +18 -0
  84. data/doc/classes/ABingoCampingPlugin/Views.src/M000039.html +30 -0
  85. data/doc/classes/Abingo.html +429 -0
  86. data/doc/classes/Abingo.src/M000001.html +18 -0
  87. data/doc/classes/Abingo.src/M000002.html +18 -0
  88. data/doc/classes/Abingo.src/M000003.html +18 -0
  89. data/doc/classes/Abingo.src/M000004.html +22 -0
  90. data/doc/classes/Abingo.src/M000005.html +69 -0
  91. data/doc/classes/Abingo.src/M000006.html +47 -0
  92. data/doc/classes/Abingo.src/M000007.html +31 -0
  93. data/doc/classes/Abingo.src/M000008.html +36 -0
  94. data/doc/classes/Abingo.src/M000009.html +18 -0
  95. data/doc/classes/Abingo.src/M000010.html +36 -0
  96. data/doc/classes/Abingo.src/M000011.html +22 -0
  97. data/doc/classes/Abingo.src/M000012.html +21 -0
  98. data/doc/classes/Abingo.src/M000013.html +18 -0
  99. data/doc/classes/Abingo.src/M000014.html +39 -0
  100. data/doc/classes/Abingo.src/M000015.html +25 -0
  101. data/doc/classes/ActiveSupport.html +107 -0
  102. data/doc/classes/ActiveSupport/Cache.html +111 -0
  103. data/doc/classes/ActiveSupport/Cache/MemoryStore.html +137 -0
  104. data/doc/classes/ActiveSupport/Cache/MemoryStore.src/M000016.html +18 -0
  105. data/doc/classes/CampingABingoTest.html +149 -0
  106. data/doc/classes/CampingABingoTest.src/M000017.html +24 -0
  107. data/doc/classes/CampingABingoTest/Controllers.html +116 -0
  108. data/doc/classes/CampingABingoTest/Controllers/Index.html +137 -0
  109. data/doc/classes/CampingABingoTest/Controllers/Index.src/M000024.html +18 -0
  110. data/doc/classes/CampingABingoTest/Controllers/Landing.html +137 -0
  111. data/doc/classes/CampingABingoTest/Controllers/Landing.src/M000026.html +18 -0
  112. data/doc/classes/CampingABingoTest/Controllers/SignIn.html +152 -0
  113. data/doc/classes/CampingABingoTest/Controllers/SignIn.src/M000030.html +18 -0
  114. data/doc/classes/CampingABingoTest/Controllers/SignIn.src/M000031.html +34 -0
  115. data/doc/classes/CampingABingoTest/Controllers/SignOut.html +137 -0
  116. data/doc/classes/CampingABingoTest/Controllers/SignOut.src/M000029.html +21 -0
  117. data/doc/classes/CampingABingoTest/Controllers/SignUp.html +152 -0
  118. data/doc/classes/CampingABingoTest/Controllers/SignUp.src/M000027.html +23 -0
  119. data/doc/classes/CampingABingoTest/Controllers/SignUp.src/M000028.html +33 -0
  120. data/doc/classes/CampingABingoTest/Controllers/Welcome.html +137 -0
  121. data/doc/classes/CampingABingoTest/Controllers/Welcome.src/M000025.html +18 -0
  122. data/doc/classes/CampingABingoTest/Helpers.html +112 -0
  123. data/doc/classes/CampingABingoTest/Models.html +119 -0
  124. data/doc/classes/CampingABingoTest/Models/CreateUserSchema.html +152 -0
  125. data/doc/classes/CampingABingoTest/Models/CreateUserSchema.src/M000032.html +26 -0
  126. data/doc/classes/CampingABingoTest/Models/CreateUserSchema.src/M000033.html +19 -0
  127. data/doc/classes/CampingABingoTest/Models/User.html +111 -0
  128. data/doc/classes/CampingABingoTest/Views.html +206 -0
  129. data/doc/classes/CampingABingoTest/Views.src/M000018.html +70 -0
  130. data/doc/classes/CampingABingoTest/Views.src/M000019.html +22 -0
  131. data/doc/classes/CampingABingoTest/Views.src/M000020.html +51 -0
  132. data/doc/classes/CampingABingoTest/Views.src/M000021.html +31 -0
  133. data/doc/classes/CampingABingoTest/Views.src/M000022.html +31 -0
  134. data/doc/classes/CampingABingoTest/Views.src/M000023.html +20 -0
  135. data/doc/created.rid +1 -0
  136. data/doc/files/examples/camping-abingo-test/camping-abingo-test_rb.html +101 -0
  137. data/doc/files/lib/camping-abingo_rb.html +457 -0
  138. data/doc/fr_class_index.html +52 -0
  139. data/doc/fr_file_index.html +28 -0
  140. data/doc/fr_method_index.html +96 -0
  141. data/doc/index.html +24 -0
  142. data/lib/camping-abingo.rb +1233 -0
  143. 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>
@@ -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