Skip to content

Commit

Permalink
Merge pull request #11091 from JMAConsulting/CRM-21279
Browse files Browse the repository at this point in the history
CRM-21279: Rebuild recipient list and calculate count on demand, store result in cache
  • Loading branch information
mlutfy authored Dec 4, 2017
2 parents eeda3fc + 44f6941 commit 24d67c4
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 48 deletions.
8 changes: 7 additions & 1 deletion CRM/Admin/Form/Preferences/Mailing.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,13 @@ public function preProcess() {
'html_type' => 'checkbox',
'title' => ts('Hashed Mailing URL\'s'),
'weight' => 11,
'description' => 'If enabled, a randomized hash key will be used to reference the mailing URL in the mailing.viewUrl token, instead of the mailing ID',
'description' => ts('If enabled, a randomized hash key will be used to reference the mailing URL in the mailing.viewUrl token, instead of the mailing ID'),
),
'auto_recipient_rebuild' => array(
'html_type' => 'checkbox',
'title' => ts('Enable automatic CiviMail recipient count display'),
'weight' => 12,
'description' => ts('Enable this setting to rebuild recipient list automatically during composing mail. Disable will allow you to rebuild recipient manually.'),
),
),
);
Expand Down
13 changes: 6 additions & 7 deletions ang/crmMailing/BlockRecipients.html
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<div ng-controller="EditRecipCtrl" class="crm-mailing-recipients-row">
<div style="float: right;">
<div class="crmMailing-recip-est">
<a href="" ng-click="previewRecipients()" title="{{ts('Preview a List of Recipients')}}">{{getRecipientsEstimate()}}</a>
</div>
</div>
<input
type="hidden"
crm-mailing-recipients
ng-model="mailing.recipients"
crm-mandatory-groups="crmMailingConst.groupNames | filter:{is_hidden:1}"
crm-ui-id="{{crmMailingBlockRecipients.id}}"
name="{{crmMailingBlockRecipients.name}}"
ng-required="true"/>
<a crm-icon="fa-wrench" ng-click="editOptions(mailing)" class="crm-hover-button" title="{{ts('Edit Recipient Options')}}"></a>
ng-required="true" />
<a crm-icon="fa-wrench" ng-click="editOptions(mailing)" class="crm-hover-button" title="{{ts('Edit Recipient Options')}}"></a>
<div ng-style="{display: permitRecipientRebuild ? '' : 'inline-block'}">
<button ng-click="rebuildRecipients()" ng-show="permitRecipientRebuild" class="crm-button" title="{{ts('Click to refresh recipient count')}}">{{getRecipientsEstimate()}}</button>
<a ng-click="previewRecipients()" class="crm-hover-button" title="{{ts('Preview a List of Recipients')}}" style="font-weight: bold;">{{getRecipientCount()}}</a>
</div>
</div>
90 changes: 67 additions & 23 deletions ang/crmMailing/EditRecipCtrl.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
// Scope members:
// - [input] mailing: object
// - [output] recipients: array of recipient records
angular.module('crmMailing').controller('EditRecipCtrl', function EditRecipCtrl($scope, dialogService, crmApi, crmMailingMgr, $q, crmMetadata, crmStatus) {
angular.module('crmMailing').controller('EditRecipCtrl', function EditRecipCtrl($scope, dialogService, crmApi, crmMailingMgr, $q, crmMetadata, crmStatus, crmMailingCache) {
// Time to wait before triggering AJAX update to recipients list
var RECIPIENTS_DEBOUNCE_MS = 100;
var SETTING_DEBOUNCE_MS = 5000;
var RECIPIENTS_PREVIEW_LIMIT = 50;

var ts = $scope.ts = CRM.ts(null);
Expand All @@ -18,29 +19,45 @@
};

$scope.recipients = null;
$scope.outdated = null;
$scope.permitRecipientRebuild = null;

$scope.getRecipientsEstimate = function() {
var ts = $scope.ts;
if ($scope.recipients === null) {
return ts('(Estimating)');
return ts('Estimating...');
}
if ($scope.recipients === 0) {
return ts('No recipients');
return ts('Estimate recipient count');
}
return ts('Refresh recipient count');
};

$scope.getRecipientCount = function() {
var ts = $scope.ts;
if ($scope.recipients === 0) {
return ts('No Recipients');
}
else if ($scope.recipients > 0) {
return ts('~%1 recipients', {1 : $scope.recipients});
}
if ($scope.recipients === 1) {
return ts('~1 recipient');
else if ($scope.outdated) {
return ts('(unknown)');
}
else {
return $scope.permitRecipientRebuild ? ts('(unknown)') : ts('Estimating...');
}
return ts('~%1 recipients', {1: $scope.recipients});
};

// We monitor four fields -- use debounce so that changes across the
// four fields can settle-down before AJAX.
var refreshRecipients = _.debounce(function() {
$scope.$apply(function() {
$scope.recipients = null;
if (!$scope.mailing) {
return;
}
crmMailingMgr.previewRecipientCount($scope.mailing).then(function(recipients) {
crmMailingMgr.previewRecipientCount($scope.mailing, crmMailingCache, !$scope.permitRecipientRebuild).then(function(recipients) {
$scope.outdated = ($scope.permitRecipientRebuild && _.difference($scope.mailing.recipients, crmMailingCache.get('mailing-' + $scope.mailing.id + '-recipient-params')) !== 0);
$scope.recipients = recipients;
});
});
Expand All @@ -53,22 +70,49 @@
$scope.$watchCollection("mailing.recipients.mailings.include", refreshRecipients);
$scope.$watchCollection("mailing.recipients.mailings.exclude", refreshRecipients);

$scope.previewRecipients = function previewRecipients() {
return crmStatus({start: ts('Previewing...'), success: ''}, crmMailingMgr.previewRecipients($scope.mailing, RECIPIENTS_PREVIEW_LIMIT).then(function(recipients) {
var model = {
count: $scope.recipients,
sample: recipients,
sampleLimit: RECIPIENTS_PREVIEW_LIMIT
};
var options = CRM.utils.adjustDialogDefaults({
width: '40%',
autoOpen: false,
title: ts('Preview (%1)', {
1: $scope.getRecipientsEstimate()
})
// refresh setting at a duration on 5sec
var refreshSetting = _.debounce(function() {
$scope.$apply(function() {
crmApi('Setting', 'getvalue', {"name": 'auto_recipient_rebuild', "return": "value"}).then(function(response) {
$scope.permitRecipientRebuild = (response.result === 0);
});
dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
}));
});
}, SETTING_DEBOUNCE_MS);
$scope.$watchCollection("permitRecipientRebuild", refreshSetting);

$scope.previewRecipients = function previewRecipients() {
var model = {
count: $scope.recipients,
sample: crmMailingCache.get('mailing-' + $scope.mailing.id + '-recipient-list'),
sampleLimit: RECIPIENTS_PREVIEW_LIMIT
};
var options = CRM.utils.adjustDialogDefaults({
width: '40%',
autoOpen: false,
title: ts('Preview (%1)', {1: $scope.getRecipientCount()})
});

// don't open preview dialog if there is no recipient to show.
if ($scope.recipients !== 0 && !$scope.outdated) {
if (!_.isEmpty(model.sample)) {
dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
}
else {
return crmStatus({start: ts('Previewing...'), success: ''}, crmMailingMgr.previewRecipients($scope.mailing, RECIPIENTS_PREVIEW_LIMIT).then(function(recipients) {
model.sample = recipients;
dialogService.open('recipDialog', '~/crmMailing/PreviewRecipCtrl.html', model, options);
}));
}
}
};

$scope.rebuildRecipients = function rebuildRecipients() {
// setting null will put 'Estimating..' text on refresh button
$scope.recipients = null;
return crmMailingMgr.previewRecipientCount($scope.mailing, crmMailingCache, true).then(function(recipients) {
$scope.outdated = (recipients === 0) ? true : false;
$scope.recipients = recipients;
});
};

// Open a dialog for editing the advanced recipient options.
Expand Down
60 changes: 43 additions & 17 deletions ang/crmMailing/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,24 +310,46 @@
});
},

previewRecipientCount: function previewRecipientCount(mailing) {
// To get list of recipients, we tentatively save the mailing and
// get the resulting recipients -- then rollback any changes.
var params = angular.extend({}, mailing, mailing.recipients, {
name: 'placeholder', // for previewing recipients on new, incomplete mailing
subject: 'placeholder', // for previewing recipients on new, incomplete mailing
options: {force_rollback: 1},
'api.mailing_job.create': 1, // note: exact match to API default
'api.MailingRecipients.getcount': {
mailing_id: '$value.id'
previewRecipientCount: function previewRecipientCount(mailing, crmMailingCache, rebuild) {
var cachekey = 'mailing-' + mailing.id + '-recipient-count';
var recipientCount = crmMailingCache.get(cachekey);
if (rebuild || _.isEmpty(recipientCount)) {
// To get list of recipients, we tentatively save the mailing and
// get the resulting recipients -- then rollback any changes.
var params = angular.extend({}, mailing, mailing.recipients, {
name: 'placeholder', // for previewing recipients on new, incomplete mailing
subject: 'placeholder', // for previewing recipients on new, incomplete mailing
options: {force_rollback: 1},
'api.mailing_job.create': 1, // note: exact match to API default
'api.MailingRecipients.getcount': {
mailing_id: '$value.id'
}
});
// if this service is executed on rebuild then also fetch the recipients list
if (rebuild) {
params = angular.extend(params, {
'api.MailingRecipients.get': {
mailing_id: '$value.id',
options: {limit: 50},
'api.contact.getvalue': {'return': 'display_name'},
'api.email.getvalue': {'return': 'email'}
}
});
crmMailingCache.put('mailing-' + mailing.id + '-recipient-params', params.recipients);
}
});
delete params.recipients; // the content was merged in
return qApi('Mailing', 'create', params).then(function (recipResult) {
// changes rolled back, so we don't care about updating mailing
mailing.modified_date = recipResult.values[recipResult.id].modified_date;
return recipResult.values[recipResult.id]['api.MailingRecipients.getcount'];
});
delete params.recipients; // the content was merged in
recipientCount = qApi('Mailing', 'create', params).then(function (recipResult) {
// changes rolled back, so we don't care about updating mailing
mailing.modified_date = recipResult.values[recipResult.id].modified_date;
if (rebuild) {
crmMailingCache.put('mailing-' + mailing.id + '-recipient-list', recipResult.values[recipResult.id]['api.MailingRecipients.get'].values);
}
return recipResult.values[recipResult.id]['api.MailingRecipients.getcount'];
});
crmMailingCache.put(cachekey, recipientCount);
}

return recipientCount;
},

// Save a (draft) mailing
Expand Down Expand Up @@ -555,4 +577,8 @@
};
});

angular.module('crmMailing').factory('crmMailingCache', ['$cacheFactory', function($cacheFactory) {
return $cacheFactory('crmMailingCache');
}]);

})(angular, CRM.$, CRM._);
13 changes: 13 additions & 0 deletions settings/Mailing.setting.php
Original file line number Diff line number Diff line change
Expand Up @@ -333,4 +333,17 @@
'description' => 'The number of emails sendable via simple mail. Make sure you understand the implications for your spam reputation and legal requirements for bulk emails before editing. As there is some risk both to your spam reputation and the products if this is misused it is a hidden setting',
'help_text' => 'CiviCRM forces users sending more than this number of mails to use CiviMails. CiviMails have additional precautions: not sending to contacts who do not want bulk mail, adding domain name and opt out links. You should familiarise yourself with the law relevant to you on bulk mailings if changing this setting. For the US https://en.wikipedia.org/wiki/CAN-SPAM_Act_of_2003 is a good place to start.',
),
'auto_recipient_rebuild' => array(
'group_name' => 'Mailing Preferences',
'group' => 'mailing',
'name' => 'auto_recipient_rebuild',
'type' => 'Boolean',
'quick_form_type' => 'YesNo',
'default' => '1',
'title' => 'Enable automatic CiviMail recipient count display',
'is_domain' => 1,
'is_contact' => 0,
'description' => 'Enable this setting to rebuild recipient list automatically during composing mail. Disable will allow you to rebuild recipient manually.',
'help_text' => 'CiviMail automatically fetches recipient list and count whenever mailing groups are included or excluded while composing bulk mail. This phenomena may degrade performance for large sites, so disable this setting to build and fetch recipients for selected groups, manually.',
),
);

0 comments on commit 24d67c4

Please sign in to comment.