Merge pull request #4878 from pratikshad/broken-webtest
[civicrm-core.git] / js / angular-crmMailing / services.js
1 (function (angular, $, _) {
2 var partialUrl = function (relPath) {
3 return CRM.resourceUrls.civicrm + '/partials/crmMailing/' + relPath;
4 };
5
6 // The representation of from/reply-to addresses is inconsistent in the mailing data-model,
7 // so the UI must do some adaptation. The crmFromAddresses provides a richer way to slice/dice
8 // the available "From:" addrs. Records are like the underlying OptionValues -- but add "email"
9 // and "author".
10 angular.module('crmMailing').factory('crmFromAddresses', function ($q, crmApi) {
11 var emailRegex = /^"(.*)" <([^@>]*@[^@>]*)>$/;
12 var addrs = _.map(CRM.crmMailing.fromAddress, function (addr) {
13 var match = emailRegex.exec(addr.label);
14 return angular.extend({}, addr, {
15 email: match ? match[2] : '(INVALID)',
16 author: match ? match[1] : '(INVALID)'
17 });
18 });
19
20 function first(array) {
21 return (array.length === 0) ? null : array[0];
22 }
23
24 return {
25 getAll: function getAll() {
26 return addrs;
27 },
28 getByAuthorEmail: function getByAuthorEmail(author, email, autocreate) {
29 var result = null;
30 _.each(addrs, function (addr) {
31 if (addr.author == author && addr.email == email) {
32 result = addr;
33 }
34 });
35 if (!result && autocreate) {
36 result = {
37 label: '(INVALID) "' + author + '" <' + email + '>',
38 author: author,
39 email: email
40 };
41 addrs.push(result);
42 }
43 return result;
44 },
45 getByEmail: function getByEmail(email) {
46 return first(_.where(addrs, {email: email}));
47 },
48 getByLabel: function (label) {
49 return first(_.where(addrs, {label: label}));
50 },
51 getDefault: function getDefault() {
52 return first(_.where(addrs, {is_default: "1"}));
53 }
54 };
55 });
56
57 angular.module('crmMailing').factory('crmMsgTemplates', function ($q, crmApi) {
58 var tpls = _.map(CRM.crmMailing.mesTemplate, function (tpl) {
59 return angular.extend({}, tpl, {
60 //id: tpl parseInt(tpl.id)
61 });
62 });
63 window.tpls = tpls;
64 var lastModifiedTpl = null;
65 return {
66 // @return Promise MessageTemplate (per APIv3)
67 get: function get(id) {
68 id = '' + id; // parseInt(id);
69 var dfr = $q.defer();
70 var tpl = _.where(tpls, {id: id});
71 if (id && tpl && tpl[0]) {
72 dfr.resolve(tpl[0]);
73 }
74 else {
75 dfr.reject(id);
76 }
77 return dfr.promise;
78 },
79 // Save a template
80 // @param tpl MessageTemplate (per APIv3) For new templates, omit "id"
81 // @return Promise MessageTemplate (per APIv3)
82 save: function (tpl) {
83 return crmApi('MessageTemplate', 'create', tpl).then(function (response) {
84 if (!tpl.id) {
85 tpl.id = '' + response.id; //parseInt(response.id);
86 tpls.push(tpl);
87 }
88 lastModifiedTpl = tpl;
89 return tpl;
90 });
91 },
92 // @return Object MessageTemplate (per APIv3)
93 getLastModifiedTpl: function () {
94 return lastModifiedTpl;
95 },
96 getAll: function getAll() {
97 return tpls;
98 }
99 };
100 });
101
102 // The crmMailingMgr service provides business logic for loading, saving, previewing, etc
103 angular.module('crmMailing').factory('crmMailingMgr', function ($q, crmApi, crmFromAddresses, crmNow) {
104 var pickDefaultMailComponent = function pickDefaultMailComponent(type) {
105 var mcs = _.where(CRM.crmMailing.headerfooterList, {
106 component_type: type,
107 is_default: "1"
108 });
109 return (mcs.length >= 1) ? mcs[0].id : null;
110 };
111
112 return {
113 // @param scalar idExpr a number or the literal string 'new'
114 // @return Promise|Object Mailing (per APIv3)
115 getOrCreate: function getOrCreate(idExpr) {
116 return (idExpr == 'new') ? this.create() : this.get(idExpr);
117 },
118 // @return Promise Mailing (per APIv3)
119 get: function get(id) {
120 var crmMailingMgr = this;
121 var mailing;
122 return crmApi('Mailing', 'getsingle', {id: id})
123 .then(function (getResult) {
124 mailing = getResult;
125 return $q.all([
126 crmMailingMgr._loadGroups(mailing),
127 crmMailingMgr._loadJobs(mailing)
128 ]);
129 })
130 .then(function () {
131 return mailing;
132 });
133 },
134 // Call MailingGroup.get and merge results into "mailing"
135 _loadGroups: function (mailing) {
136 return crmApi('MailingGroup', 'get', {mailing_id: mailing.id})
137 .then(function (groupResult) {
138 mailing.groups = {include: [], exclude: []};
139 mailing.mailings = {include: [], exclude: []};
140 _.each(groupResult.values, function (mailingGroup) {
141 var bucket = (mailingGroup.entity_table == 'civicrm_group') ? 'groups' : 'mailings';
142 var entityId = parseInt(mailingGroup.entity_id);
143 mailing[bucket][mailingGroup.group_type].push(entityId);
144 });
145 });
146 },
147 // Call MailingJob.get and merge results into "mailing"
148 _loadJobs: function (mailing) {
149 return crmApi('MailingJob', 'get', {mailing_id: mailing.id, is_test: 0})
150 .then(function (jobResult) {
151 mailing.jobs = mailing.jobs || {};
152 angular.extend(mailing.jobs, jobResult.values);
153 });
154 },
155 // @return Object Mailing (per APIv3)
156 create: function create() {
157 return {
158 jobs: {}, // {jobId: JobRecord}
159 name: "",
160 campaign_id: null,
161 from_name: crmFromAddresses.getDefault().author,
162 from_email: crmFromAddresses.getDefault().email,
163 replyto_email: "",
164 subject: "",
165 groups: {include: [], exclude: []},
166 mailings: {include: [], exclude: []},
167 body_html: "",
168 body_text: "",
169 footer_id: null, // pickDefaultMailComponent('Footer'),
170 header_id: null, // pickDefaultMailComponent('Header'),
171 visibility: "Public Pages",
172 url_tracking: "1",
173 dedupe_email: "1",
174 forward_replies: "0",
175 auto_responder: "0",
176 open_tracking: "1",
177 override_verp: "1",
178 optout_id: pickDefaultMailComponent('OptOut'),
179 reply_id: pickDefaultMailComponent('Reply'),
180 resubscribe_id: pickDefaultMailComponent('Resubscribe'),
181 unsubscribe_id: pickDefaultMailComponent('Unsubscribe')
182 };
183 },
184
185 // @param mailing Object (per APIv3)
186 // @return Promise
187 'delete': function (mailing) {
188 if (mailing.id) {
189 return crmApi('Mailing', 'delete', {id: mailing.id});
190 }
191 else {
192 var d = $q.defer();
193 d.resolve();
194 return d.promise;
195 }
196 },
197
198 // Copy all data fields in (mailingFrom) to (mailingTgt) -- except for (excludes)
199 // ex: crmMailingMgr.mergeInto(newMailing, mailingTemplate, ['subject']);
200 mergeInto: function mergeInto(mailingTgt, mailingFrom, excludes) {
201 var MAILING_FIELDS = [
202 // always exclude: 'id'
203 'name',
204 'campaign_id',
205 'from_name',
206 'from_email',
207 'replyto_email',
208 'subject',
209 'dedupe_email',
210 'groups',
211 'mailings',
212 'body_html',
213 'body_text',
214 'footer_id',
215 'header_id',
216 'visibility',
217 'url_tracking',
218 'dedupe_email',
219 'forward_replies',
220 'auto_responder',
221 'open_tracking',
222 'override_verp',
223 'optout_id',
224 'reply_id',
225 'resubscribe_id',
226 'unsubscribe_id'
227 ];
228 if (!excludes) {
229 excludes = [];
230 }
231 _.each(MAILING_FIELDS, function (field) {
232 if (!_.contains(excludes, field)) {
233 mailingTgt[field] = mailingFrom[field];
234 }
235 });
236 },
237
238 // @param mailing Object (per APIv3)
239 // @return Promise an object with "subject", "body_text", "body_html"
240 preview: function preview(mailing) {
241 var params = angular.extend({}, mailing, {
242 options: {force_rollback: 1},
243 'api.Mailing.preview': {
244 id: '$value.id'
245 }
246 });
247 return crmApi('Mailing', 'create', params).then(function (result) {
248 // changes rolled back, so we don't care about updating mailing
249 return result.values[result.id]['api.Mailing.preview'].values;
250 });
251 },
252
253 // @param mailing Object (per APIv3)
254 // @param int previewLimit
255 // @return Promise for a list of recipients (mailing_id, contact_id, api.contact.getvalue, api.email.getvalue)
256 previewRecipients: function previewRecipients(mailing, previewLimit) {
257 // To get list of recipients, we tentatively save the mailing and
258 // get the resulting recipients -- then rollback any changes.
259 var params = angular.extend({}, mailing, {
260 name: 'placeholder', // for previewing recipients on new, incomplete mailing
261 subject: 'placeholder', // for previewing recipients on new, incomplete mailing
262 options: {force_rollback: 1},
263 'api.mailing_job.create': 1, // note: exact match to API default
264 'api.MailingRecipients.get': {
265 mailing_id: '$value.id',
266 options: {limit: previewLimit},
267 'api.contact.getvalue': {'return': 'display_name'},
268 'api.email.getvalue': {'return': 'email'}
269 }
270 });
271 return crmApi('Mailing', 'create', params).then(function (recipResult) {
272 // changes rolled back, so we don't care about updating mailing
273 return recipResult.values[recipResult.id]['api.MailingRecipients.get'].values;
274 });
275 },
276
277 // Save a (draft) mailing
278 // @param mailing Object (per APIv3)
279 // @return Promise
280 save: function (mailing) {
281 var params = angular.extend({}, mailing, {
282 'api.mailing_job.create': 0 // note: exact match to API default
283 });
284
285 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
286 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
287 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
288 delete params.scheduled_date;
289
290 delete params.jobs;
291
292 return crmApi('Mailing', 'create', params).then(function (result) {
293 if (result.id && !mailing.id) {
294 mailing.id = result.id;
295 } // no rollback, so update mailing.id
296 // Perhaps we should reload mailing based on result?
297 return mailing;
298 });
299 },
300
301 // Schedule/send the mailing
302 // @param mailing Object (per APIv3)
303 // @return Promise
304 submit: function (mailing) {
305 var crmMailingMgr = this;
306 var params = {
307 id: mailing.id,
308 approval_date: crmNow(),
309 scheduled_date: mailing.scheduled_date ? mailing.scheduled_date : crmNow()
310 };
311 return crmApi('Mailing', 'submit', params)
312 .then(function (result) {
313 angular.extend(mailing, result.values[result.id]); // Perhaps we should reload mailing based on result?
314 return crmMailingMgr._loadJobs(mailing);
315 })
316 .then(function () {
317 return mailing;
318 });
319 },
320
321 // Immediately send a test message
322 // @param mailing Object (per APIv3)
323 // @param to Object with either key "email" (string) or "gid" (int)
324 // @return Promise for a list of delivery reports
325 sendTest: function (mailing, recipient) {
326 var params = angular.extend({}, mailing, {
327 // options: {force_rollback: 1}, // Test mailings include tracking features, so the mailing must be persistent
328 'api.Mailing.send_test': {
329 mailing_id: '$value.id',
330 test_email: recipient.email,
331 test_group: recipient.gid
332 }
333 });
334
335 // WORKAROUND: Mailing.create (aka CRM_Mailing_BAO_Mailing::create()) interprets scheduled_date
336 // as an *intent* to schedule and creates tertiary records. Saving a draft with a scheduled_date
337 // is therefore not allowed. Remove this after fixing Mailing.create's contract.
338 delete params.scheduled_date;
339
340 delete params.jobs;
341
342 return crmApi('Mailing', 'create', params).then(function (result) {
343 if (result.id && !mailing.id) {
344 mailing.id = result.id;
345 } // no rollback, so update mailing.id
346 return result.values[result.id]['api.Mailing.send_test'].values;
347 });
348 }
349 };
350 });
351
352 // The preview manager performs preview actions while putting up a visible UI (e.g. dialogs & status alerts)
353 angular.module('crmMailing').factory('crmMailingPreviewMgr', function (dialogService, crmMailingMgr, crmStatus) {
354 return {
355 // @param mode string one of 'html', 'text', or 'full'
356 // @return Promise
357 preview: function preview(mailing, mode) {
358 var templates = {
359 html: partialUrl('dialog/previewHtml.html'),
360 text: partialUrl('dialog/previewText.html'),
361 full: partialUrl('dialog/previewFull.html')
362 };
363 var result = null;
364 var p = crmMailingMgr
365 .preview(mailing)
366 .then(function (content) {
367 var options = {
368 autoOpen: false,
369 modal: true,
370 title: ts('Subject: %1', {
371 1: content.subject
372 })
373 };
374 result = dialogService.open('previewDialog', templates[mode], content, options);
375 });
376 crmStatus({start: ts('Previewing'), success: ''}, p);
377 return result;
378 },
379
380 // @param to Object with either key "email" (string) or "gid" (int)
381 // @return Promise
382 sendTest: function sendTest(mailing, recipient) {
383 var promise = crmMailingMgr.sendTest(mailing, recipient)
384 .then(function (deliveryInfos) {
385 var count = Object.keys(deliveryInfos).length;
386 if (count === 0) {
387 CRM.alert(ts('Could not identify any recipients. Perhaps the group is empty?'));
388 }
389 })
390 ;
391 return crmStatus({start: ts('Sending...'), success: ts('Sent')}, promise);
392 }
393 };
394 });
395
396 })(angular, CRM.$, CRM._);