Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CRM-21279: Rebuild recipient list and calculate count on demand, store result in cache #11091

Merged
merged 3 commits into from
Dec 4, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.'),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mlutfy I have added ts(). Thanks for notifying :) Is there anything else that needs to be done to get this PR merged?

),
),
);
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.',
),
);