opawesome 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|