-
-
Notifications
You must be signed in to change notification settings - Fork 697
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by dreispt
- Loading branch information
Showing
25 changed files
with
916 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
**This file is going to be generated by oca-gen-addon-readme.** | ||
|
||
*Manual changes will be overwritten.* | ||
|
||
Please provide content in the ``readme`` directory: | ||
|
||
* **DESCRIPTION.rst** (required) | ||
* INSTALL.rst (optional) | ||
* CONFIGURE.rst (optional) | ||
* **USAGE.rst** (optional, highly recommended) | ||
* DEVELOP.rst (optional) | ||
* ROADMAP.rst (optional) | ||
* HISTORY.rst (optional, recommended) | ||
* **CONTRIBUTORS.rst** (optional, highly recommended) | ||
* CREDITS.rst (optional) | ||
|
||
Content of this README will also be drawn from the addon manifest, | ||
from keys such as name, authors, maintainers, development_status, | ||
and license. | ||
|
||
A good, one sentence summary in the manifest is also highly recommended. | ||
|
||
|
||
Automatic changelog generation | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
`HISTORY.rst` can be auto generated using `towncrier <https://pypi.org/project/towncrier>`_. | ||
|
||
Just put towncrier compatible changelog fragments into `readme/newsfragments` | ||
and the changelog file will be automatically generated and updated when a new fragment is added. | ||
|
||
Please refer to `towncrier` documentation to know more. | ||
|
||
NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. | ||
If you need to run it manually, refer to `OCA/maintainer-tools README <https://github.com/OCA/maintainer-tools>`_. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import models |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
{ | ||
"name": "A/B Testing", | ||
"category": "Website", | ||
"version": "13.0.1.0.0", | ||
"author": "Onestein, Odoo Community Association (OCA)", | ||
"license": "AGPL-3", | ||
"website": "https://onestein.nl", | ||
"depends": ["website"], | ||
"data": [ | ||
"security/ir_model_access.xml", | ||
"templates/assets.xml", | ||
"templates/website.xml", | ||
"views/ir_ui_view_view.xml", | ||
"views/target_view.xml", | ||
"views/target_conversion_view.xml", | ||
"menuitems.xml", | ||
], | ||
"qweb": ["static/src/xml/editor.xml"], | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<?xml version="1.0" encoding="UTF-8" ?> | ||
<odoo> | ||
<menuitem | ||
id="ab_testing_menu" | ||
parent="website.menu_website_configuration" | ||
name="A/B Testing" | ||
/> | ||
<menuitem | ||
id="ab_testing_target_menu" | ||
parent="ab_testing_menu" | ||
name="Targets" | ||
action="ab_testing_target_action" | ||
/> | ||
<menuitem | ||
id="ab_testing_target_conversion_menu" | ||
parent="ab_testing_menu" | ||
name="Conversions" | ||
action="ab_testing_target_conversion_action" | ||
/> | ||
</odoo> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from . import ir_http, ir_ui_view, target, target_conversion, target_trigger |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from odoo import models | ||
from odoo.http import request | ||
|
||
|
||
class IrHttp(models.AbstractModel): | ||
_inherit = "ir.http" | ||
|
||
@classmethod | ||
def _dispatch(cls): | ||
response = super(IrHttp, cls)._dispatch() | ||
if request.is_frontend: | ||
website = request.website | ||
path = request.httprequest.path | ||
if not request.env.user.has_group("website.group_website_designer"): | ||
matching_triggers = ( | ||
request.env["ab.testing.target.trigger"] | ||
.sudo() | ||
.search( | ||
[ | ||
("target_id.website_id", "=", website.id), | ||
("on", "=", "url_visit"), | ||
("url", "=", path), | ||
] | ||
) | ||
) | ||
matching_triggers.create_conversion() | ||
return response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import random | ||
|
||
from odoo import _, api, fields, models | ||
from odoo.exceptions import AccessError, UserError | ||
from odoo.http import request | ||
|
||
|
||
class IrUiView(models.Model): | ||
_inherit = "ir.ui.view" | ||
|
||
ab_testing_enabled = fields.Boolean(string="A/B Testing", copy=False) | ||
|
||
master_id = fields.Many2one(comodel_name="ir.ui.view", copy=False) | ||
|
||
variant_ids = fields.One2many( | ||
comodel_name="ir.ui.view", inverse_name="master_id", string="Variants" | ||
) | ||
|
||
def render(self, values=None, engine="ir.qweb", minimal_qcontext=False): | ||
website = self.env["website"].get_current_website() | ||
if ( | ||
website | ||
and self.ab_testing_enabled | ||
and not self.env.user.has_group("website.group_website_publisher") | ||
): | ||
if "ab_testing" not in request.session: | ||
request.session["ab_testing"] = {"active_variants": {}} | ||
if self.id not in request.session["ab_testing"]["active_variants"]: | ||
random_index = random.randint(0, len(self.variant_ids)) | ||
selected_view = self | ||
if random_index: | ||
selected_view = self.variant_ids[random_index - 1] | ||
ab_testing = request.session["ab_testing"].copy() | ||
ab_testing["active_variants"][self.id] = selected_view.id | ||
request.session["ab_testing"] = ab_testing | ||
return selected_view.render(values, engine, minimal_qcontext) | ||
else: | ||
selection_view_id = request.session["ab_testing"]["active_variants"][ | ||
self.id | ||
] | ||
if selection_view_id == self.id: | ||
return super().render(values, engine, minimal_qcontext) | ||
selected_view = self.search([("id", "=", selection_view_id)]) | ||
if selected_view: | ||
return selected_view.render(values, engine, minimal_qcontext) | ||
ab_testing = request.session["ab_testing"].copy() | ||
del ab_testing["active_variants"][self.id] | ||
elif website and self.env.user.has_group("website.group_website_publisher"): | ||
variants = self.env["ir.ui.view"] | ||
if self.master_id: | ||
variants += self.master_id | ||
variants += self.master_id.variant_ids | ||
else: | ||
variants += self | ||
variants += self.variant_ids | ||
if values is None: | ||
values = {} | ||
values["ab_testing_variants"] = variants | ||
|
||
if ( | ||
"ab_testing" in request.session | ||
and not self.master_id | ||
and self.id in request.session["ab_testing"]["active_variants"] | ||
): | ||
active_variant = self.variant_ids.filtered( | ||
lambda v: v.id | ||
== request.session["ab_testing"]["active_variants"][self.id] | ||
) | ||
if active_variant: | ||
values["active_variant"] = active_variant | ||
return active_variant.render(values, engine, minimal_qcontext) | ||
|
||
return super().render(values, engine, minimal_qcontext) | ||
|
||
def create_variant(self, name): | ||
self.ensure_one() | ||
if self.master_id: | ||
raise UserError(_("Cannot create variant of variant.")) | ||
if self.variant_ids.filtered(lambda v: v.name == name): | ||
raise UserError(_("Variant '%s' already exists.") % name) | ||
variant = self.copy({"name": name, "master_id": self.id}) | ||
self._copy_inheritance(variant.id) | ||
return variant.id | ||
|
||
def _copy_inheritance(self, new_id): | ||
"""Copy the inheritance recursively""" | ||
for view in self: | ||
for child in view.inherit_children_ids: | ||
copy = child.copy({"inherit_id": new_id}) | ||
child._copy_inheritance(copy.id) | ||
|
||
def toggle_ab_testing_enabled(self): | ||
self.ensure_one() | ||
if self.master_id: | ||
raise UserError(_("This is not the master page.")) | ||
self.ab_testing_enabled = not self.ab_testing_enabled | ||
|
||
def switch_variant(self, variant_id): | ||
self.ensure_one() | ||
if not self.env.user.has_group("website.group_website_publisher"): | ||
raise AccessError( | ||
_("Cannot deliberately switch variant as non-designer user.") | ||
) | ||
if not variant_id: | ||
raise UserError(_("No variant specified.")) | ||
|
||
if "ab_testing" not in request.session: | ||
request.session["ab_testing"] = {"active_variants": {}} | ||
ab_testing = request.session["ab_testing"].copy() | ||
ab_testing["active_variants"][self.id] = variant_id | ||
request.session["ab_testing"] = ab_testing | ||
|
||
@api.model | ||
def get_active_variants(self): | ||
if "ab_testing" not in request.session: | ||
request.session["ab_testing"] = {"active_variants": {}} | ||
ids = list(request.session["ab_testing"]["active_variants"].values()) | ||
return self.search([("id", "in", ids)]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
from odoo import api, fields, models | ||
|
||
|
||
class Target(models.Model): | ||
_name = "ab.testing.target" | ||
_description = "Target" | ||
|
||
def _default_website(self): | ||
return self.env["website"].search( | ||
[("company_id", "=", self.env.company.id)], limit=1 | ||
) | ||
|
||
website_id = fields.Many2one( | ||
comodel_name="website", | ||
string="Website", | ||
default=_default_website, | ||
ondelete="cascade", | ||
) | ||
name = fields.Char(required=True) | ||
active = fields.Boolean(default=True) | ||
|
||
trigger_ids = fields.One2many( | ||
name="Triggers", | ||
comodel_name="ab.testing.target.trigger", | ||
inverse_name="target_id", | ||
) | ||
|
||
conversion_ids = fields.One2many( | ||
name="Conversions", | ||
comodel_name="ab.testing.target.conversion", | ||
inverse_name="target_id", | ||
) | ||
|
||
conversion_count = fields.Integer(compute="_compute_conversion_count") | ||
|
||
@api.depends("conversion_ids") | ||
def _compute_conversion_count(self): | ||
for target in self: | ||
target.conversion_count = len(target.conversion_ids) | ||
|
||
def open_conversion_view(self): | ||
self.ensure_one() | ||
action = self.env.ref("website_ab_testing.ab_testing_target_conversion_action") | ||
action = action.read()[0] | ||
action["domain"] = [("target_id", "=", self.id)] | ||
action["context"] = "{}" | ||
return action | ||
|
||
def open_conversion_graph(self): | ||
self.ensure_one() | ||
action = self.env.ref("website_ab_testing.ab_testing_target_conversion_action") | ||
action = action.read()[0] | ||
action["domain"] = [("target_id", "=", self.id)] | ||
action["views"] = [(False, "graph")] | ||
return action |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
from odoo import api, fields, models | ||
|
||
|
||
class TargetConversion(models.Model): | ||
_name = "ab.testing.target.conversion" | ||
_description = "Conversion" | ||
|
||
date = fields.Datetime() | ||
target_id = fields.Many2one( | ||
name="Target", | ||
comodel_name="ab.testing.target", | ||
compute="_compute_target_id", | ||
store=True, | ||
) | ||
|
||
trigger_id = fields.Many2one( | ||
name="Trigger", comodel_name="ab.testing.target.trigger", ondelete="cascade" | ||
) | ||
|
||
view_ids = fields.Many2many(name="Active Variants", comodel_name="ir.ui.view") | ||
|
||
view_names = fields.Char( | ||
name="Active Variant Names", compute="_compute_view_names", store=True | ||
) | ||
|
||
@api.depends("trigger_id", "trigger_id.target_id") | ||
def _compute_target_id(self): | ||
for conversion in self: | ||
conversion.target_id = ( | ||
conversion.trigger_id and conversion.trigger_id.target_id | ||
) | ||
|
||
@api.depends("view_ids", "view_ids.name") | ||
def _compute_view_names(self): | ||
for conversion in self: | ||
conversion.view_names = ", ".join( | ||
conversion.view_ids.sorted(key=lambda l: l.id).mapped("name") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from odoo import _, api, fields, models | ||
|
||
|
||
class TargetTrigger(models.Model): | ||
_name = "ab.testing.target.trigger" | ||
_description = "Goal Trigger" | ||
|
||
name = fields.Char(compute="_compute_name",) | ||
|
||
target_id = fields.Many2one( | ||
string="Target", | ||
comodel_name="ab.testing.target", | ||
required=True, | ||
ondelete="cascade", | ||
) | ||
|
||
on = fields.Selection(string="On", selection=[("url_visit", "Url Visit")]) | ||
|
||
url = fields.Char(default="/") | ||
|
||
@api.depends("on", "url") | ||
def _compute_name(self): | ||
for trigger in self: | ||
name = "" | ||
if trigger.on == "url_visit": | ||
name = _("When visitors visit '%s'") % trigger.url | ||
trigger.name = name | ||
|
||
def create_conversion(self, date=None, variants=None): | ||
if variants is None: | ||
variants = self.env["ir.ui.view"].get_active_variants() | ||
if not date: | ||
date = fields.Datetime.now() | ||
for trigger in self: | ||
self.env["ab.testing.target.conversion"].create( | ||
{ | ||
"date": date, | ||
"view_ids": [(4, v.id) for v in variants], | ||
"trigger_id": trigger.id, | ||
} | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* Dennis Sluijk <[email protected]> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
This module adds A/B testing functionality to the CMS of Odoo. | ||
A/B testing a conversion rate optimization tool. | ||
In A/B testing you show multiple (mostly two) variants of the same page and determine | ||
by the rate of conversion which version / variant is best. | ||
|
||
More information on A/B testing can be found here: `<https://wikipedia.org/wiki/A/B_testing>`_ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
* E-commerce sale to conversion | ||
* Record retention time (or make some link with website.visitor) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
First you want to configure a target: | ||
|
||
#. Go to `Website` > `A/B Testing` > `Targets`; | ||
#. click `Create`; | ||
#. choose a name for your target e.g. 'More Sales' or 'More Members'; | ||
#. configure the triggers (triggers generate conversions e.g. when a certain page is visited or when a visitor bought a product from the webshop); | ||
#. click `Save`. | ||
|
||
Now we want to create different variants of our pages so we can potentially increase our conversion rate: | ||
|
||
* Go to the website editor; | ||
* go to your landing page; | ||
* on the top right click on `A/B Testing` and `New Variant` to create a different version of the page; | ||
* when you're done making variants make sure to enable A/B testing for the page by clicking the toggle in the `A/B Testing` menu | ||
|
||
When your test has ran for some time we can find out which variant is best. | ||
To find out which variant has lead to the most conversions, you can either | ||
go to `Website` > `A/B Testing` > `Conversions` or click the `Statistics` button on the Target form. |
Oops, something went wrong.