google-debug.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  1. /**
  2. * See https://developers.google.com/api-client-library/javascript/
  3. * See https://developers.google.com/apis-explorer/#p/
  4. *
  5. * googleApis: { 'calendar': { version: 'v3' } }
  6. */
  7. Ext.define('Ext.google.ux.Client', {
  8. extend: 'Ext.Mixin',
  9. mixins: [
  10. 'Ext.mixin.Mashup'
  11. ],
  12. requiredScripts: [
  13. '//apis.google.com/js/client.js?onload=_ext_google_ux_client_initialize_'
  14. ],
  15. statics: {
  16. getApiVersion: function(api) {
  17. var library = this.libraries[api];
  18. return library && library.state == 2 ? library.version : null;
  19. }
  20. },
  21. mixinConfig: {
  22. extended: function(baseClass, derivedClass, classBody) {
  23. this.load(classBody.googleApis);
  24. }
  25. },
  26. onClassMixedIn: function(cls) {
  27. this.load(cls.prototype.googleApis);
  28. },
  29. privates: {
  30. statics: {
  31. /**
  32. * @property {Boolean} initialized
  33. * `true` if the google client has been loaded and initialized.
  34. * @private
  35. */
  36. initialized: false,
  37. /**
  38. * @property {Boolean} blocked
  39. * `true` if this class has blocked Ext.env.Ready, else false.
  40. * @private
  41. */
  42. blocked: false,
  43. /**
  44. * @property {Number} loading
  45. * Keep track of how many libraries are loading.
  46. * @private
  47. */
  48. loading: 0,
  49. /**
  50. * @property {Object} libraries
  51. * Information about required libraries.
  52. * { `api_name`: { version: string, state: int }
  53. * state: 0 (pending), 1 (loading), 2 (loaded)
  54. * Example: { calendar: { version: 'v1', state: 1 } }
  55. * @private
  56. */
  57. libraries: {},
  58. load: function(apis) {
  59. var libraries = this.libraries,
  60. version, library;
  61. if (!Ext.isObject(apis)) {
  62. return;
  63. }
  64. Ext.Object.each(apis, function(api, cfg) {
  65. version = cfg.version || 'v1';
  66. library = libraries[api];
  67. if (!Ext.isDefined(library)) {
  68. libraries[api] = {
  69. version: version,
  70. state: 0
  71. };
  72. } else if (library.version !== version) {
  73. Ext.log.error('Google API: failed to load version "' + version + '" of the', '"' + api + '" API: "' + library.version + '" already loaded.');
  74. }
  75. });
  76. this.refresh();
  77. },
  78. refresh: function() {
  79. var me = this;
  80. if (!me.initialized) {
  81. return;
  82. }
  83. if (!me.blocked) {
  84. Ext.env.Ready.block();
  85. me.blocked = true;
  86. }
  87. Ext.Object.each(me.libraries, function(api, library) {
  88. if (library.state == 0) {
  89. library.state = 1;
  90. // loading
  91. gapi.client.load(api, library.version, function() {
  92. library.state = 2;
  93. // loaded
  94. if (!--me.loading) {
  95. me.refresh();
  96. }
  97. });
  98. }
  99. if (library.state == 1) {
  100. me.loading++;
  101. }
  102. });
  103. if (!me.loading && me.blocked) {
  104. Ext.env.Ready.unblock();
  105. me.blocked = false;
  106. }
  107. },
  108. initialize: function() {
  109. this.initialized = true;
  110. this.refresh();
  111. }
  112. }
  113. }
  114. });
  115. // See https://developers.google.com/api-client-library/javascript/features/authentication
  116. _ext_google_ux_client_initialize_ = function() {
  117. gapi.auth.init(function() {
  118. Ext.google.ux.Client.initialize();
  119. });
  120. };
  121. /**
  122. * Base proxy for accessing **[Google API](https://developers.google.com/apis-explorer/#p/)** resources.
  123. */
  124. Ext.define('Ext.google.data.AbstractProxy', {
  125. extend: 'Ext.data.proxy.Server',
  126. mixins: [
  127. 'Ext.google.ux.Client'
  128. ],
  129. // TODO: Batch actions
  130. // https://developers.google.com/api-client-library/javascript/features/batch
  131. /**
  132. * @cfg batchActions
  133. * @inheritdoc
  134. */
  135. batchActions: false,
  136. /**
  137. * @cfg reader
  138. * @inheritdoc
  139. */
  140. reader: {
  141. type: 'json',
  142. rootProperty: 'items',
  143. messageProperty: 'error'
  144. },
  145. /**
  146. * @method buildApiRequests
  147. * Returns a list of API request(s), **not executed**.
  148. * @param {Ext.data.Request} request The data request
  149. * @return {Object[]} API request(s)
  150. * @abstract
  151. */
  152. /**
  153. * @protected
  154. * @inheritdoc
  155. */
  156. doRequest: function(operation) {
  157. var me = this,
  158. request = me.buildRequest(operation),
  159. writer = me.getWriter(),
  160. error = false;
  161. if (writer && operation.allowWrite()) {
  162. request = writer.write(request);
  163. }
  164. me.execute(me.buildApiRequests(request)).then(function(response) {
  165. me.processApiResponse(operation, request, response);
  166. });
  167. return request;
  168. },
  169. /**
  170. * @method buildUrl
  171. * @protected
  172. * @inheritdoc
  173. */
  174. buildUrl: function(request) {
  175. return '';
  176. },
  177. privates: {
  178. execute: function(requests) {
  179. requests = [].concat(requests);
  180. // BUG: when using the gapi batch feature and trying to modify the same event
  181. // more than one time, the request partially fails and returns a 502 error.
  182. // See https://code.google.com/a/google.com/p/apps-api-issues/issues/detail?id=4528
  183. // TODO: use the following code once fixed! also check that it doesn't break
  184. // maxResults limit for event list requests.
  185. //var batch = gapi.client.newBatch();
  186. //Ext.Array.each(requests, function(r, i) { batch.add(r, { id: i }); });
  187. //return batch.execute();
  188. // WORKAROUND for the issue above (REMOVE ME)
  189. var results = [];
  190. return Ext.Array.reduce(requests, function(sequence, r) {
  191. return sequence.then(function() {
  192. return r.then(function(result) {
  193. results.push(result);
  194. });
  195. });
  196. }, Ext.Deferred.resolved()).then(function() {
  197. return {
  198. result: results
  199. };
  200. });
  201. },
  202. processApiResponse: function(operation, request, responses) {
  203. var error = false,
  204. results = [];
  205. // responses.result is not a regular Object, can't iterate with Ext.Object.each()
  206. Ext.each(Object.keys(responses.result), function(index) {
  207. var result = responses.result[index].result;
  208. if (result.error) {
  209. error = result.error.message;
  210. return false;
  211. }
  212. results.push(result);
  213. });
  214. this.processResponse(true, operation, request, {
  215. results: error ? [] : results,
  216. success: !error,
  217. error: error
  218. });
  219. },
  220. sanitizeItems: function(items) {
  221. var results = [],
  222. ids = [];
  223. // Batch can return different versions of the same record, only keep the last one.
  224. Ext.Array.each(items, function(item) {
  225. if (!Ext.Array.contains(ids, item.id)) {
  226. results.push(item);
  227. ids.push(item.id);
  228. }
  229. }, this, true);
  230. return results;
  231. }
  232. }
  233. });
  234. /**
  235. * Proxy to access Google **[event resources](https://developers.google.com/google-apps/calendar/v3/reference/events)**.
  236. */
  237. Ext.define('Ext.google.data.EventsProxy', {
  238. extend: 'Ext.google.data.AbstractProxy',
  239. alias: 'proxy.google-events',
  240. googleApis: {
  241. 'calendar': {
  242. version: 'v3'
  243. }
  244. },
  245. /**
  246. * @method buildApiRequests
  247. * @protected
  248. * @inheritdoc
  249. */
  250. buildApiRequests: function(request) {
  251. var me = this,
  252. action = request.getAction();
  253. switch (action) {
  254. case 'read':
  255. return me.buildReadApiRequests(request);
  256. case 'create':
  257. return me.buildCreateApiRequests(request);
  258. case 'update':
  259. return me.buildUpdateApiRequests(request);
  260. case 'destroy':
  261. return me.buildDestroyApiRequests(request);
  262. default:
  263. Ext.raise('unsupported request: events.' + action);
  264. return null;
  265. }
  266. },
  267. /**
  268. * @method extractResponseData
  269. * @protected
  270. * @inheritdoc
  271. */
  272. extractResponseData: function(response) {
  273. var me = this,
  274. data = me.callParent(arguments),
  275. items = [];
  276. Ext.each(data.results, function(result) {
  277. switch (result.kind) {
  278. case 'calendar#events':
  279. items = items.concat(result.items.map(me.fromApiEvent.bind(me)));
  280. break;
  281. case 'calendar#event':
  282. items.push(me.fromApiEvent(result));
  283. break;
  284. default:
  285. break;
  286. }
  287. });
  288. return {
  289. items: me.sanitizeItems(items),
  290. success: data.success,
  291. error: data.error
  292. };
  293. },
  294. privates: {
  295. // https://developers.google.com/google-apps/calendar/v3/reference/events
  296. toApiEvent: function(data, allDay) {
  297. var res = {};
  298. Ext.Object.each(data, function(key, value) {
  299. var dateTime = null,
  300. date = null;
  301. switch (key) {
  302. case 'calendarId':
  303. case 'description':
  304. res[key] = value;
  305. break;
  306. case 'id':
  307. res.eventId = value;
  308. break;
  309. case 'title':
  310. res.summary = value;
  311. break;
  312. case 'startDate':
  313. case 'endDate':
  314. if (allDay) {
  315. date = new Date(value);
  316. date.setHours(0, -date.getTimezoneOffset());
  317. date = Ext.Date.format(date, 'Y-m-d');
  318. } else {
  319. dateTime = Ext.Date.format(new Date(value), 'c');
  320. };
  321. // Need to explicitly set unused date field to null
  322. // http://stackoverflow.com/a/35658479
  323. res[key.slice(0, -4)] = {
  324. date: date,
  325. dateTime: dateTime
  326. };
  327. break;
  328. default:
  329. break;
  330. }
  331. });
  332. return res;
  333. },
  334. // https://developers.google.com/google-apps/calendar/v3/reference/events
  335. fromApiEvent: function(data) {
  336. var res = {
  337. allDay: true
  338. };
  339. Ext.Object.each(data, function(key, value) {
  340. var date, offset, allDay;
  341. switch (key) {
  342. case 'id':
  343. case 'description':
  344. res[key] = value;
  345. break;
  346. case 'summary':
  347. res.title = value;
  348. break;
  349. case 'start':
  350. case 'end':
  351. date = Ext.Date.parse(value.dateTime || value.date, 'C');
  352. offset = date.getTimezoneOffset();
  353. allDay = !!value.date;
  354. // IMPORTANT: all day events must have their time equal to 00:00 GMT
  355. if (allDay && offset !== 0) {
  356. date.setHours(0, -offset);
  357. };
  358. res[key + 'Date'] = date;
  359. res.allDay = res.allDay && allDay;
  360. break;
  361. default:
  362. break;
  363. }
  364. });
  365. return res;
  366. },
  367. // See https://developers.google.com/google-apps/calendar/v3/reference/events/list
  368. buildReadApiRequests: function(request) {
  369. // by default, the API returns max 250 events per request, up to 2500. Since we
  370. // don't have control on the min & max requested times, and don't know how many
  371. // events will be returned, let's split requests per 3 months and set maxResults
  372. // to 2500 (~26 events per day - should be enough!?).
  373. var rparams = request.getParams(),
  374. start = new Date(rparams.startDate),
  375. end = new Date(rparams.endDate),
  376. requests = [],
  377. next;
  378. while (start < end) {
  379. next = Ext.Date.add(start, Ext.Date.MONTH, 3);
  380. if (next > end) {
  381. next = end;
  382. }
  383. requests.push(gapi.client.calendar.events.list({
  384. calendarId: rparams.calendar,
  385. timeMin: Ext.Date.format(start, 'C'),
  386. timeMax: Ext.Date.format(next, 'C'),
  387. singleEvents: true,
  388. maxResults: 2500
  389. }));
  390. start = next;
  391. }
  392. return requests;
  393. },
  394. // https://developers.google.com/google-apps/calendar/v3/reference/events/insert
  395. buildCreateApiRequests: function(request) {
  396. var record = request.getRecords()[0];
  397. // batch not currently supported!
  398. return gapi.client.calendar.events.insert(this.toApiEvent(request.getJsonData(), record.get('allDay')));
  399. },
  400. // https://developers.google.com/google-apps/calendar/v3/reference/events/patch
  401. // https://developers.google.com/google-apps/calendar/v3/reference/events/move
  402. buildUpdateApiRequests: function(request) {
  403. var record = request.getRecords()[0],
  404. // batch not currently supported!
  405. params = this.toApiEvent(request.getJsonData(), record.get('allDay')),
  406. prevCalendarId = record.getModified('calendarId'),
  407. currCalendarId = record.get('calendarId'),
  408. eventId = record.getId(),
  409. requests = [];
  410. // REQUIRED fields for the patch API
  411. params.calendarId = currCalendarId;
  412. params.eventId = eventId;
  413. if (prevCalendarId && prevCalendarId !== currCalendarId) {
  414. // The event has been moved to another calendar
  415. requests.push(gapi.client.calendar.events.move({
  416. destination: currCalendarId,
  417. calendarId: prevCalendarId,
  418. eventId: eventId
  419. }));
  420. }
  421. if (Object.keys(params).length > 2) {
  422. // There is fields to update other than the calendarId + eventId
  423. requests.push(gapi.client.calendar.events.patch(params));
  424. }
  425. return requests;
  426. },
  427. // https://developers.google.com/google-apps/calendar/v3/reference/events/delete
  428. buildDestroyApiRequests: function(request) {
  429. var record = request.getRecords()[0];
  430. // batch not currently supported!
  431. data = request.getJsonData();
  432. // The current calendar implementation nullifies the calendar ID before deleting
  433. // it, so let's get it from the previous values if not anymore in data.
  434. data.calendarId = data.calendarId || record.get('calendarId') || record.getPrevious('calendarId');
  435. // ['delete'] to make YUI happy
  436. return gapi.client.calendar.events['delete']({
  437. 'calendarId': data.calendarId,
  438. 'eventId': data.id
  439. });
  440. }
  441. }
  442. });
  443. /**
  444. * Proxy to access Google **[calendar resources](https://developers.google.com/google-apps/calendar/v3/reference/calendarList)**.
  445. */
  446. Ext.define('Ext.google.data.CalendarsProxy', {
  447. extend: 'Ext.google.data.AbstractProxy',
  448. alias: 'proxy.google-calendars',
  449. requires: [
  450. 'Ext.google.data.EventsProxy'
  451. ],
  452. googleApis: {
  453. 'calendar': {
  454. version: 'v3'
  455. }
  456. },
  457. /**
  458. * @method buildApiRequests
  459. * @protected
  460. * @inheritdoc
  461. */
  462. buildApiRequests: function(request) {
  463. var me = this,
  464. action = request.getAction();
  465. switch (action) {
  466. case 'read':
  467. return me.buildReadApiRequests(request);
  468. case 'update':
  469. return me.buildUpdateApiRequests(request);
  470. default:
  471. Ext.raise('unsupported request: calendars.' + action);
  472. return null;
  473. }
  474. },
  475. /**
  476. * @method extractResponseData
  477. * @protected
  478. * @inheritdoc
  479. */
  480. extractResponseData: function(response) {
  481. var me = this,
  482. data = me.callParent(arguments),
  483. items = [];
  484. // We assume that the response contains only results of the same kind.
  485. Ext.each(data.results, function(result) {
  486. switch (result.kind) {
  487. case 'calendar#calendarList':
  488. items = items.concat(result.items.map(me.fromApiCalendar.bind(me)));
  489. break;
  490. default:
  491. break;
  492. }
  493. });
  494. return {
  495. items: me.sanitizeItems(items),
  496. success: data.success,
  497. error: data.error
  498. };
  499. },
  500. privates: {
  501. // https://developers.google.com/google-apps/calendar/v3/reference/calendarList#resource
  502. toApiCalendar: function(data) {
  503. var res = {};
  504. Ext.Object.each(data, function(key, value) {
  505. switch (key) {
  506. case 'id':
  507. res.calendarId = value;
  508. break;
  509. case 'hidden':
  510. res.selected = !value;
  511. break;
  512. default:
  513. break;
  514. }
  515. });
  516. return res;
  517. },
  518. // https://developers.google.com/google-apps/calendar/v3/reference/calendarList#resource
  519. fromApiCalendar: function(data) {
  520. var record = {
  521. hidden: !data.selected,
  522. editable: false,
  523. eventStore: {
  524. autoSync: true,
  525. proxy: {
  526. type: 'google-events',
  527. resourceTypes: 'events'
  528. }
  529. }
  530. };
  531. Ext.Object.each(data, function(key, value) {
  532. switch (key) {
  533. case 'id':
  534. case 'description':
  535. record[key] = value;
  536. break;
  537. case 'backgroundColor':
  538. record.color = value;
  539. break;
  540. case 'summary':
  541. record.title = value;
  542. break;
  543. case 'accessRole':
  544. record.editable = (value == 'owner' || value == 'writer');
  545. break;
  546. default:
  547. break;
  548. }
  549. });
  550. return record;
  551. },
  552. // https://developers.google.com/google-apps/calendar/v3/reference/calendarList/list
  553. buildReadApiRequests: function(request) {
  554. return gapi.client.calendar.calendarList.list();
  555. },
  556. // https://developers.google.com/google-apps/calendar/v3/reference/calendarList/patch
  557. buildUpdateApiRequests: function(request) {
  558. var data = this.toApiCalendar(request.getJsonData());
  559. return gapi.client.calendar.calendarList.patch(data);
  560. }
  561. }
  562. });
  563. /**
  564. * This base class can be used by derived classes to dynamically require Google API's.
  565. */
  566. Ext.define('Ext.ux.google.Api', {
  567. mixins: [
  568. 'Ext.mixin.Mashup'
  569. ],
  570. requiredScripts: [
  571. '//www.google.com/jsapi'
  572. ],
  573. statics: {
  574. loadedModules: {}
  575. },
  576. /*
  577. * feeds: [ callback1, callback2, .... ] transitions to -> feeds : true (when complete)
  578. */
  579. onClassExtended: function(cls, data, hooks) {
  580. var onBeforeClassCreated = hooks.onBeforeCreated,
  581. Api = this;
  582. // the Ext.ux.google.Api class
  583. hooks.onBeforeCreated = function(cls, data) {
  584. var me = this,
  585. apis = [],
  586. requiresGoogle = Ext.Array.from(data.requiresGoogle),
  587. loadedModules = Api.loadedModules,
  588. remaining = 0,
  589. callback = function() {
  590. if (!--remaining) {
  591. onBeforeClassCreated.call(me, cls, data, hooks);
  592. }
  593. Ext.env.Ready.unblock();
  594. },
  595. api, i, length;
  596. /*
  597. * requiresGoogle: [
  598. * 'feeds',
  599. * { api: 'feeds', version: '1.x',
  600. * callback : fn, nocss : true } //optionals
  601. * ]
  602. */
  603. length = requiresGoogle.length;
  604. for (i = 0; i < length; ++i) {
  605. if (Ext.isString(api = requiresGoogle[i])) {
  606. apis.push({
  607. api: api
  608. });
  609. } else if (Ext.isObject(api)) {
  610. apis.push(Ext.apply({}, api));
  611. }
  612. }
  613. Ext.each(apis, function(api) {
  614. var name = api.api,
  615. version = String(api.version || '1.x'),
  616. module = loadedModules[name];
  617. if (!module) {
  618. ++remaining;
  619. Ext.env.Ready.block();
  620. loadedModules[name] = module = [
  621. callback
  622. ].concat(api.callback || []);
  623. delete api.api;
  624. delete api.version;
  625. if (!window.google) {
  626. Ext.raise("'google' is not defined.");
  627. return false;
  628. }
  629. google.load(name, version, Ext.applyIf({
  630. callback: function() {
  631. loadedModules[name] = true;
  632. for (var n = module.length; n-- > 0; ) {
  633. module[n]();
  634. }
  635. }
  636. }, //iterate callbacks in reverse
  637. api));
  638. } else if (module !== true) {
  639. module.push(callback);
  640. }
  641. });
  642. if (!remaining) {
  643. onBeforeClassCreated.call(me, cls, data, hooks);
  644. }
  645. };
  646. }
  647. });