opawesome 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +39 -0
- data/app/assets/javascripts/opawesome/cookies.js +141 -0
- data/app/assets/javascripts/opawesome.js +36 -0
- data/app/controllers/opawesome/sessions_controller.rb +16 -0
- data/app/controllers/opawesome/tests_controller.rb +31 -0
- data/app/helpers/opawesome/sessions_helper.rb +2 -0
- data/app/helpers/opawesome/tracker_helper.rb +56 -0
- data/app/models/opawesome/test.rb +47 -0
- data/app/models/opawesome/test_option.rb +19 -0
- data/app/models/opawesome.rb +5 -0
- data/app/views/opawesome/tests/_option_fields.html.erb +5 -0
- data/app/views/opawesome/tests/edit.html.erb +30 -0
- data/app/views/opawesome/tests/index.html.erb +11 -0
- data/app/views/opawesome/tests/show.html.erb +33 -0
- data/config/routes.rb +6 -0
- data/db/migrate/20130519180140_create_opawesome_tests.rb +13 -0
- data/db/migrate/20130519180402_create_opawesome_test_options.rb +12 -0
- data/lib/opawesome/engine.rb +14 -0
- data/lib/opawesome/version.rb +3 -0
- data/lib/opawesome.rb +7 -0
- data/lib/tasks/opawesome_tasks.rake +4 -0
- data/spec/controllers/opawesome/sessions_controller_spec.rb +50 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/development.sqlite3 +0 -0
- data/spec/dummy/db/schema.rb +34 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/log/development.log +155 -0
- data/spec/dummy/log/test.log +1258 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/factories/opawesome_test_options.rb +8 -0
- data/spec/factories/opawesome_tests.rb +8 -0
- data/spec/helpers/opawesome/tracker_helper_spec.rb +163 -0
- data/spec/models/opawesome/test_option_spec.rb +31 -0
- data/spec/models/opawesome/test_spec.rb +41 -0
- data/spec/spec_helper.rb +19 -0
- metadata +250 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2013 Alex Rehberg
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'rdoc/task'
|
10
|
+
rescue LoadError
|
11
|
+
require 'rdoc/rdoc'
|
12
|
+
require 'rake/rdoctask'
|
13
|
+
RDoc::Task = Rake::RDocTask
|
14
|
+
end
|
15
|
+
|
16
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
17
|
+
rdoc.rdoc_dir = 'rdoc'
|
18
|
+
rdoc.title = 'Opawesome'
|
19
|
+
rdoc.options << '--line-numbers'
|
20
|
+
rdoc.rdoc_files.include('README.rdoc')
|
21
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
22
|
+
end
|
23
|
+
|
24
|
+
Bundler::GemHelper.install_tasks
|
25
|
+
|
26
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
27
|
+
load 'rails/tasks/engine.rake'
|
28
|
+
|
29
|
+
Bundler::GemHelper.install_tasks
|
30
|
+
|
31
|
+
Dir[File.join(File.dirname(__FILE__), 'tasks/**/*.rake')].each {|f| load f }
|
32
|
+
|
33
|
+
require 'rspec/core'
|
34
|
+
require 'rspec/core/rake_task'
|
35
|
+
|
36
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
37
|
+
RSpec::Core::RakeTask.new(:spec => 'app:db:test:prepare')
|
38
|
+
|
39
|
+
task :default => :spec
|
@@ -0,0 +1,141 @@
|
|
1
|
+
/*!
|
2
|
+
* Cookies.js - 0.3.1
|
3
|
+
* Wednesday, April 24 2013 @ 2:28 AM EST
|
4
|
+
*
|
5
|
+
* Copyright (c) 2013, Scott Hamper
|
6
|
+
* Licensed under the MIT license,
|
7
|
+
* http://www.opensource.org/licenses/MIT
|
8
|
+
*/
|
9
|
+
(function (undefined) {
|
10
|
+
'use strict';
|
11
|
+
|
12
|
+
var Cookies = function (key, value, options) {
|
13
|
+
return arguments.length === 1 ?
|
14
|
+
Cookies.get(key) : Cookies.set(key, value, options);
|
15
|
+
};
|
16
|
+
|
17
|
+
// Allows for setter injection in unit tests
|
18
|
+
Cookies._document = document;
|
19
|
+
Cookies._navigator = navigator;
|
20
|
+
|
21
|
+
Cookies.defaults = {
|
22
|
+
path: '/'
|
23
|
+
};
|
24
|
+
|
25
|
+
Cookies.get = function (key) {
|
26
|
+
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
|
27
|
+
Cookies._renewCache();
|
28
|
+
}
|
29
|
+
|
30
|
+
return Cookies._cache[key];
|
31
|
+
};
|
32
|
+
|
33
|
+
Cookies.set = function (key, value, options) {
|
34
|
+
options = Cookies._getExtendedOptions(options);
|
35
|
+
options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires);
|
36
|
+
|
37
|
+
Cookies._document.cookie = Cookies._generateCookieString(key, value, options);
|
38
|
+
|
39
|
+
return Cookies;
|
40
|
+
};
|
41
|
+
|
42
|
+
Cookies.expire = function (key, options) {
|
43
|
+
return Cookies.set(key, undefined, options);
|
44
|
+
};
|
45
|
+
|
46
|
+
Cookies._getExtendedOptions = function (options) {
|
47
|
+
return {
|
48
|
+
path: options && options.path || Cookies.defaults.path,
|
49
|
+
domain: options && options.domain || Cookies.defaults.domain,
|
50
|
+
expires: options && options.expires || Cookies.defaults.expires,
|
51
|
+
secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure
|
52
|
+
};
|
53
|
+
};
|
54
|
+
|
55
|
+
Cookies._isValidDate = function (date) {
|
56
|
+
return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
|
57
|
+
};
|
58
|
+
|
59
|
+
Cookies._getExpiresDate = function (expires, now) {
|
60
|
+
now = now || new Date();
|
61
|
+
switch (typeof expires) {
|
62
|
+
case 'number': expires = new Date(now.getTime() + expires * 1000); break;
|
63
|
+
case 'string': expires = new Date(expires); break;
|
64
|
+
}
|
65
|
+
|
66
|
+
if (expires && !Cookies._isValidDate(expires)) {
|
67
|
+
throw new Error('`expires` parameter cannot be converted to a valid Date instance');
|
68
|
+
}
|
69
|
+
|
70
|
+
return expires;
|
71
|
+
};
|
72
|
+
|
73
|
+
Cookies._generateCookieString = function (key, value, options) {
|
74
|
+
key = encodeURIComponent(key);
|
75
|
+
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
|
76
|
+
options = options || {};
|
77
|
+
|
78
|
+
var cookieString = key + '=' + value;
|
79
|
+
cookieString += options.path ? ';path=' + options.path : '';
|
80
|
+
cookieString += options.domain ? ';domain=' + options.domain : '';
|
81
|
+
cookieString += options.expires ? ';expires=' + options.expires.toGMTString() : '';
|
82
|
+
cookieString += options.secure ? ';secure' : '';
|
83
|
+
|
84
|
+
return cookieString;
|
85
|
+
};
|
86
|
+
|
87
|
+
Cookies._getCookieObjectFromString = function (documentCookie) {
|
88
|
+
var cookieObject = {};
|
89
|
+
var cookiesArray = documentCookie ? documentCookie.split('; ') : [];
|
90
|
+
|
91
|
+
for (var i = 0; i < cookiesArray.length; i++) {
|
92
|
+
var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);
|
93
|
+
|
94
|
+
if (cookieObject[cookieKvp.key] === undefined) {
|
95
|
+
cookieObject[cookieKvp.key] = cookieKvp.value;
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
return cookieObject;
|
100
|
+
};
|
101
|
+
|
102
|
+
Cookies._getKeyValuePairFromCookieString = function (cookieString) {
|
103
|
+
// "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')`
|
104
|
+
var separatorIndex = cookieString.indexOf('=');
|
105
|
+
|
106
|
+
// IE omits the "=" when the cookie value is an empty string
|
107
|
+
separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex;
|
108
|
+
|
109
|
+
return {
|
110
|
+
key: decodeURIComponent(cookieString.substr(0, separatorIndex)),
|
111
|
+
value: decodeURIComponent(cookieString.substr(separatorIndex + 1))
|
112
|
+
};
|
113
|
+
};
|
114
|
+
|
115
|
+
Cookies._renewCache = function () {
|
116
|
+
Cookies._cache = Cookies._getCookieObjectFromString(Cookies._document.cookie);
|
117
|
+
Cookies._cachedDocumentCookie = Cookies._document.cookie;
|
118
|
+
};
|
119
|
+
|
120
|
+
Cookies._areEnabled = function () {
|
121
|
+
return Cookies._navigator.cookieEnabled ||
|
122
|
+
Cookies.set('cookies.js', 1).get('cookies.js') === '1';
|
123
|
+
};
|
124
|
+
|
125
|
+
Cookies.enabled = Cookies._areEnabled();
|
126
|
+
|
127
|
+
// AMD support
|
128
|
+
if (typeof define === 'function' && define.amd) {
|
129
|
+
define(function () { return Cookies; });
|
130
|
+
// CommonJS and Node.js module support.
|
131
|
+
} else if (typeof exports !== 'undefined') {
|
132
|
+
// Support Node.js specific `module.exports` (which can be a function)
|
133
|
+
if (typeof module !== 'undefined' && module.exports) {
|
134
|
+
exports = module.exports = Cookies;
|
135
|
+
}
|
136
|
+
// But always support CommonJS module 1.1.1 spec (`exports` cannot be a function)
|
137
|
+
exports.Cookies = Cookies;
|
138
|
+
} else {
|
139
|
+
window.Cookies = Cookies;
|
140
|
+
}
|
141
|
+
})();
|
@@ -0,0 +1,36 @@
|
|
1
|
+
//= require 'opawesome/cookies'
|
2
|
+
|
3
|
+
OptimizatonTestForm = {
|
4
|
+
setup: function(){
|
5
|
+
$('.edit_test').on('click', '.remove_option', this.remove_attribute);
|
6
|
+
$('.edit_test').on('click', '.add_fields', this.add_attribute);
|
7
|
+
},
|
8
|
+
|
9
|
+
remove_attribute: function(event){
|
10
|
+
$(this).prev('input[type=hidden]').val('1');
|
11
|
+
$(this).closest('.input').hide();
|
12
|
+
event.preventDefault();
|
13
|
+
},
|
14
|
+
|
15
|
+
add_attribute: function(event){
|
16
|
+
time = new Date().getTime()
|
17
|
+
regexp = new RegExp($(this).data('id'), 'g')
|
18
|
+
$(this).before($(this).data('fields').replace(regexp, time))
|
19
|
+
event.preventDefault()
|
20
|
+
}
|
21
|
+
}
|
22
|
+
|
23
|
+
OptimizationTracker = {
|
24
|
+
create_session: function(){
|
25
|
+
if(Cookies('opt_valid_session') === undefined) {
|
26
|
+
$.post('/opawesome/sessions', function(data) {
|
27
|
+
// optimization tracking started successfully (we're not doing anything special here yet)
|
28
|
+
});
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
$(document).ready(function(){
|
34
|
+
OptimizatonTestForm.setup();
|
35
|
+
OptimizationTracker.create_session();
|
36
|
+
});
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class Opawesome::SessionsController < ApplicationController
|
2
|
+
def create
|
3
|
+
# check for existing option cookies
|
4
|
+
unless cookies[:opaw_valid_session]
|
5
|
+
cookies.each do |k, v|
|
6
|
+
if k[0..4] == 'opaw_' && k != 'opaw_valid_session' && k != 'opaw_ignore'
|
7
|
+
option = Opawesome::TestOption.where(id: v).first
|
8
|
+
option.select! unless option.nil?
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
cookies[:opaw_valid_session] = true
|
14
|
+
render nothing: true, status: 200
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Opawesome::TestsController < ApplicationController
|
2
|
+
def index
|
3
|
+
@tests = Opawesome::Test.order('created_at DESC').all
|
4
|
+
end
|
5
|
+
|
6
|
+
def show
|
7
|
+
@test = Opawesome::Test.find(params[:id])
|
8
|
+
@test_options = @test.options.sort_by {|option| option.conversion_rate }
|
9
|
+
@test_options.reverse!
|
10
|
+
end
|
11
|
+
|
12
|
+
def edit
|
13
|
+
@test = Opawesome::Test.find(params[:id])
|
14
|
+
end
|
15
|
+
|
16
|
+
def update
|
17
|
+
@test = Opawesome::Test.find(params[:id])
|
18
|
+
|
19
|
+
if @test.update_attributes(params[:opawesome_test])
|
20
|
+
redirect_to opawesome_test_path(@test)
|
21
|
+
else
|
22
|
+
render :edit
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def destroy
|
27
|
+
@test = Opawesome::Test.find(params[:id])
|
28
|
+
@test.destroy
|
29
|
+
redirect_to opawesome_tests_path
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# TODO: make this less of an ugly mess
|
2
|
+
|
3
|
+
module Opawesome::TrackerHelper
|
4
|
+
def opaw_select(key, default)
|
5
|
+
if cookies["opaw_#{key}"]
|
6
|
+
option = Opawesome::TestOption.where(id: cookies["opaw_#{key}"]).first
|
7
|
+
|
8
|
+
if option.nil?
|
9
|
+
cookies["opaw_#{key}"] = nil
|
10
|
+
else
|
11
|
+
return option.value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
test = Opawesome::Test.where(key: key).first_or_initialize
|
16
|
+
|
17
|
+
# set up defaults if the test is new
|
18
|
+
if test.new_record?
|
19
|
+
test.name = test.key.to_s.humanize
|
20
|
+
test.save!
|
21
|
+
Opawesome::TestOption.create!(test_id: test.id, value: default, conversion_count: 0, selection_count: 0)
|
22
|
+
end
|
23
|
+
|
24
|
+
if cookies[:opaw_ignore]
|
25
|
+
test.best_option.value
|
26
|
+
else
|
27
|
+
# select an option and save it in the user's cookies
|
28
|
+
option = test.select_option
|
29
|
+
|
30
|
+
option.select! unless cookies[:opaw_valid_session].nil?
|
31
|
+
|
32
|
+
cookies.permanent["opaw_#{key}"] = option.id
|
33
|
+
option.value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def opaw_convert!(key)
|
38
|
+
return if cookies[:opaw_ignore]
|
39
|
+
|
40
|
+
if cookies["opaw_#{key}"]
|
41
|
+
option = Opawesome::TestOption.where(id: cookies["opaw_#{key}"]).first
|
42
|
+
|
43
|
+
if option.nil?
|
44
|
+
cookies["opaw_#{key}"] = nil
|
45
|
+
else
|
46
|
+
option.convert! if cookies[:opaw_valid_session]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def check_opaw_ignore
|
52
|
+
if params[:opaw_ignore]
|
53
|
+
cookies.permanent[:opaw_ignore] = true
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
class Opawesome::Test < ActiveRecord::Base
|
2
|
+
attr_accessible :key, :name, :options_attributes
|
3
|
+
|
4
|
+
has_many :options, class_name: Opawesome::TestOption
|
5
|
+
|
6
|
+
accepts_nested_attributes_for :options, allow_destroy: true
|
7
|
+
|
8
|
+
validates :key, presence: true, uniqueness: true
|
9
|
+
|
10
|
+
def selection_count
|
11
|
+
options.map(&:selection_count).reduce(:+)
|
12
|
+
end
|
13
|
+
|
14
|
+
def conversion_count
|
15
|
+
options.map(&:conversion_count).reduce(:+)
|
16
|
+
end
|
17
|
+
|
18
|
+
def best_option
|
19
|
+
best_option = options.first
|
20
|
+
|
21
|
+
options.each do |option|
|
22
|
+
if option.conversion_rate > best_option.conversion_rate
|
23
|
+
best_option = option
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
best_option
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO: Break this out in to an OptionSelector class so we can support different selection strategies
|
31
|
+
def select_option
|
32
|
+
# round robbin until each option has at least 20 conversions, which gives us a good base to start from
|
33
|
+
options.shuffle.each do |option|
|
34
|
+
return option if option.conversion_count < 20
|
35
|
+
end
|
36
|
+
|
37
|
+
# use the best performing option most of the time, but try others as well
|
38
|
+
epsilon = 0.1
|
39
|
+
random = Random.rand
|
40
|
+
|
41
|
+
if random < epsilon
|
42
|
+
options.sample
|
43
|
+
else
|
44
|
+
best_option
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Opawesome::TestOption < ActiveRecord::Base
|
2
|
+
attr_accessible :test_id, :value, :conversion_count, :selection_count
|
3
|
+
|
4
|
+
belongs_to :test, class_name: Opawesome::Test
|
5
|
+
|
6
|
+
def conversion_rate
|
7
|
+
(selection_count == 0 or conversion_count == 0) ? 0 : (conversion_count.to_f / selection_count.to_f * 100.0)
|
8
|
+
end
|
9
|
+
|
10
|
+
def select!
|
11
|
+
self.selection_count += 1
|
12
|
+
save!
|
13
|
+
end
|
14
|
+
|
15
|
+
def convert!
|
16
|
+
self.conversion_count += 1
|
17
|
+
save!
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
<h1>Edit Test</h1>
|
2
|
+
|
3
|
+
<%= form_for @test, html: {class: 'edit_test'} do |f| %>
|
4
|
+
<div class="input">
|
5
|
+
<%= f.label :name, 'Name' %>
|
6
|
+
<%= f.text_field :name %>
|
7
|
+
</div>
|
8
|
+
|
9
|
+
<h2>Test Options</h2>
|
10
|
+
<p><strong>NOTE:</strong> only edit options to fix typos, if you want to remove an option and add a new one, fully remove the option you want to get rid of and then create a new option. If you don't do this, the conversion stats from the old option will be used.</p>
|
11
|
+
<%= f.fields_for :options do |builder| %>
|
12
|
+
<%= render 'option_fields', f: builder %>
|
13
|
+
<% end %>
|
14
|
+
<%=
|
15
|
+
new_object = Opawesome::TestOption.new
|
16
|
+
id = new_object.object_id
|
17
|
+
fields = f.fields_for(:options, new_object, child_index: id) do |builder|
|
18
|
+
render "option_fields", f: builder
|
19
|
+
end
|
20
|
+
link_to("New Test Option", '#', class: "add_fields btn", data: {id: id, fields: fields.gsub("\n", "")})
|
21
|
+
%>
|
22
|
+
|
23
|
+
<div class="input">
|
24
|
+
<% if @test.new_record? %>
|
25
|
+
<%= f.submit "Create Test", class: 'btn' %>
|
26
|
+
<% else %>
|
27
|
+
<%= f.submit "Update Test", class: 'btn' %>
|
28
|
+
<% end %>
|
29
|
+
</div>
|
30
|
+
<% end %>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<h1>Opawesome Tests</h1>
|
2
|
+
<p>Viewing all tests</p>
|
3
|
+
|
4
|
+
<table cellspacing="3" class="admin_dashboard_table">
|
5
|
+
<% @tests.each do |test| %>
|
6
|
+
<tr>
|
7
|
+
<td><%= link_to test.name, opawesome_test_path(test) %></td>
|
8
|
+
<td><%= link_to "Edit", edit_opawesome_test_path(test) %></td>
|
9
|
+
</tr>
|
10
|
+
<% end %>
|
11
|
+
</table>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
<h1><%= @test.name %></h1>
|
2
|
+
<p><%= link_to "Edit test and options", edit_opawesome_test_path(@test), class: 'btn' %></p>
|
3
|
+
|
4
|
+
<h2>Test options</h2>
|
5
|
+
<table>
|
6
|
+
<thead>
|
7
|
+
<tr>
|
8
|
+
<th>Value</th>
|
9
|
+
<th>Selection Count</th>
|
10
|
+
<th>Conversion Count</th>
|
11
|
+
<th>Conversion Rate</th>
|
12
|
+
</tr>
|
13
|
+
</thead>
|
14
|
+
<tbody>
|
15
|
+
<% @test_options.each do |option| %>
|
16
|
+
<% if @test.best_option == option %>
|
17
|
+
<tr>
|
18
|
+
<td><strong><%= option.value %></strong></td>
|
19
|
+
<td><strong><%= option.selection_count %></strong></td>
|
20
|
+
<td><strong><%= option.conversion_count %></strong></td>
|
21
|
+
<td><strong><%= option.conversion_rate.round(2) %>%</strong></td>
|
22
|
+
</tr>
|
23
|
+
<% else %>
|
24
|
+
<tr>
|
25
|
+
<td><%= option.value %></td>
|
26
|
+
<td><%= option.selection_count %></td>
|
27
|
+
<td><%= option.conversion_count %></td>
|
28
|
+
<td><%= option.conversion_rate.round(2) %>%</td>
|
29
|
+
</tr>
|
30
|
+
<% end %>
|
31
|
+
<% end %>
|
32
|
+
</tbody>
|
33
|
+
</table>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateOpawesomeTestOptions < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :opawesome_test_options do |t|
|
4
|
+
t.integer :test_id
|
5
|
+
t.integer :conversion_count, default: 0
|
6
|
+
t.integer :selection_count, default: 0
|
7
|
+
t.string :value
|
8
|
+
|
9
|
+
t.timestamps
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Opawesome
|
2
|
+
class Engine < ::Rails::Engine
|
3
|
+
initializer "opawesome.tracking_methods" do |app|
|
4
|
+
ActionController::Base.send :include, Opawesome::TrackerHelper
|
5
|
+
end
|
6
|
+
|
7
|
+
config.generators do |g|
|
8
|
+
g.test_framework :rspec, fixture: false
|
9
|
+
g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
10
|
+
g.assets false
|
11
|
+
g.helper false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/opawesome.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Opawesome::SessionsController do
|
4
|
+
describe "POST 'create'" do
|
5
|
+
it "should respond with an empty success response" do
|
6
|
+
post :create
|
7
|
+
response.should be_successful
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should create a valid optimization session cookie" do
|
11
|
+
post :create
|
12
|
+
cookies[:opaw_valid_session].should eq(true)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when there are existing test option ids" do
|
16
|
+
before do
|
17
|
+
@test_option = mock_model Opawesome::TestOption, select!: true
|
18
|
+
Opawesome::TestOption.stub(:where){ Opawesome::TestOption }
|
19
|
+
Opawesome::TestOption.stub(:first){ @test_option }
|
20
|
+
cookies[:opaw_test] = @test_option.id
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should find the test option" do
|
24
|
+
Opawesome::TestOption.should_receive(:where).with(hash_including(id: @test_option.id))
|
25
|
+
post :create
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should select the option" do
|
29
|
+
@test_option.should_receive(:select!)
|
30
|
+
post :create
|
31
|
+
end
|
32
|
+
|
33
|
+
context "when the session has already been validated" do
|
34
|
+
before do
|
35
|
+
cookies[:opaw_valid_session] = true
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should not find the test option" do
|
39
|
+
Opawesome::TestOption.should_not_receive(:where)
|
40
|
+
post :create
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should not select the option" do
|
44
|
+
@test_option.should_not_receive(:select!)
|
45
|
+
post :create
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|