diff options
Diffstat (limited to 'www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB')
18 files changed, 1661 insertions, 0 deletions
diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.html new file mode 100644 index 00000000..92448156 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.html @@ -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> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.js new file mode 100644 index 00000000..d738ab6e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockMailing.js @@ -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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.html new file mode 100644 index 00000000..7d124cc0 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.html @@ -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> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.js new file mode 100644 index 00000000..5809cdd0 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/BlockSetup.js @@ -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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl.js new file mode 100644 index 00000000..b189bafc --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl.js @@ -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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/edit.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/edit.html new file mode 100644 index 00000000..825f3a57 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/edit.html @@ -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> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/main.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/main.html new file mode 100644 index 00000000..15822f6e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/main.html @@ -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> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/report.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/report.html new file mode 100644 index 00000000..68b79245 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/EditCtrl/report.html @@ -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> {{stats[am.name][rateStats[statType.name]] || ' '}} + <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}}" <{{am.mailing.from_email}}> + </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> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.html new file mode 100644 index 00000000..5d2c0768 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.html @@ -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> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.js new file mode 100644 index 00000000..d0ced773 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ListCtrl.js @@ -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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/NewCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/NewCtrl.js new file mode 100644 index 00000000..edcfa705 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/NewCtrl.js @@ -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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ReportCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ReportCtrl.js new file mode 100644 index 00000000..8755945e --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/ReportCtrl.js @@ -0,0 +1,56 @@ +(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.rateStats = { + 'Unique Clicks': 'clickthrough_rate', + 'Delivered': 'delivered_rate', + 'Opened': 'opened_rate', + }; + $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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.html new file mode 100644 index 00000000..cdfcad3b --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.html @@ -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> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.js new file mode 100644 index 00000000..d26e35b1 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Slider.js @@ -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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Stats.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Stats.js new file mode 100644 index 00000000..da2ebb63 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/Stats.js @@ -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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.html b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.html new file mode 100644 index 00000000..0c2db4f6 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.html @@ -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> diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.js new file mode 100644 index 00000000..f378f641 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/WinnerDialogCtrl.js @@ -0,0 +1,43 @@ +(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() { + crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, + abtest.submitFinal(abtest.mailings[mailingName].id).then(function (r) { + delete abtest.$CrmMailingABReportCnt; + })) + .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._); diff --git a/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/services.js b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/services.js new file mode 100644 index 00000000..2e9fa926 --- /dev/null +++ b/www/crm/wp-content/plugins/civicrm/civicrm/ang/crmMailingAB/services.js @@ -0,0 +1,234 @@ +(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(winner_id) { + var crmMailingAB = this; + var params = { + id: this.ab.id, + status: 'Final', + winner_id: winner_id, + 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._); |