CRM-15578 - crmMailing(AB) - Submission & status tracking
authorTim Otten <totten@civicrm.org>
Fri, 19 Dec 2014 02:42:22 +0000 (18:42 -0800)
committerTim Otten <totten@civicrm.org>
Fri, 19 Dec 2014 08:10:07 +0000 (00:10 -0800)
When loading or submitting, determine the status of any jobs tied to this mailing.

When attempting to edit a submitted mailing, display a message instead of a form.

Change the technique for updating URL -- instead of watching 'mailing.id',
explicitly call updateUrl after saving.  This ensures a consistent, linear
sequence of operations.  (Note: The watch technique was useful when there
were different variations of save() in EditMailingCtrl and
PreviewMailingCtrl, but we eliminated PreviewMailingCtrl, so it's much
easier to manage the dataflows in a centralized fashion.)

js/angular-crmMailing.js
js/angular-crmMailing/services.js
js/angular-crmMailingAB.js
js/angular-crmMailingAB/services.js
partials/crmMailing/edit-unified.html
partials/crmMailing/edit-unified2.html
partials/crmMailing/edit-wizard.html
partials/crmMailing/edit.html
partials/crmMailingAB/edit.html

index 0f5b3bff035316b01407bc24e4826e2a4b7488f1..1124a8a15398aa4954758fa8da7610cd7eb856df 100644 (file)
     $scope.partialUrl = partialUrl;
     var ts = $scope.ts = CRM.ts('CiviMail');
 
+    $scope.isSubmitted = function isSubmitted() {
+      return _.size($scope.mailing.jobs) > 0;
+    };
+
     // @return Promise
     $scope.previewMailing = function previewMailing(mailing, mode) {
       return crmMailingPreviewMgr.preview(mailing, mode);
@@ -87,7 +91,8 @@
       var savePromise = crmMailingMgr.save(mailing)
         .then(function () {
           return attachments.save();
-        });
+        })
+        .then(updateUrl);
       return crmStatus({start: ts('Saving...'), success: ''}, savePromise)
         .then(function () {
           crmMailingPreviewMgr.sendTest(mailing, recipient);
     // @return Promise
     $scope.submit = function submit() {
       var promise = crmMailingMgr.save($scope.mailing)
-        .then(function () {
-          // pre-condition: the mailing exists *before* saving attachments to it
-          return $scope.attachments.save();
-        })
-        .then(function () {
-          return crmMailingMgr.submit($scope.mailing);
-        });
+          .then(function () {
+            // pre-condition: the mailing exists *before* saving attachments to it
+            return $scope.attachments.save();
+          })
+          .then(function () {
+            return crmMailingMgr.submit($scope.mailing);
+          })
+          .then(function () {
+            updateUrl();
+            return $scope.mailing;
+          })
+        ;
       return crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, promise);
     };
 
             // pre-condition: the mailing exists *before* saving attachments to it
             return $scope.attachments.save();
           })
+          .then(function () {
+            updateUrl();
+            return $scope.mailing;
+          })
       );
     };
 
       });
     };
 
-    // Transition URL "/mailing/new" => "/mailing/123" as soon as ID is known
-    $scope.$watch('mailing.id', function (newValue, oldValue) {
-      if (newValue && newValue != oldValue) {
-        var parts = $location.path().split('/'); // e.g. "/mailing/new" or "/mailing/123/wizard"
-        parts[2] = newValue;
+    // Transition URL "/mailing/new" => "/mailing/123"
+    function updateUrl() {
+      var parts = $location.path().split('/'); // e.g. "/mailing/new" or "/mailing/123/wizard"
+      if (parts[2] != $scope.mailing.id) {
+        parts[2] = $scope.mailing.id;
         $location.path(parts.join('/'));
         $location.replace();
         // FIXME: Angular unnecessarily refreshes UI
+        // WARNING: Changing the URL triggers a full reload. Any pending AJAX operations
+        // could be inconsistently applied. Run updateUrl() after other changes complete.
       }
-    });
+    }
   });
 
   // Controller for the edit-recipients fields (
index 3202ee054dbf70967682118f110f48e6794cafb0..483ddee045ee323ca99e54d6439d9fe51dac2189 100644 (file)
@@ -29,7 +29,7 @@
     var emailRegex = /^"(.*)" \<([^@\>]*@[^@\>]*)\>$/;
     var addrs = _.map(CRM.crmMailing.fromAddress, function (addr) {
       var match = emailRegex.exec(addr.label);
-      return _.extend({}, addr, {
+      return angular.extend({}, addr, {
         email: match ? match[2] : '(INVALID)',
         author: match ? match[1] : '(INVALID)'
       });
@@ -74,7 +74,7 @@
 
   angular.module('crmMailing').factory('crmMsgTemplates', function ($q, crmApi) {
     var tpls = _.map(CRM.crmMailing.mesTemplate, function (tpl) {
-      return _.extend({}, tpl, {
+      return angular.extend({}, tpl, {
         //id: tpl parseInt(tpl.id)
       });
     });
       },
       // @return Promise Mailing (per APIv3)
       get: function get(id) {
-        return crmApi('Mailing', 'getsingle', {id: id}).then(function (mailing) {
-          return crmApi('MailingGroup', 'get', {mailing_id: id}).then(function (groupResult) {
+        var crmMailingMgr = this;
+        var mailing;
+        return crmApi('Mailing', 'getsingle', {id: id})
+          .then(function (getResult) {
+            mailing = getResult;
+            return $q.all([
+              crmMailingMgr._loadGroups(mailing),
+              crmMailingMgr._loadJobs(mailing)
+            ]);
+          })
+          .then(function () {
+            return mailing;
+          });
+      },
+      // Call MailingGroup.get and merge results into "mailing"
+      _loadGroups: function (mailing) {
+        return crmApi('MailingGroup', 'get', {mailing_id: mailing.id})
+          .then(function (groupResult) {
             mailing.groups = {include: [], exclude: []};
             mailing.mailings = {include: [], exclude: []};
             _.each(groupResult.values, function (mailingGroup) {
               var entityId = parseInt(mailingGroup.entity_id);
               mailing[bucket][mailingGroup.group_type].push(entityId);
             });
-            return mailing;
           });
-        });
+      },
+      // Call MailingJob.get and merge results into "mailing"
+      _loadJobs: function (mailing) {
+        return crmApi('MailingJob', 'get', {mailing_id: mailing.id, is_test: 0})
+          .then(function (jobResult) {
+            mailing.jobs = mailing.jobs || {};
+            angular.extend(mailing.jobs, jobResult.values);
+          });
       },
       // @return Object Mailing (per APIv3)
       create: function create() {
         return {
+          jobs: {}, // {jobId: JobRecord}
           name: "revert this", // fixme
           campaign_id: null,
           from_name: crmFromAddresses.getDefault().author,
       // @param mailing Object (per APIv3)
       // @return Promise an object with "subject", "body_text", "body_html"
       preview: function preview(mailing) {
-        var params = _.extend({}, mailing, {
+        var params = angular.extend({}, mailing, {
           options: {force_rollback: 1},
           'api.Mailing.preview': {
             id: '$value.id'
       previewRecipients: function previewRecipients(mailing, previewLimit) {
         // To get list of recipients, we tentatively save the mailing and
         // get the resulting recipients -- then rollback any changes.
-        var params = _.extend({}, mailing, {
+        var params = angular.extend({}, mailing, {
           options: {force_rollback: 1},
           'api.mailing_job.create': 1, // note: exact match to API default
           'api.MailingRecipients.get': {
       // @param mailing Object (per APIv3)
       // @return Promise
       save: function (mailing) {
-        var params = _.extend({}, mailing, {
+        var params = angular.extend({}, mailing, {
           'api.mailing_job.create': 0 // note: exact match to API default
         });
 
         // is therefore not allowed. Remove this after fixing Mailing.create's contract.
         delete params.scheduled_date;
 
+        delete params.jobs;
+
         return crmApi('Mailing', 'create', params).then(function (result) {
           if (result.id && !mailing.id) {
             mailing.id = result.id;
           }  // no rollback, so update mailing.id
           // Perhaps we should reload mailing based on result?
-          return result.values[result.id];
+          return mailing;
         });
       },
 
       // @param mailing Object (per APIv3)
       // @return Promise
       submit: function (mailing) {
+        var crmMailingMgr = this;
         var params = {
           id: mailing.id,
           approval_date: createNow(),
           scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : createNow()
         };
-        return crmApi('Mailing', 'submit', params).then(function (result) {
-          _.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
-          return mailing;
-        });
+        return crmApi('Mailing', 'submit', params)
+          .then(function (result) {
+            angular.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
+            return crmMailingMgr._loadJobs(mailing);
+          })
+          .then(function () {
+            return mailing;
+          });
       },
 
       // Immediately send a test message
       // @param to Object with either key "email" (string) or "gid" (int)
       // @return Promise for a list of delivery reports
       sendTest: function (mailing, recipient) {
-        var params = _.extend({}, mailing, {
+        var params = angular.extend({}, mailing, {
           // options:  {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
           'api.Mailing.send_test': {
             mailing_id: '$value.id',
         // is therefore not allowed. Remove this after fixing Mailing.create's contract.
         delete params.scheduled_date;
 
+        delete params.jobs;
+
         return crmApi('Mailing', 'create', params).then(function (result) {
           if (result.id && !mailing.id) {
             mailing.id = result.id;
index 47273b8d2b946f3d572de7af74bafaf4f809a58f..9ca3f2d6031f50943b72bdb270d2f8136045990d 100644 (file)
     $scope.testing_criteria = crmMailingABCriteria.getAll();
   });
 
-  angular.module('crmMailingAB').controller('CrmMailingABEditCtrl', function ($scope, abtest, crmMailingABCriteria, crmMailingMgr, crmMailingPreviewMgr, crmStatus) {
-    window.ab = abtest;
+  angular.module('crmMailingAB').controller('CrmMailingABEditCtrl', function ($scope, abtest, crmMailingABCriteria, crmMailingMgr, crmMailingPreviewMgr, crmStatus, $q, $location) {
     $scope.abtest = abtest;
     var ts = $scope.ts = CRM.ts('CiviMail');
     $scope.crmMailingABCriteria = crmMailingABCriteria;
     $scope.crmMailingConst = CRM.crmMailing;
     $scope.partialUrl = partialUrl;
 
+    $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});
       }
       crmMailingMgr.mergeInto(abtest.mailings.c, abtest.mailings.a, ['name']);
     };
+
+    // @return Promise
     $scope.save = function save() {
       $scope.sync();
-      return crmStatus({start: ts('Saving...'), success: ts('Saved')}, abtest.save());
+      return crmStatus({start: ts('Saving...'), success: ts('Saved')}, abtest.save().then(updateUrl));
     };
+
     // @return Promise
     $scope.previewMailing = function previewMailing(mailingName, mode) {
       return crmMailingPreviewMgr.preview(abtest.mailings[mailingName], mode);
 
     // @return Promise
     $scope.sendTest = function sendTest(mailingName, recipient) {
-      return crmStatus({start: ts('Saving...'), success: ''}, abtest.save())
+      return crmStatus({start: ts('Saving...'), success: ''}, abtest.save().then(updateUrl))
         .then(function () {
           crmMailingPreviewMgr.sendTest(abtest.mailings[mailingName], recipient);
         });
     };
+
+    // @return Promise
     $scope.delete = function () {
-      throw "Not implemented: EditCtrl.delete"
+      return crmStatus({start: ts('Deleting...'), success: ts('Deleted')}, abtest.delete());
     };
-    $scope.submit = function () {
-      throw "Not implemented: EditCtrl.submit"
+
+    // @return Promise
+    $scope.submit = function submit() {
+      return crmStatus({start: ts('Saving...'), success: ''}, abtest.save().then(updateUrl))
+        .then(function () {
+          return crmStatus({start: ts('Submitting...'), success: ts('Submitted')}, $q.all([
+            crmMailingMgr.submit(abtest.mailings.a),
+            crmMailingMgr.submit(abtest.mailings.b)
+          ]));
+        });
     };
 
     function updateCriteriaName() {
       $scope.criteriaName = criteria ? criteria.name : null;
     }
 
+    // Transition URL "/abtest/new" => "/abtest/123"
+    function updateUrl() {
+      var parts = $location.path().split('/'); // e.g. "/abtest/new" or "/abtest/123/wizard"
+      if (parts[2] != $scope.abtest.ab.id) {
+        parts[2] = $scope.abtest.ab.id;
+        $location.path(parts.join('/'));
+        $location.replace();
+        // FIXME: Angular unnecessarily refreshes UI
+        // WARNING: Changing the URL triggers a full reload. Any pending AJAX operations
+        // could be inconsistently applied. Run updateUrl() after other changes complete.
+      }
+    }
+
     // initialize
     updateCriteriaName();
     $scope.sync();
index 7fe65355aa49b137c1b20b0384196c3abf74f6f5..becabe19fae5adccdf92f04fb052e3340897dfdb 100644 (file)
@@ -70,6 +70,9 @@
         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 crmMailingAB;
           });
       },
+      // @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() {
index 005c57178d1a78c7ec15ae1639c16b568871d2db..5aaad4c4b5c581fdfff78e8a8e0669f1e0690f36 100644 (file)
@@ -2,7 +2,11 @@
   <pre>{{mailing|json}}</pre>
 </div>
 
-<form name="crmMailing">
+<div ng-show="isSubmitted()">
+  {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" ng-hide="isSubmitted()">
   <div class="crm-block crm-form-block crmMailing">
 
     <div crm-mailing-block-summary crm-mailing="mailing"/>
index 9140e522cdf1b3a6d57e42132ac76214a7e20221..00725dc93f0285cf7745031117a76403494e757b 100644 (file)
@@ -2,7 +2,11 @@
   <pre>{{mailing|json}}</pre>
 </div>
 
-<form name="crmMailing">
+<div ng-show="isSubmitted()">
+  {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" ng-hide="isSubmitted()">
   <div class="crm-block crm-form-block crmMailing">
 
     <div crm-mailing-block-summary crm-mailing="mailing"/>
@@ -36,7 +40,7 @@
       <div crm-mailing-block-schedule crm-mailing="mailing"/>
     </div>
 
-    <button ng-click="submit().then(leave)">{{ts('Submit Mailing')}}</button>
+    <button ng-click="submit()">{{ts('Submit Mailing')}}</button>
     <button ng-click="save()">{{ts('Save Draft')}}</button>
     <button crm-confirm="{title:ts('Delete Draft?'), message:ts('Are you sure you want to delete the draft mailing?')}" on-yes="delete().then(leave)">{{ts('Delete Draft')}}</button>
   </div>
index 1548e5b3c0124f6ee5a70f74b8ff6f3665dfbece..31b9104b4f719f115452e6d156a6a111311c5bed 100644 (file)
@@ -2,7 +2,11 @@
   <pre>{{mailing|json}}</pre>
 </div>
 
-<form name="crmMailing">
+<div ng-show="isSubmitted()">
+  {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" ng-hide="isSubmitted()">
   <div class="crm-block crm-form-block crmMailing">
 
     <div crm-ui-wizard>
index f6ed9c29f11d5d3dff8915b190dd9f17881e0e01..0b914e760e449c768161cde54f44e35043627f40 100644 (file)
@@ -2,7 +2,11 @@
   <pre>{{mailing|json}}</pre>
 </div>
 
-<form name="crmMailing" novalidate>
+<div ng-show="isSubmitted()">
+  {{ts('This mailing has been submitted.')}}
+</div>
+
+<form name="crmMailing" novalidate ng-hide="isSubmitted()">
   <div class="crm-block crm-form-block crmMailing">
     <div crm-ui-wizard>
       <div crm-ui-wizard-step crm-title="ts('Define Mailing')">
@@ -47,7 +51,7 @@
           <div crm-mailing-block-schedule crm-mailing="mailing"/>
         </div>
         <center>
-          <a class="crmMailing-submit-button" ng-click="submit().then(leave)">
+          <a class="crmMailing-submit-button" ng-click="submit()">
             <div>{{ts('Submit Mailing')}}</div>
           </a>
         </center>
index ae935fde70eb54d50115bf9597b062423e6c405e..9211e23435626fb697a1fa051525594270044846 100644 (file)
@@ -9,8 +9,11 @@
   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-show="isSubmitted()">
+  {{ts('This mailing has been submitted.')}}
+</div>
 
-<form name="crmMailingAB" novalidate>
+<form name="crmMailingAB" novalidate ng-hide="isSubmitted()">
   <div class="crm-block crm-form-block crmMailing">
     <div crm-ui-wizard>
       <div crm-ui-wizard-step="10" crm-title="ts('Setup')">
       <span crm-ui-wizard-buttons style="float:right;">
         <button
           crm-confirm="{title:ts('Delete Draft?'), message:ts('Are you sure you want to delete the draft mailing?')}"
-          on-yes="delete()">{{ts('Delete Draft')}}
+          on-yes="delete().then(leave)">{{ts('Delete Draft')}}
         </button>
         <button ng-click="save()">{{ts('Save Draft')}}</button>
       </span>