First commit

This commit is contained in:
Theodotos Andreou 2018-01-14 13:10:16 +00:00
commit c6e2478c40
13918 changed files with 2303184 additions and 0 deletions

View file

@ -0,0 +1,196 @@
<!--
Required vars: abtest, fields
Note: Much of this file is duplicated in crmMailing and crmMailingAB with variations on placement/title/binding.
It could perhaps be thinned by 30-60% by making more directives.
This template follows a basic pattern. For each included field, there are three variants, as in this example:
- fromAddress: The default From: address shared by both mailings (representatively mapped to mailing A)
- fromAddressA: The From: address for mailing A
- fromAddressB: The From: address for mailing B
Each variant is guarded with "ng-if='fields.fieldName'"; if true, the field will be displayed and
processed by Angular; if false, the field will be hidden and completely ignored by Angular.
-->
<div class="crm-block" ng-form="subform" crm-ui-id-scope>
<div class="crm-group">
<div crm-ui-field="{name: 'subform.msg_template_id', title: ts('Template')}" ng-if="fields.msg_template_id">
<div ng-controller="MsgTemplateCtrl">
<select
crm-ui-id="subform.msg_template_id"
name="msg_template_id"
class="fa-clipboard"
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Message Template')}"
ng-model="abtest.mailings.a.msg_template_id"
ng-change="loadTemplate(abtest.mailings.a, abtest.mailings.a.msg_template_id)"
>
<option value=""></option>
<option ng-repeat="frm in crmMsgTemplates.getAll() | orderBy:'msg_title'" ng-value="frm.id">{{frm.msg_title}}</option>
</select>
<a crm-icon="fa-floppy-o" ng-click="saveTemplate(abtest.mailings.a)" class="crm-hover-button" title="{{ts('Save As')}}"></a>
</div>
</div>
<div crm-ui-field="{name: 'subform.msg_template_idA', title: ts('Template (A)')}" ng-if="fields.msg_template_idA">
<div ng-controller="MsgTemplateCtrl">
<select
crm-ui-id="subform.msg_template_idA"
name="msg_template_idA"
class="fa-clipboard"
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Message Template')}"
ng-model="abtest.mailings.a.msg_template_id"
ng-change="loadTemplate(abtest.mailings.a, abtest.mailings.a.msg_template_id)"
>
<option value=""></option>
<option ng-repeat="frm in crmMsgTemplates.getAll() | orderBy:'msg_title'" ng-value="frm.id">{{frm.msg_title}}</option>
</select>
<a crm-icon="fa-floppy-o" ng-click="saveTemplate(abtest.mailings.a)" class="crm-hover-button" title="{{ts('Save As')}}"></a>
</div>
</div>
<div crm-ui-field="{name: 'subform.msg_template_idB', title: ts('Template (B)')}" ng-if="fields.msg_template_idB">
<div ng-controller="MsgTemplateCtrl">
<select
crm-ui-id="subform.msg_template_idB"
name="msg_template_idB"
class="fa-clipboard"
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Message Template')}"
ng-model="abtest.mailings.b.msg_template_id"
ng-change="loadTemplate(abtest.mailings.b, abtest.mailings.b.msg_template_id)"
>
<option value=""></option>
<option ng-repeat="frm in crmMsgTemplates.getAll() | orderBy:'msg_title'" ng-value="frm.id">{{frm.msg_title}}</option>
</select>
<a crm-icon="fa-floppy-o" ng-click="saveTemplate(abtest.mailings.b)" class="crm-hover-button" title="{{ts('Save As')}}"></a>
</div>
</div>
<div crm-ui-field="{name: 'subform.fromAddress', title: ts('From'), help: hs('from_email')}" ng-if="fields.fromAddress">
<span ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="abtest.mailings.a">
<select
crm-ui-id="subform.fromAddress"
crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('Email address')}"
name="fromAddress"
ng-model="fromPlaceholder.label"
required>
<option value=""></option>
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
</select>
</span>
</div>
<div crm-ui-field="{name: 'subform.fromAddressA', title: ts('From (A)'), help: hs('from_email')}" ng-if="fields.fromAddressA">
<span ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="abtest.mailings.a">
<select
crm-ui-id="subform.fromAddressA"
crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('Email address')}"
name="fromAddressA"
ng-model="fromPlaceholder.label"
required>
<option value=""></option>
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
</select>
</span>
</div>
<div crm-ui-field="{name: 'subform.fromAddressB', title: ts('From (B)'), help: hs('from_email')}" ng-if="fields.fromAddressB">
<span ng-controller="EmailAddrCtrl" crm-mailing-from-address="fromPlaceholder" crm-mailing="abtest.mailings.b">
<select
crm-ui-id="subform.fromAddressB"
crm-ui-select="{dropdownAutoWidth : true, allowClear: false, placeholder: ts('Email address')}"
name="fromAddressB"
ng-model="fromPlaceholder.label"
required>
<option value=""></option>
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
</select>
</span>
</div>
<div crm-ui-field="{name: 'subform.replyTo', title: ts('Reply-To')}" ng-show="crmMailingConst.enableReplyTo" ng-if="fields.replyTo">
<span ng-controller="EmailAddrCtrl">
<select
crm-ui-id="subform.replyTo"
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Email address')}"
name="replyTo"
ng-change="checkReplyToChange(abtest.mailings.a)"
ng-model="abtest.mailings.a.replyto_email"
>
<option value=""></option>
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
</select>
</span>
</div>
<div crm-ui-field="{name: 'subform.replyToA', title: ts('Reply-To (A)')}" ng-show="crmMailingConst.enableReplyTo" ng-if="fields.replyToA">
<span ng-controller="EmailAddrCtrl">
<select
crm-ui-id="subform.replyToA"
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Email address')}"
name="replyToA"
ng-change="checkReplyToChange(abtest.mailings.a)"
ng-model="abtest.mailings.a.replyto_email"
>
<option value=""></option>
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
</select>
</span>
</div>
<div crm-ui-field="{name: 'subform.replyToB', title: ts('Reply-To (B)')}" ng-show="crmMailingConst.enableReplyTo" ng-if="fields.replyToB">
<span ng-controller="EmailAddrCtrl">
<select
crm-ui-id="subform.replyToB"
crm-ui-select="{dropdownAutoWidth : true, allowClear: true, placeholder: ts('Email address')}"
name="replyToB"
ng-change="checkReplyToChange(abtest.mailings.b)"
ng-model="abtest.mailings.b.replyto_email"
>
<option value=""></option>
<option ng-repeat="frm in crmFromAddresses.getAll() | filter:{is_active:1} | orderBy:'weight'" value="{{frm.label}}">{{frm.label}}</option>
</select>
</span>
</div>
<div crm-ui-field="{name: 'subform.subject', title: ts('Subject')}" ng-if="fields.subject">
<div style="float: right;">
<input crm-mailing-token on-select="$broadcast('insert:subject', token.name)" tabindex="-1">
</div>
<input
crm-ui-id="subform.subject"
crm-ui-insert-rx="insert:subject"
type="text"
class="crm-form-text"
ng-model="abtest.mailings.a.subject"
required
placeholder="Subject"
name="subject" >
</div>
<div crm-ui-field="{name: 'subform.subjectA', title: ts('Subject (A)')}" ng-if="fields.subjectA">
<div style="float: right;">
<input crm-mailing-token on-select="$broadcast('insert:subjectA', token.name)" tabindex="-1">
</div>
<input
crm-ui-id="subform.subjectA"
crm-ui-insert-rx="insert:subjectA"
type="text"
class="crm-form-text"
ng-model="abtest.mailings.a.subject"
required
placeholder="Subject"
name="subjectA" >
</div>
<div crm-ui-field="{name: 'subform.subjectB', title: ts('Subject (B)')}" ng-if="fields.subjectB">
<div style="float: right;">
<input crm-mailing-token on-select="$broadcast('insert:subjectB', token.name)" tabindex="-1">
</div>
<input
crm-ui-id="subform.subjectB"
crm-ui-insert-rx="insert:subjectB"
type="text"
class="crm-form-text"
ng-model="abtest.mailings.b.subject"
required
placeholder="Subject"
name="subjectB" >
</div>
</div>
</div>

View file

@ -0,0 +1,32 @@
(function(angular, $, _) {
// example:
// scope.myAbtest = new CrmMailingAB();
// <crm-mailing-ab-block-mailing="{fromAddressA: 1, fromAddressB: 1}" crm-abtest="myAbtest" />
var simpleDirectives = {
crmMailingAbBlockMailing: '~/crmMailingAB/BlockMailing.html'
};
_.each(simpleDirectives, function(templateUrl, directiveName) {
angular.module('crmMailingAB').directive(directiveName, function($parse, crmMailingABCriteria, crmUiHelp) {
var scopeDesc = {crmAbtest: '@'};
scopeDesc[directiveName] = '@';
return {
scope: scopeDesc,
templateUrl: templateUrl,
link: function(scope, elm, attr) {
var model = $parse(attr.crmAbtest);
scope.abtest = model(scope.$parent);
scope.crmMailingConst = CRM.crmMailing;
scope.crmMailingABCriteria = crmMailingABCriteria;
scope.ts = CRM.ts(null);
scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
var fieldsModel = $parse(attr[directiveName]);
scope.fields = fieldsModel(scope.$parent);
}
};
});
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,67 @@
<div class="crm-block" ng-form="setupForm" crm-ui-id-scope>
<div class="crm-group">
<div class="help" ng-if="fields.help">
{{ts('A/B testing allows you to send two test mailings to a random subset of your recipients. After collecting and comparing metrics, the more successful mailing will be sent to the remaining recipients.')}}
</div>
<div crm-ui-field="{name: 'setupForm.abName', title: ts('Name'), help: hs('name')}" ng-if="fields.abName">
<input type="text"
crm-ui-id="setupForm.abName"
name="abName"
ng-model="abtest.ab.name"
class="crm-form-text"
placeholder="A/B Test Name"
required/>
</div>
<div crm-ui-field="{name: 'setupForm.campaign', title: ts('Campaign'), help: hs({id: 'id-campaign_id', file: 'CRM/Campaign/Form/addCampaignToComponent'})}" ng-show="crmMailingConst.campaignEnabled"
ng-if="fields.campaign">
<input
crm-entityref="{entity: 'Campaign', select: {allowClear: true, placeholder: ts('Select Campaign')}}"
crm-ui-id="setupForm.campaign"
name="campaign"
ng-model="abtest.mailings.a.campaign_id"
ng-change="abtest.mailings.b.campaign_id=abtest.mailings.a.campaign_id"
/>
</div>
<div crm-ui-field="{title: ts('Test Type')}" ng-if="fields.testing_criteria">
<div ng-repeat="criteria in crmMailingABCriteria.getAll()">
<label>
<input name="testing_criteria" ng-model="abtest.ab.testing_criteria" type="radio"
value="{{criteria.value}}" required/>
{{criteria.label}}
</label>
</div>
</div>
<div crm-ui-field="{name: 'setupForm.recipients', title: ts('Recipients')}" ng-if="fields.recipients">
<div crm-mailing-block-recipients="{name: 'recipients', id: 'setupForm.recipients'}" crm-mailing="abtest.mailings.a"></div>
</div>
<div crm-ui-field="{title: ts('Distribution')}" ng-if="fields.group_percentage">
<div crm-mailing-ab-slider ng-model="abtest.ab.group_percentage"></div>
</div>
<div crm-ui-field="{title: ts('Send')}" ng-if="fields.scheduled_date">
<div crm-mailing-radio-date="schedule" ng-model="abtest.mailings.a.scheduled_date">
<div>
<input ng-model="schedule.mode" type="radio" name="send" value="now" id="schedule-send-now"/>
<label for="schedule-send-now">{{ts('Send A/B test immediately')}}</label>
</div>
<div>
<input ng-model="schedule.mode" type="radio" name="send" value="at" id="schedule-send-at"/>
<label for="schedule-send-at">{{ts('Send A/B test at:')}}</label>
<input crm-ui-datepicker ng-model="schedule.datetime"/>
</div>
</div>
</div>
<div crm-ui-field="{title: ts('Assess')}" ng-if="fields.declare_winning_time">
<div crm-mailing-radio-date="assessSched" ng-model="abtest.ab.declare_winning_time">
<div>
<input ng-model="assessSched.mode" type="radio" name="assess" value="now" id="schedule-assess-now"/>
<label for="schedule-assess-now">{{ts('Assess A/B results on an on-going basis')}}</label>
</div>
<div>
<input ng-model="assessSched.mode" type="radio" name="assess" value="at" id="schedule-assess-at"/>
<label for="schedule-assess-at">{{ts('Assess A/B test at:')}}</label>
<input crm-ui-datepicker ng-model="assessSched.datetime"/>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,32 @@
(function(angular, $, _) {
// example:
// scope.myAbtest = new CrmMailingAB();
// <crm-mailing-ab-block-setup="{abName: 1, group_percentage: 1}" crm-abtest="myAbtest" />
var simpleDirectives = {
crmMailingAbBlockSetup: '~/crmMailingAB/BlockSetup.html'
};
_.each(simpleDirectives, function(templateUrl, directiveName) {
angular.module('crmMailingAB').directive(directiveName, function($parse, crmMailingABCriteria, crmUiHelp) {
var scopeDesc = {crmAbtest: '@'};
scopeDesc[directiveName] = '@';
return {
scope: scopeDesc,
templateUrl: templateUrl,
link: function(scope, elm, attr) {
var model = $parse(attr.crmAbtest);
scope.abtest = model(scope.$parent);
scope.crmMailingConst = CRM.crmMailing;
scope.crmMailingABCriteria = crmMailingABCriteria;
scope.ts = CRM.ts(null);
scope.hs = crmUiHelp({file: 'CRM/Mailing/MailingUI'});
var fieldsModel = $parse(attr[directiveName]);
scope.fields = fieldsModel(scope.$parent);
}
};
});
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,149 @@
(function(angular, $, _) {
angular.module('crmMailingAB').controller('CrmMailingABEditCtrl', function($scope, abtest, crmMailingABCriteria, crmMailingMgr, crmMailingPreviewMgr, crmStatus, $q, $location, crmBlocker, $interval, $timeout, CrmAutosaveCtrl, dialogService) {
$scope.abtest = abtest;
var ts = $scope.ts = CRM.ts(null);
var block = $scope.block = crmBlocker();
$scope.crmUrl = CRM.url;
var myAutosave = null;
$scope.crmMailingABCriteria = crmMailingABCriteria;
$scope.crmMailingConst = CRM.crmMailing;
$scope.checkPerm = CRM.checkPerm;
$scope.isSubmitted = function isSubmitted() {
return _.size(abtest.mailings.a.jobs) > 0 || _.size(abtest.mailings.b.jobs) > 0;
};
$scope.sync = function sync() {
abtest.mailings.a.name = ts('Test A (%1)', {1: abtest.ab.name});
abtest.mailings.b.name = ts('Test B (%1)', {1: abtest.ab.name});
abtest.mailings.c.name = ts('Final (%1)', {1: abtest.ab.name});
if (abtest.ab.testing_criteria) {
// TODO review fields exposed in UI and make sure the sync rules match
switch (abtest.ab.testing_criteria) {
case 'subject':
var exclude_subject = [
'name',
'recipients',
'subject'
];
crmMailingMgr.mergeInto(abtest.mailings.b, abtest.mailings.a, exclude_subject);
crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings.a, exclude_subject);
break;
case 'from':
var exclude_from = [
'name',
'recipients',
'from_name',
'from_email'
];
crmMailingMgr.mergeInto(abtest.mailings.b, abtest.mailings.a, exclude_from);
crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings.a, exclude_from);
break;
case 'full_email':
var exclude_full_email = [
'name',
'recipients',
'subject',
'from_name',
'from_email',
'replyto_email',
'override_verp', // keep override_verp and replyto_Email linked
'body_html',
'body_text'
];
crmMailingMgr.mergeInto(abtest.mailings.b, abtest.mailings.a, exclude_full_email);
crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings.a, exclude_full_email);
break;
default:
throw "Unrecognized testing_criteria";
}
}
return true;
};
// @return Promise
$scope.save = function save() {
return block(crmStatus({start: ts('Saving...'), success: ts('Saved')}, abtest.save()));
};
// @return Promise
$scope.previewMailing = function previewMailing(mailingName, mode) {
return crmMailingPreviewMgr.preview(abtest.mailings[mailingName], mode);
};
// @return Promise
$scope.sendTest = function sendTest(mailingName, recipient) {
return block(crmStatus({start: ts('Saving...'), success: ''}, abtest.save())
.then(function() {
crmMailingPreviewMgr.sendTest(abtest.mailings[mailingName], recipient);
}));
};
// @return Promise
$scope.delete = function() {
return block(crmStatus({start: ts('Deleting...'), success: ts('Deleted')}, abtest.delete().then($scope.leave)));
};
// @return Promise
$scope.submit = function submit() {
if (block.check() || $scope.crmMailingAB.$invalid) {
return;
}
return block(crmStatus({start: ts('Saving...'), success: ''}, abtest.save())
.then(function() {
return crmStatus({
start: ts('Submitting...'),
success: ts('Submitted')
}, myAutosave.suspend(abtest.submitTest()));
// Note: We're going to leave, so we don't care that submit() modifies several server-side records.
// If we stayed on this page, then we'd care about updating and call: abtest.submitTest().then(...abtest.load()...)
})
);
};
$scope.leave = function leave() {
$location.path('abtest');
$location.replace();
};
$scope.selectWinner = function selectWinner(mailingName) {
var model = {
abtest: $scope.abtest,
mailingName: mailingName
};
var options = CRM.utils.adjustDialogDefaults({
autoOpen: false,
height: 'auto',
width: '40%',
title: ts('Select Final Mailing (Test %1)', {
1: mailingName.toUpperCase()
})
});
return myAutosave.suspend(dialogService.open('selectWinnerDialog', '~/crmMailingAB/WinnerDialogCtrl.html', model, options));
};
// initialize
var syncJob = $interval($scope.sync, 333);
$scope.$on('$destroy', function() {
$interval.cancel(syncJob);
});
myAutosave = new CrmAutosaveCtrl({
save: $scope.save,
saveIf: function() {
return abtest.ab.status == 'Draft' && $scope.sync();
},
model: function() {
return abtest.getAutosaveSignature();
},
form: function() {
return $scope.crmMailingAB;
}
});
$timeout(myAutosave.start);
$scope.$on('$destroy', myAutosave.stop);
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,178 @@
<!--
Implicit Controller: CrmMailingABEditCtrl
An ABTest includes two mailings, but we don't require the user to enter two complete mailings. For
simplicity, the email composition UI generally displays A (unless we specifically decided to expose an
individual field from B). At the end of the composition process, the controller's "sync" operation will
merge shared settings from "A" into "B".
-->
<div ng-form="crmMailingABEdit">
<div class="crm-block crm-form-block crmMailing">
<div crm-ui-wizard>
<div crm-ui-wizard-step="10" crm-title="ts('Setup')" ng-form="setupForm">
<div
crm-mailing-ab-block-setup="{
help: 1,
abName: 1,
campaign: 1,
testing_criteria: 1
}"
crm-abtest="abtest"></div>
</div>
<div crm-ui-wizard-step="11" crm-title="ts('Target')" ng-form="targetForm">
<div
crm-mailing-ab-block-setup="{
recipients: 1,
group_percentage: 1
}"
crm-abtest="abtest"></div>
</div>
<div crm-ui-wizard-step="20" crm-title="ts('Compose')" ng-if="abtest.ab.testing_criteria != 'full_email'" ng-form="composeForm">
<div crm-ui-tab-set>
<div crm-ui-tab id="tab-mailing" crm-title="ts('Mailing')">
<div
ng-if="abtest.ab.testing_criteria == 'from'"
crm-mailing-ab-block-mailing="{
msg_template_id: 1,
fromAddressA: 1,
fromAddressB: 1,
subject: 1
}"
crm-abtest="abtest"></div>
<div
ng-if="abtest.ab.testing_criteria == 'subject'"
crm-mailing-ab-block-mailing="{
msg_template_id: 1,
fromAddress: 1,
replyTo: 1,
subjectA: 1,
subjectB: 1
}"
crm-abtest="abtest"></div>
<div crm-ui-accordion="{title: ts('HTML')}">
<div crm-mailing-body-html crm-mailing="abtest.mailings.a"></div>
</div>
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !abtest.mailings.a.body_text}">
<div crm-mailing-body-text crm-mailing="abtest.mailings.a"></div>
</div>
</div>
<!--
FIXME: Attachment UI works, but we haven't implemented backend logic for copying/sharing
of attachments among mailings A/B/C.
<div crm-ui-tab id="tab-attachment" crm-title="ts('Attachments')">
<div crm-attachments="abtest.attachments.a"></div>
</div>
-->
<div crm-ui-tab id="tab-header" crm-title="ts('Header and Footer')">
<div crm-mailing-block-header-footer crm-mailing="abtest.mailings.a"></div>
</div>
<div crm-ui-tab id="tab-pub" crm-title="ts('Publication')">
<div crm-mailing-block-publication crm-mailing="abtest.mailings.a"></div>
</div>
<div crm-ui-tab id="tab-response" crm-title="ts('Responses')">
<div crm-mailing-block-responses crm-mailing="abtest.mailings.a"></div>
</div>
</div>
<div crm-ui-accordion="{title: ts('Preview (A)')}">
<div crm-mailing-block-preview crm-mailing="abtest.mailings.a" on-preview="previewMailing('a', preview.mode)" on-send="sendTest('a', preview.recipient)"></div>
</div>
<div crm-ui-accordion="{title: ts('Preview (B)')}">
<div crm-mailing-block-preview crm-mailing="abtest.mailings.b" on-preview="previewMailing('b', preview.mode)" on-send="sendTest('b', preview.recipient)"></div>
</div>
</div>
<div crm-ui-wizard-step="21" crm-title="ts('Compose (A)')" ng-if="abtest.ab.testing_criteria == 'full_email'" ng-form="composeAForm">
<div crm-ui-tab-set>
<div crm-ui-tab id="tab-mailingA" crm-title="ts('Mailing')">
<div
crm-mailing-ab-block-mailing="{
msg_template_idA: 1,
fromAddressA: 1,
replyToA: 1,
subjectA: 1
}"
crm-abtest="abtest"></div>
<div crm-ui-accordion="{title: ts('HTML')}">
<div crm-mailing-body-html crm-mailing="abtest.mailings.a"></div>
</div>
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !abtest.mailings.a.body_text}">
<div crm-mailing-body-text crm-mailing="abtest.mailings.a"></div>
</div>
</div>
<div crm-ui-tab id="tab-attachmentA" crm-title="ts('Attachments')">
<div crm-attachments="abtest.attachments.a"></div>
</div>
<div crm-ui-tab id="tab-headerA" crm-title="ts('Header and Footer')">
<div crm-mailing-block-header-footer crm-mailing="abtest.mailings.a"></div>
</div>
<div crm-ui-tab id="tab-pubA" crm-title="ts('Publication')">
<div crm-mailing-block-publication crm-mailing="abtest.mailings.a"></div>
</div>
<div crm-ui-tab id="tab-responseA" crm-title="ts('Responses')">
<div crm-mailing-block-responses crm-mailing="abtest.mailings.a"></div>
</div>
</div>
<div crm-ui-accordion="{title: ts('Preview')}">
<div crm-mailing-block-preview crm-mailing="abtest.mailings.a" on-preview="previewMailing('a', preview.mode)" on-send="sendTest('a', preview.recipient)"></div>
</div>
</div>
<div crm-ui-wizard-step="22" crm-title="ts('Compose (B)')" ng-if="abtest.ab.testing_criteria == 'full_email'" ng-form="composeBForm">
<div crm-ui-tab-set>
<div crm-ui-tab id="tab-mailingB" crm-title="ts('Mailing')">
<div
crm-mailing-ab-block-mailing="{
msg_template_idB: 1,
fromAddressB: 1,
replyToB: 1,
subjectB: 1
}"
crm-abtest="abtest"></div>
<div crm-ui-accordion="{title: ts('HTML')}">
<div crm-mailing-body-html crm-mailing="abtest.mailings.b"></div>
</div>
<div crm-ui-accordion="{title: ts('Plain Text'), collapsed: !abtest.mailings.b.body_text}">
<div crm-mailing-body-text crm-mailing="abtest.mailings.b"></div>
</div>
</div>
<div crm-ui-tab id="tab-attachmentB" crm-title="ts('Attachments')">
<div crm-attachments="abtest.attachments.b"></div>
</div>
<div crm-ui-tab id="tab-headerB" crm-title="ts('Header and Footer')">
<div crm-mailing-block-header-footer crm-mailing="abtest.mailings.b"></div>
</div>
<div crm-ui-tab id="tab-pubB" crm-title="ts('Publication')">
<div crm-mailing-block-publication crm-mailing="abtest.mailings.b"></div>
</div>
<div crm-ui-tab id="tab-responseB" crm-title="ts('Responses')">
<div crm-mailing-block-responses crm-mailing="abtest.mailings.b"></div>
</div>
</div>
<div crm-ui-accordion="{title: ts('Preview')}">
<div crm-mailing-block-preview crm-mailing="abtest.mailings.b" on-preview="previewMailing('b', preview.mode)" on-send="sendTest('b', preview.recipient)"></div>
</div>
</div>
<div crm-ui-wizard-step="30" crm-title="ts('Schedule')" ng-form="schedForm">
<div
crm-mailing-ab-block-setup="{
scheduled_date: 1,
declare_winning_time: 1
}"
crm-abtest="abtest"></div>
<center>
<a class="button crmMailing-submit-button" ng-click="submit()" ng-class="{blocking: block.check(), disabled: crmMailingAB.$invalid}">
<div>{{ts('Submit Mailing')}}</div>
</a>
</center>
</div>
<span crm-ui-wizard-buttons style="float:right;">
<button
crm-icon="fa-trash"
ng-show="checkPerm('delete in CiviMail')"
ng-disabled="block.check()"
crm-confirm="{title:ts('Delete Draft'), message:ts('Are you sure you want to permanently delete this mailing?')}"
on-yes="delete()">{{ts('Delete Draft')}}
</button>
<button crm-icon="fa-floppy-o" ng-disabled="block.check()" ng-click="save().then(leave) ">{{ts('Save Draft')}}</button>
</span>
</div>
</div>
</div>

View file

@ -0,0 +1,10 @@
<!--
Implicit Controller: CrmMailingABEditCtrl
-->
<div crm-ui-debug="abtest.ab"></div>
<div crm-ui-debug="abtest.mailings"></div>
<form name="crmMailingAB" novalidate>
<div ng-include="'~/crmMailingAB/EditCtrl/edit.html'" ng-if="!isSubmitted()"></div>
<div ng-include="'~/crmMailingAB/EditCtrl/report.html'" ng-if="isSubmitted()"></div>
</form>

View file

@ -0,0 +1,194 @@
<!--
Implicit Controller: CrmMailingABEditCtrl
-->
<div class="messages help">
<div class="msg-title crm-title">{{ts('A/B Test Results')}}: {{abtest.ab.name}}</div>
{{ts('This report displays the current results for your A/B test. You can return to this page to view the latest statistics by navigating to "Manage A/B Tests" and clicking "Results".')}}
</div>
<div ng-controller="CrmMailingABReportCtrl">
<table class="crm-mailing-ab-table">
<thead>
<tr ng-show="abtest.ab.status == 'Testing'">
<td></td>
<td ng-repeat="am in getActiveMailings()">
<button crm-icon="fa-trophy" ng-click="selectWinner(am.name)">{{ts('Select as Final')}}</button>
</td>
<td></td>
</tr>
</thead>
<thead>
<tr>
<th>{{ts('Delivery')}}</th>
<th ng-repeat="am in getActiveMailings()" class="crm-mailing-ab-col">{{am.title}}</th>
<th ng-show="abtest.ab.status == 'Testing'">{{ts('Final')}}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ts('Status')}}</td>
<td ng-repeat="am in getActiveMailings()">
<span ng-repeat="job in am.mailing.jobs" ng-hide="job.is_test == 1 || job.parent_id != null">{{job.status}}</span>
</td>
<td ng-show="abtest.ab.status == 'Testing'">{{ts('Not selected')}}</td>
</tr>
<tr>
<td>{{ts('Scheduled')}}</td>
<td ng-repeat="am in getActiveMailings()">
<div ng-repeat="job in am.mailing.jobs" ng-hide="job.is_test == 1 || job.parent_id != null">{{job.scheduled_date}}</div>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('Started at')}}</td>
<td ng-repeat="am in getActiveMailings()">
<div ng-repeat="job in am.mailing.jobs" ng-hide="job.is_test == 1 || job.parent_id != null">{{job.start_date || ts('Not started')}}</div>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('Completed at')}}</td>
<td ng-repeat="am in getActiveMailings()">
<div ng-repeat="job in am.mailing.jobs" ng-hide="job.is_test == 1 || job.parent_id != null">{{job.end_date || ts('Not completed')}}</div>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
</tbody>
<thead>
<tr>
<th>{{ts('Performance')}}</th>
<th ng-repeat="am in getActiveMailings()" class="crm-mailing-ab-col">{{am.title}}</th>
<th ng-show="abtest.ab.status == 'Testing'">{{ts('Final')}}</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="statType in statTypes">
<td>{{statType.title}}</td>
<td ng-repeat="am in getActiveMailings()">
<a
class="crm-hover-button action-item"
ng-href="{{statUrl(am.mailing, statType, 'search')}}"
ng-if="checkPerm('view all contacts') || checkPerm('edit all contacts')"
title="{{ts('Search for contacts using \'%1\'', {1: statType.title})}}"
crm-icon="fa-search"
></a>
<a
class="crm-hover-button action-item"
ng-href="{{statUrl(am.mailing, statType, 'events')}}"
title="{{ts('Browse events of type \'%1\'', {1: statType.title})}}"
>{{stats[am.name][statType.name] || ts('n/a')}}</a>
<a
class="crm-hover-button action-item"
ng-href="{{statUrl(am.mailing, statType, 'report')}}"
title="{{ts('Reports for \'%1\'', {1: statType.title})}}"
crm-icon="clipboard"
></a>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
</tbody>
<thead>
<tr>
<th>{{ts('Details')}}</th>
<th ng-repeat="am in getActiveMailings()" class="crm-mailing-ab-col">{{am.title}}</th>
<th ng-show="abtest.ab.status == 'Testing'">{{ts('Final')}}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ts('Mailing Name')}}</td>
<td ng-repeat="am in getActiveMailings()">
{{am.mailing.name}}
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('From')}}</td>
<td ng-repeat="am in getActiveMailings()">
"{{am.mailing.from_name}}" &lt;{{am.mailing.from_email}}&gt;
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('Subject')}}</td>
<td ng-repeat="am in getActiveMailings()">
{{am.mailing.subject}}
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr ng-controller="ViewRecipCtrl">
<td>{{ts('Recipients')}}</td>
<td ng-repeat="am in getActiveMailings()">
<div ng-show="getIncludesAsString(am.mailing)">
<strong>{{ts('Include:')}}</strong> {{getIncludesAsString(am.mailing)}}
</div>
<div ng-show="getExcludesAsString(am.mailing)">
<strong>{{ts('Exclude:')}}</strong> <s>{{getExcludesAsString(am.mailing)}}</s>
</div>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('Content')}}</td>
<td ng-repeat="am in getActiveMailings()">
<a crm-icon="fa-television" class="crm-hover-button action-item" ng-click="previewMailing(am.name,'html')" ng-show="am.mailing.body_html">{{ts('HTML')}}</a>
<a crm-icon="fa-file-text-o" class="crm-hover-button action-item" ng-click="previewMailing(am.name,'text')" ng-show="am.mailing.body_text">{{ts('Text')}}</a>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('Attachments')}}</td>
<td ng-repeat="am in getActiveMailings()">
<div ng-repeat="file in am.attachments.files"><a ng-href="{{file.url}}" target="_blank">{{file.name}}</a></div>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('Tracking')}}</td>
<td ng-repeat="am in getActiveMailings()">
<div crm-mailing-review-bool crm-on="am.mailing.url_tracking=='1'" crm-title="ts('Click-Throughs')"></div>
<div crm-mailing-review-bool crm-on="am.mailing.open_tracking=='1'" crm-title="ts('Opens')"></div>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('Responding')}}</td>
<td ng-repeat="am in getActiveMailings()">
<div crm-mailing-review-bool crm-on="am.mailing.override_verp=='0'" crm-title="ts('Track Replies')"></div>
<div crm-mailing-review-bool crm-on="am.mailing.override_verp=='0' && mailing.forward_replies=='1'" crm-title="ts('Forward Replies')"></div>
<div ng-controller="PreviewComponentCtrl">
<div ng-show="am.mailing.override_verp == '0' && mailing.auto_responder"><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Auto-Respond'), am.mailing.reply_id)">{{ts('Auto-Respond')}}</a></div>
<div><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Opt-out'), am.mailing.optout_id)">{{ts('Opt-out')}}</a></div>
<div><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Resubscribe'), am.mailing.resubscribe_id)">{{ts('Resubscribe')}}</a></div>
<div><a crm-icon="fa-envelope" class="crm-hover-button action-item" ng-click="previewComponent(ts('Unsubscribe'), am.mailing.unsubscribe_id)">{{ts('Unsubscribe')}}</a></div>
</div>
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
<tr>
<td>{{ts('Publication')}}</td>
<td ng-repeat="am in getActiveMailings()">
{{am.mailing.visibility}}
</td>
<td ng-show="abtest.ab.status == 'Testing'"></td>
</tr>
</tbody>
</table>
<!--
<div crm-ui-tab-set>
<div crm-ui-tab id="tab-opens" crm-title="ts('Opens (WIP)')">
<div crm-mailing-ab-stats="{criteria: 'open', split_count: 5}" crm-abtest="abtest"></div>
</div>
<div crm-ui-tab id="tab-clicks" crm-title="ts('Total Clicks (WIP)')">
<div crm-mailing-ab-stats="{criteria: 'total unique clicks', split_count: 5}" crm-abtest="abtest"></div>
</div>
</div>
-->
</div>

View file

@ -0,0 +1,63 @@
<!--
Controller: ABListingCtrl
Required vars: mailingABList
-->
<span crm-ui-order="{var: 'myOrder', defaults: ['-created_date']}"></span>
<div crm-ui-accordion="{title: ts('Filter'), collapsed: true}">
<form name="filterForm">
<span>
<input class="big crm-form-text" ng-model="filter.name" placeholder="{{ts('Name')}}"/>
</span>
<span>
<select crm-ui-select style="width: 10em;" ng-model="filter.status">
<option value="">{{ts('- Status -')}}</option>
<option ng-repeat="o in fields.status.options" ng-value="o.key">{{o.value}}</option>
</select>
</span>
<span>
<select crm-ui-select style="width: 20em;" ng-model="filter.testing_criteria">
<option value="">{{ts('- Test Type -')}}</option>
<option ng-repeat="o in fields.testing_criteria.options" ng-value="o.key">{{o.value}}</option>
</select>
</span>
</form>
</div>
<div ng-show="mailingABList.length">
<table class="display">
<thead>
<tr>
<th><a crm-ui-order-by="[myOrder, 'name']">{{ts('Name')}}</a></th>
<th><a crm-ui-order-by="[myOrder, 'status']">{{ts('Status')}}</a></th>
<th><a crm-ui-order-by="[myOrder, 'testing_criteria']">{{ts('Test Type')}}</a></th>
<th><a crm-ui-order-by="[myOrder, 'created_date']">{{ts('Created')}}</a></th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="mailingAB in mailingABList | filter:filter | orderBy:myOrder.get()">
<td>{{mailingAB.name}}</td>
<td>{{crmMailingABStatus.getByName(mailingAB.status).label}}</td>
<td>{{crmMailingABCriteria.get(mailingAB.testing_criteria).label}}</td>
<td>{{mailingAB.created_date}}</td>
<td>
<a class="action-item crm-hover-button" ng-href="#/abtest/{{mailingAB.id}}" ng-show="mailingAB.status == 'Draft'">{{ts('Continue')}}</a>
<a class="action-item crm-hover-button" ng-href="#/abtest/{{mailingAB.id}}" ng-show="mailingAB.status != 'Draft'">{{ts('Results')}}</a>
</td>
</tr>
</tbody>
</table>
</div>
<div ng-show="mailingABList.length === 0" class="messages status no-popup">
<i class="crm-i fa-info-circle"></i>
{{ts('You have no A/B mailings')}}
</div>
<div class="crm-submit-buttons">
<br>
<a ng-href="#/abtest/new" class="button"><span><i class="crm-i fa-bar-chart"></i> {{ts('New A/B Test')}}</span></a>
</div>

View file

@ -0,0 +1,12 @@
(function(angular, $, _) {
angular.module('crmMailingAB').controller('CrmMailingABListCtrl', function($scope, mailingABList, crmMailingABCriteria, crmMailingABStatus, fields) {
var ts = $scope.ts = CRM.ts(null);
$scope.mailingABList = _.values(mailingABList.values);
$scope.crmMailingABCriteria = crmMailingABCriteria;
$scope.crmMailingABStatus = crmMailingABStatus;
$scope.fields = fields;
$scope.filter = {};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,11 @@
(function(angular, $, _) {
angular.module('crmMailingAB').controller('CrmMailingABNewCtrl', function($scope, abtest, $location) {
// Transition URL "/abtest/new/foo" => "/abtest/123/foo"
var parts = $location.path().split('/'); // e.g. "/mailing/new" or "/mailing/123/wizard"
parts[2] = abtest.id;
$location.path(parts.join('/'));
$location.replace();
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,52 @@
(function(angular, $, _) {
angular.module('crmMailingAB').controller('CrmMailingABReportCtrl', function($scope, crmApi, crmMailingStats) {
var ts = $scope.ts = CRM.ts(null);
var CrmMailingABReportCnt = 1, activeMailings = null;
$scope.getActiveMailings = function() {
if ($scope.abtest.$CrmMailingABReportCnt != CrmMailingABReportCnt) {
$scope.abtest.$CrmMailingABReportCnt = ++CrmMailingABReportCnt;
activeMailings = [
{
name: 'a',
title: ts('Mailing A'),
mailing: $scope.abtest.mailings.a,
attachments: $scope.abtest.attachments.a
},
{
name: 'b',
title: ts('Mailing B'),
mailing: $scope.abtest.mailings.b,
attachments: $scope.abtest.attachments.b
}
];
if ($scope.abtest.ab.status == 'Final') {
activeMailings.push({
name: 'c',
title: ts('Final'),
mailing: $scope.abtest.mailings.c,
attachments: $scope.abtest.attachments.c
});
}
}
return activeMailings;
};
crmMailingStats.getStats({
a: $scope.abtest.ab.mailing_id_a,
b: $scope.abtest.ab.mailing_id_b,
c: $scope.abtest.ab.mailing_id_c
}).then(function(stats) {
$scope.stats = stats;
});
$scope.statTypes = crmMailingStats.getStatTypes();
$scope.statUrl = function statUrl(mailing, statType, view) {
return crmMailingStats.getUrl(mailing, statType, view, 'abtest/' + $scope.abtest.ab.id);
};
$scope.checkPerm = CRM.checkPerm;
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,25 @@
<table class="crm-mailing-ab-slider">
<tbody>
<tr>
<td style="width: 10em;">{{ts('Test Mailing A')}}</td>
<td>
<div class="slider-test slider-a"></div>
</td>
<td style="width: 5em;">({{testValue}}%)</td>
</tr>
<tr>
<td>{{ts('Test Mailing B')}}</td>
<td>
<div class="slider-test slider-b"></div>
</td>
<td>({{testValue}}%)</td>
</tr>
</tbody>
<tr>
<td>{{ts('Final Mailing')}}</td>
<td>
<div class="slider-win slider-b"></div>
</td>
<td>({{winValue}}%)</td>
</tr>
</table>

View file

@ -0,0 +1,60 @@
(function(angular, $, _) {
// example: <div crm-mailing-ab-slider ng-model="abtest.ab.group_percentage"></div>
angular.module('crmMailingAB').directive('crmMailingAbSlider', function() {
return {
require: '?ngModel',
scope: {},
templateUrl: '~/crmMailingAB/Slider.html',
link: function(scope, element, attrs, ngModel) {
var TEST_MIN = 1, TEST_MAX = 50;
var sliders = $('.slider-test,.slider-win', element);
var sliderTests = $('.slider-test', element);
var sliderWin = $('.slider-win', element);
scope.ts = CRM.ts(null);
scope.testValue = 0;
scope.winValue = 100;
// set the base value (following a GUI event)
function setValue(value) {
value = Math.min(TEST_MAX, Math.max(TEST_MIN, value));
scope.$apply(function() {
ngModel.$setViewValue(value);
scope.testValue = value;
scope.winValue = 100 - (2 * scope.testValue);
sliderTests.slider('value', scope.testValue);
sliderWin.slider('value', scope.winValue);
});
}
sliders.slider({
min: 0,
max: 100,
range: 'min',
step: 1
});
sliderTests.slider({
slide: function slideTest(event, ui) {
event.preventDefault();
setValue(ui.value);
}
});
sliderWin.slider({
slide: function slideWinner(event, ui) {
event.preventDefault();
setValue(Math.round((100 - ui.value) / 2));
}
});
ngModel.$render = function() {
scope.testValue = ngModel.$viewValue;
scope.winValue = 100 - (2 * scope.testValue);
sliderTests.slider('value', scope.testValue);
sliderWin.slider('value', scope.winValue);
};
}
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,280 @@
(function (angular, $, _) {
// FIXME: This code is long and hasn't been fully working for me, but I've moved it into a spot
// where it at least fits in a bit better.
// example: <div crm-mailing-ab-stats="{split_count: 6, criteria:'Open'}" crm-abtest="myabtest" />
// options (see also: Mailing.graph_stats API)
// - split_count: int
// - criteria: string
// - target_date: string, date
// - target_url: string
angular.module('crmMailingAB').directive('crmMailingAbStats', function (crmApi, $parse) {
return {
scope: {
crmMailingAbStats: '@',
crmAbtest: '@'
},
template: '<div class="crm-mailing-ab-stats"></div>',
link: function (scope, element, attrs) {
var abtestModel = $parse(attrs.crmAbtest);
var optionModel = $parse(attrs.crmMailingAbStats);
var options = angular.extend({}, optionModel(scope.$parent), {
criteria: 'Open', // e.g. 'Open', 'Total Unique Clicks'
split_count: 5
});
scope.$watch(attrs.crmAbtest, refresh);
function refresh() {
var abtest = abtestModel(scope.$parent);
if (!abtest) {
console.log('failed to draw stats - missing abtest');
return;
}
scope.graph_data = [
{},
{},
{},
{},
{}
];
var keep_cnt = 0;
for (var i = 1; i <= options.split_count; i++) {
var result = crmApi('MailingAB', 'graph_stats', {
id: abtest.ab.id,
target_date: abtest.ab.declare_winning_time ? abtest.ab.declare_winning_time : 'now',
target_url: null, // FIXME
criteria: options.criteria,
split_count: options.split_count,
split_count_select: i
});
/*jshint -W083 */
result.then(function (data) {
var temp = 0;
keep_cnt++;
for (var key in data.values.A) {
temp = key;
}
var t = data.values.A[temp].time.split(" ");
var m = t[0];
var year = t[2];
var day = t[1].substr(0, t[1].length - 3);
var t1, hur, hour, min;
if (_.isEmpty(t[3])) {
t1 = t[4].split(":");
hur = t1[0];
if (t[5] == "AM") {
hour = hur;
if (hour == 12) {
hour = 0;
}
}
if (t[5] == "PM") {
hour = parseInt(hur) + 12;
}
min = t1[1];
}
else {
t1 = t[3].split(":");
hur = t1[0];
if (t[4] == "AM") {
hour = hur;
if (hour == 12) {
hour = 0;
}
}
if (t[4] == "PM") {
hour = parseInt(hur) + 12;
}
min = t1[1];
}
var month = 0;
switch (m) {
case "January":
month = 0;
break;
case "February":
month = 1;
break;
case "March":
month = 2;
break;
case "April":
month = 3;
break;
case "May":
month = 4;
break;
case "June":
month = 5;
break;
case "July":
month = 6;
break;
case "August":
month = 7;
break;
case "September":
month = 8;
break;
case "October":
month = 9;
break;
case "November":
month = 10;
break;
case "December":
month = 11;
break;
}
var tp = new Date(year, month, day, hour, min, 0, 0);
scope.graph_data[temp - 1] = {
time: tp,
x: data.values.A[temp].count,
y: data.values.B[temp].count
};
if (keep_cnt == options.split_count) {
scope.graphload = true;
data = scope.graph_data;
// set up a colour variable
var color = d3.scale.category10();
// map one colour each to x, y and z
// keys grabs the key value or heading of each key value pair in the json
// but not time
color.domain(d3.keys(data[0]).filter(function (key) {
return key !== "time";
}));
// create a nested series for passing to the line generator
// it's best understood by console logging the data
var series = color.domain().map(function (name) {
return {
name: name,
values: data.map(function (d) {
return {
time: d.time,
score: +d[name]
};
})
};
});
// Set the dimensions of the canvas / graph
var margin = {
top: 30,
right: 20,
bottom: 40,
left: 75
},
width = 550 - margin.left - margin.right,
height = 350 - margin.top - margin.bottom;
// Set the ranges
//var x = d3.time.scale().range([0, width]).domain([0,10]);
var x = d3.time.scale().range([0, width]);
var y = d3.scale.linear().range([height, 0]);
// Define the axes
var xAxis = d3.svg.axis().scale(x)
.orient("bottom").ticks(10);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
// Define the line
// Note you plot the time / score pair from each key you created earlier
var valueline = d3.svg.line()
.x(function (d) {
return x(d.time);
})
.y(function (d) {
return y(d.score);
});
// Adds the svg canvas
var svg = d3.select($('.crm-mailing-ab-stats', element)[0])
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Scale the range of the data
x.domain(d3.extent(data, function (d) {
return d.time;
}));
// note the nested nature of this you need to dig an additional level
y.domain([
d3.min(series, function (c) {
return d3.min(c.values, function (v) {
return v.score;
});
}),
d3.max(series, function (c) {
return d3.max(c.values, function (v) {
return v.score;
});
})
]);
svg.append("text") // text label for the x axis
.attr("x", width / 2)
.attr("y", height + margin.bottom)
.style("text-anchor", "middle")
.text("Time");
svg.append("text") // text label for the x axis
.style("text-anchor", "middle")
.text(scope.winnercriteria).attr("transform",function (d) {
return "rotate(-90)";
}).attr("x", -height / 2)
.attr("y", -30);
// create a variable called series and bind the date
// for each series append a g element and class it as series for css styling
series = svg.selectAll(".series")
.data(series)
.enter().append("g")
.attr("class", "series");
// create the path for each series in the variable series i.e. x, y and z
// pass each object called x, y nad z to the lne generator
series.append("path")
.attr("class", "line")
.attr("d", function (d) {
// console.log(d); // to see how d3 iterates through series
return valueline(d.values);
})
.style("stroke", function (d) {
return color(d.name);
});
// Add the X Axis
svg.append("g") // Add the X Axis
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.attr("transform", function (d) {
return "rotate(-30)";
});
// Add the Y Axis
svg.append("g") // Add the Y Axis
.attr("class", "y axis")
.call(yAxis);
}
});
}
}
} // link()
};
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,19 @@
<div ng-controller="CrmMailingABWinnerDialogCtrl">
<form novalidate name="winnerForm">
<div class="help">
{{ts('After selecting %1 as the winner, one must schedule the delivery for the final mailing.', {1: mailingTitle})}}
</div>
<div crm-mailing-radio-date="schedule" ng-model="abtest.mailings.c.scheduled_date">
<div>
<input ng-model="schedule.mode" type="radio" name="send" value="now" id="schedule-send-now"/>
<label for="schedule-send-now">{{ts('Send final mailing immediately')}}</label>
</div>
<div>
<input ng-model="schedule.mode" type="radio" name="send" value="at" id="schedule-send-at"/>
<label for="schedule-send-at">{{ts('Send final mailing at:')}}</label>
<input crm-ui-datepicker ng-model="schedule.datetime"/>
</div>
</div>
</form>
</div>

View file

@ -0,0 +1,52 @@
(function(angular, $, _) {
angular.module('crmMailingAB').controller('CrmMailingABWinnerDialogCtrl', function($scope, $timeout, dialogService, crmMailingMgr, crmStatus) {
var ts = $scope.ts = CRM.ts(null);
var abtest = $scope.abtest = $scope.model.abtest;
var mailingName = $scope.model.mailingName;
var titles = {a: ts('Mailing A'), b: ts('Mailing B')};
$scope.mailingTitle = titles[mailingName];
function init() {
// When using dialogService with a button bar, the major button actions
// need to be registered with the dialog widget (and not embedded in
// the body of the dialog).
var buttons = [
{
text: ts('Submit final mailing'),
icons: {primary: 'fa-paper-plane'},
click: function() {
crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings[mailingName], [
'name',
'recipients',
'scheduled_date'
]);
crmStatus({start: ts('Saving...'), success: ''}, abtest.save())
.then(function() {
return crmStatus({start: ts('Submitting...'), success: ts('Submitted')},
abtest.submitFinal().then(function(r) {
delete abtest.$CrmMailingABReportCnt;
return r;
}));
})
.then(function() {
dialogService.close('selectWinnerDialog', abtest);
});
}
},
{
text: ts('Cancel'),
icons: {primary: 'fa-times'},
click: function() {
dialogService.cancel('selectWinnerDialog');
}
}
];
dialogService.setButtons('selectWinnerDialog', buttons);
}
$timeout(init);
});
})(angular, CRM.$, CRM._);

View file

@ -0,0 +1,233 @@
(function (angular, $, _) {
function OptionGroup(values) {
this.get = function get(value) {
var r = _.where(values, {value: '' + value});
return r.length > 0 ? r[0] : null;
};
this.getByName = function get(name) {
var r = _.where(values, {name: '' + name});
return r.length > 0 ? r[0] : null;
};
this.getAll = function getAll() {
return values;
};
}
angular.module('crmMailingAB').factory('crmMailingABCriteria', function () {
// TODO Get data from server
var values = {
'1': {value: 'subject', name: 'subject', label: ts('Test different "Subject" lines')},
'2': {value: 'from', name: 'from', label: ts('Test different "From" lines')},
'3': {value: 'full_email', name: 'full_email', label: ts('Test entirely different emails')}
};
return new OptionGroup(values);
});
angular.module('crmMailingAB').factory('crmMailingABStatus', function () {
// TODO Get data from server
var values = {
'1': {value: '1', name: 'Draft', label: ts('Draft')},
'2': {value: '2', name: 'Testing', label: ts('Testing')},
'3': {value: '3', name: 'Final', label: ts('Final')}
};
return new OptionGroup(values);
});
// CrmMailingAB is a data-model which combines an AB test (APIv3 "MailingAB"), three mailings (APIv3 "Mailing"),
// and three sets of attachments (APIv3 "Attachment").
//
// example:
// var abtest = new CrmMailingAB(123);
// abtest.load().then(function(){
// alert("Mailing A is named "+abtest.mailings.a.name);
// });
angular.module('crmMailingAB').factory('CrmMailingAB', function (crmApi, crmMailingMgr, $q, CrmAttachments) {
function CrmMailingAB(id) {
this.id = id;
this.mailings = {};
this.attachments = {};
}
angular.extend(CrmMailingAB.prototype, {
getAutosaveSignature: function() {
return [
this.ab,
this.mailings,
this.attachments.a.getAutosaveSignature(),
this.attachments.b.getAutosaveSignature(),
this.attachments.c.getAutosaveSignature()
];
},
// @return Promise CrmMailingAB
load: function load() {
var crmMailingAB = this;
if (!crmMailingAB.id) {
crmMailingAB.ab = {
name: '',
status: 'Draft',
mailing_id_a: null,
mailing_id_b: null,
mailing_id_c: null,
domain_id: null,
testing_criteria: 'subject',
winner_criteria: null,
specific_url: '',
declare_winning_time: null,
group_percentage: 10
};
var mailingDefaults = {
// Most defaults provided by Mailing.create API, but we
// want to force-enable tracking.
open_tracking: "1",
url_tracking: "1",
mailing_type:"experiment"
};
crmMailingAB.mailings.a = crmMailingMgr.create(mailingDefaults);
crmMailingAB.mailings.b = crmMailingMgr.create(mailingDefaults);
mailingDefaults.mailing_type = 'winner';
crmMailingAB.mailings.c = crmMailingMgr.create(mailingDefaults);
crmMailingAB.attachments.a = new CrmAttachments(function () {
return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab.mailing_id_a};
});
crmMailingAB.attachments.b = new CrmAttachments(function () {
return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab.mailing_id_b};
});
crmMailingAB.attachments.c = new CrmAttachments(function () {
return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab.mailing_id_c};
});
var dfr = $q.defer();
dfr.resolve(crmMailingAB);
return dfr.promise;
}
else {
return crmApi('MailingAB', 'get', {id: crmMailingAB.id})
.then(function (abResult) {
if (abResult.count != 1) {
throw "Failed to load AB Test";
}
crmMailingAB.ab = abResult.values[abResult.id];
return crmMailingAB._loadMailings();
});
}
},
// @return Promise CrmMailingAB
save: function save() {
var crmMailingAB = this;
return crmMailingAB._saveMailings()
.then(function () {
return crmApi('MailingAB', 'create', crmMailingAB.ab)
.then(function (abResult) {
if (!crmMailingAB.id) {
crmMailingAB.id = crmMailingAB.ab.id = abResult.id;
}
});
})
.then(function () {
return crmMailingAB;
});
},
// Schedule the test
// @return Promise CrmMailingAB
// Note: Submission may cause the server state to change. Consider abtest.submit().then(...abtest.load()...)
submitTest: function submitTest() {
var crmMailingAB = this;
var params = {
id: this.ab.id,
status: 'Testing',
approval_date: 'now',
scheduled_date: this.mailings.a.scheduled_date ? this.mailings.a.scheduled_date : 'now'
};
return crmApi('MailingAB', 'submit', params)
.then(function () {
return crmMailingAB.load();
});
},
// Schedule the final mailing
// @return Promise CrmMailingAB
// Note: Submission may cause the server state to change. Consider abtest.submit().then(...abtest.load()...)
submitFinal: function submitFinal() {
var crmMailingAB = this;
var params = {
id: this.ab.id,
status: 'Final',
approval_date: 'now',
scheduled_date: this.mailings.c.scheduled_date ? this.mailings.c.scheduled_date : 'now'
};
return crmApi('MailingAB', 'submit', params)
.then(function () {
return crmMailingAB.load();
});
},
// @param mailing Object (per APIv3)
// @return Promise
'delete': function () {
if (this.id) {
return crmApi('MailingAB', 'delete', {id: this.id});
}
else {
var d = $q.defer();
d.resolve();
return d.promise;
}
},
// Load mailings A, B, and C (if available)
// @return Promise CrmMailingAB
_loadMailings: function _loadMailings() {
var crmMailingAB = this;
var todos = {};
_.each(['a', 'b', 'c'], function (mkey) {
if (crmMailingAB.ab['mailing_id_' + mkey]) {
todos[mkey] = crmMailingMgr.get(crmMailingAB.ab['mailing_id_' + mkey])
.then(function (mailing) {
crmMailingAB.mailings[mkey] = mailing;
crmMailingAB.attachments[mkey] = new CrmAttachments(function () {
return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab['mailing_id_' + mkey]};
});
return crmMailingAB.attachments[mkey].load();
});
}
else {
crmMailingAB.mailings[mkey] = crmMailingMgr.create();
crmMailingAB.attachments[mkey] = new CrmAttachments(function () {
return {entity_table: 'civicrm_mailing', entity_id: crmMailingAB.ab['mailing_id_' + mkey]};
});
}
});
return $q.all(todos).then(function () {
return crmMailingAB;
});
},
// Save mailings A, B, and C (if available)
// @return Promise CrmMailingAB
_saveMailings: function _saveMailings() {
var crmMailingAB = this;
var todos = {};
var p = $q.when(true);
_.each(['a', 'b', 'c'], function (mkey) {
if (!crmMailingAB.mailings[mkey]) {
return;
}
if (crmMailingAB.ab['mailing_id_' + mkey]) {
// paranoia: in case caller forgot to manage id on mailing
crmMailingAB.mailings[mkey].id = crmMailingAB.ab['mailing_id_' + mkey];
}
p = p.then(function(){
return crmMailingMgr.save(crmMailingAB.mailings[mkey])
.then(function () {
crmMailingAB.ab['mailing_id_' + mkey] = crmMailingAB.mailings[mkey].id;
return crmMailingAB.attachments[mkey].save();
});
});
});
return p.then(function () {
return crmMailingAB;
});
}
});
return CrmMailingAB;
});
})(angular, CRM.$, CRM._);