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