Merge lp:~hatch/juju-gui/bundle-detail-view into lp:juju-gui/experimental

Proposed by Jeff Pihach
Status: Needs review
Proposed branch: lp:~hatch/juju-gui/bundle-detail-view
Merge into: lp:juju-gui/experimental
Diff against target: 1749 lines (+855/-797)
7 files modified
app/modules-debug.js (+1/-1)
app/store/charm.js (+0/-769)
app/store/charmworld.js (+798/-0)
app/subapps/browser/browser.js (+1/-2)
app/subapps/browser/views/charm.js (+53/-19)
app/templates/bundle.handlebars (+2/-2)
undocumented (+0/-4)
To merge this branch: bzr merge lp:~hatch/juju-gui/bundle-detail-view
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+188456@code.launchpad.net

Description of the change

WIP - Bundle details view

This is a WIP fist pass for the bundle details view. Because there is
no way to access this from the application you can access it by
using the following URL:

http://localhost:8888/bundle/~bac/wiki/3/wiki/:flags:/charmworldv3/

Unfortunately the rename caused the diff to go wonky so the additions
in charmworld.js were the bundle and _bundle functions.

https://codereview.appspot.com/14153043/

To post a comment you must log in.
Revision history for this message
Jeff Pihach (hatch) wrote :

Reviewers: mp+188456_code.launchpad.net,

Message:
Please take a look.

Description:
WIP - Bundle details view

This is a WIP fist pass for the bundle details view. Because there is
no way to access this from the application you can access it by
using the following URL:

http://localhost:8888/bundle/~bac/wiki/3/wiki/:flags:/charmworldv3/

https://code.launchpad.net/~hatch/juju-gui/bundle-detail-view/+merge/188456

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/14153043/

Affected files (+857, -797 lines):
   A [revision details]
   M app/modules-debug.js
   D app/store/charm.js
   A app/store/charmworld.js
   M app/subapps/browser/browser.js
   M app/subapps/browser/views/charm.js
   M app/templates/bundle.handlebars
   M undocumented

1100. By Jeff Pihach

renaming back to attempt to get a proper diff

1101. By Jeff Pihach

bzr rename

Revision history for this message
Jeff Pihach (hatch) wrote :
Revision history for this message
Richard Harding (rharding) wrote :
Download full text (5.1 KiB)

I think this is exactly on the right path. I'd tweak a few things. I
think the View can be a bit simpler by dealing with a single ATTR for a
model instance or a token id and that logic should be pushed to the
store. That way all those decisions are made there.

We can chat about details later but looks good and thanks for working on
getting this into play.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js
File app/store/charmworld.js (right):

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode34
app/store/charmworld.js:34: juju.charmworld = ns;
ns is already defined? why do we need juju.charmworld

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode234
app/store/charmworld.js:234: Public method to make an API call to fetch
a bundle from Charmworld.
don't need "Public method" we already can tell that.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode242
app/store/charmworld.js:242: bundle: function(bundleID, callbacks,
bindScope) {
if you compare this to the charm call, there's some work we could do
here around the caching. We should add support for caching details as we
do in the charms so that subsequent calls will be instantly available.
The cache is maintained by the store instance I think as I know it's
shared around both Browser and main app code.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode247
app/store/charmworld.js:247: API call to fetch a charm's details.
bundle's details.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode273
app/store/charmworld.js:273: promiseCharm: function(charmId, cache,
defaultSeries) {
should we have a promiseBundle? This came after the original charm call
and maybe working with a promise from the start would be nice here?

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode383
app/store/charmworld.js:383: iconpath: function(charmID) {
did you verify this works for bundle ids?

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode415
app/store/charmworld.js:415: buildCategoryIconPath: function(categoryID)
{
any calls to this are hints that we need to look for the later check for
using the default charm icon and add a check for default bundle icon.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode509
app/store/charmworld.js:509: related: function(charmID, callbacks,
bindScope) {
we'll need a bundle related method as well. Per the call with Gary
today, related bundles are lists of other bundles in the same basket as
the current one.

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/browser.js
File app/subapps/browser/browser.js (right):

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/browser.js#newcode924
app/subapps/browser/browser.js:924: if (idBits.length > 1) {
I'm assuming existing tests pass with this change?

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/views/charm.js
File app/subapps/browser/views/charm.js (right):

https://codereview.appspot.com/14153043/diff/...

Read more...

Revision history for this message
Jeff Pihach (hatch) wrote :
Download full text (3.8 KiB)

Thanks for the review. Replies are below - lets chat in the morning and
then I will start a few real branches to tackle these changes.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js
File app/store/charmworld.js (right):

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode34
app/store/charmworld.js:34: juju.charmworld = ns;
Not sure, this was from the old code.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode242
app/store/charmworld.js:242: bundle: function(bundleID, callbacks,
bindScope) {
Right, I wasn't sure how the cache was implemented so I left it out at
this point to get a functional example asap.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode273
app/store/charmworld.js:273: promiseCharm: function(charmId, cache,
defaultSeries) {
I agree, as I was reading through this stuff it would be great if we
only used the promises for charms and bundles.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode383
app/store/charmworld.js:383: iconpath: function(charmID) {
It does not so I removed it from the template - atm it looks like we
only have default icons, but will confirm in the am.

https://codereview.appspot.com/14153043/diff/4001/app/store/charmworld.js#newcode509
app/store/charmworld.js:509: related: function(charmID, callbacks,
bindScope) {
Hmm good call - that can be a follow-up

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/browser.js
File app/subapps/browser/browser.js (right):

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/browser.js#newcode924
app/subapps/browser/browser.js:924: if (idBits.length > 1) {
You bet - I haven't manually checked the validity of all the paths yet
but all of the tests pass and qa was ok.

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/views/charm.js
File app/subapps/browser/views/charm.js (right):

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/views/charm.js#newcode36
app/subapps/browser/views/charm.js:36: ns.BrowserCharmView =
Y.Base.create('browser-view-charmview', Y.View, [
We were trying to come up with a good name :)

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/views/charm.js#newcode762
app/subapps/browser/views/charm.js:762: _renderBundleView:
function(bundle) {
It's definitely missing a lot of logic :) It's still a WIP - but we also
don't have UX for this view yet so any effort spent here might be wasted
- we can talk in the am.

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/views/charm.js#newcode781
app/subapps/browser/views/charm.js:781: charmID = this.get('charmID'),
Yeah I had intended on renaming this assuming the impl made sense.

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/views/charm.js#newcode786
app/subapps/browser/views/charm.js:786: if (this.get('charm')) {
I was also thinking of something similar but I didn't want to modify
that deep for this prototype

https://codereview.appspot.com/14153043/diff/4001/app/subapps/browser/views/charm.js#newcode793
app/subapps/brows...

Read more...

Unmerged revisions

1101. By Jeff Pihach

bzr rename

1100. By Jeff Pihach

renaming back to attempt to get a proper diff

1099. By Jeff Pihach

added renamed file

1098. By Jeff Pihach

first pass at adding a bundle view handling

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'app/modules-debug.js'
2--- app/modules-debug.js 2013-09-27 16:26:18 +0000
3+++ app/modules-debug.js 2013-09-30 21:13:03 +0000
4@@ -371,7 +371,7 @@
5 },
6
7 'juju-charm-store': {
8- fullpath: '/juju-ui/store/charm.js'
9+ fullpath: '/juju-ui/store/charmworld.js'
10 },
11
12 'juju-websocket-logging': {
13
14=== removed file 'app/store/charm.js'
15--- app/store/charm.js 2013-09-27 14:15:38 +0000
16+++ app/store/charm.js 1970-01-01 00:00:00 +0000
17@@ -1,769 +0,0 @@
18-/*
19-This file is part of the Juju GUI, which lets users view and manage Juju
20-environments within a graphical interface (https://launchpad.net/juju-gui).
21-Copyright (C) 2012-2013 Canonical Ltd.
22-
23-This program is free software: you can redistribute it and/or modify it under
24-the terms of the GNU Affero General Public License version 3, as published by
25-the Free Software Foundation.
26-
27-This program is distributed in the hope that it will be useful, but WITHOUT
28-ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
29-SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
30-General Public License for more details.
31-
32-You should have received a copy of the GNU Affero General Public License along
33-with this program. If not, see <http://www.gnu.org/licenses/>.
34-*/
35-
36-'use strict';
37-
38-/**
39- * Provide the Charmworld API datastore class.
40- *
41- * @module store
42- * @submodule store.charm
43- */
44-
45-YUI.add('juju-charm-store', function(Y) {
46- var juju = Y.namespace('juju'),
47- ns = Y.namespace('juju.charmworld'),
48- models = Y.namespace('juju.models');
49-
50- // Make Y.juju.charmworld refernce the "juju.charmworld" namespace.
51- juju.charmworld = ns;
52-
53- ns.ApiHelper = Y.Base.create('ApiHelper', Y.Base, [], {
54-
55- /**
56- Initializer for the class.
57-
58- @method initializer
59- @param {Object} cfg The configuration for the API interface.
60- @return {undefined} Nothing.
61- */
62- initializer: function(cfg) {
63- this.sendRequest = cfg.sendRequest;
64- },
65-
66- /**
67- Send a request and handle the response from the API.
68-
69- @method makeRequest
70- @param {Object} args any query params and arguments required.
71- @return {undefined} Nothing.
72- */
73- makeRequest: function(apiEndpoint, callbacks, args) {
74- // Any query string args need to be put onto the endpoint for calling.
75- if (args) {
76- apiEndpoint = apiEndpoint + '?' + Y.QueryString.stringify(args);
77- }
78-
79- this.sendRequest({
80- request: apiEndpoint,
81- callback: {
82- success: function(io_request) {
83- var res = Y.JSON.parse(
84- io_request.response.results[0].responseText
85- );
86- callbacks.success(res);
87- },
88-
89- 'failure': function(io_request) {
90- var respText = io_request.response.results[0].responseText,
91- res;
92- if (respText) {
93- res = Y.JSON.parse(respText);
94- }
95- callbacks.failure(res, io_request);
96- }
97- }
98- });
99- },
100-
101- /**
102- Normalize a charm name so we can request its full data. Charm lookup
103- requires a very specific form of the charm identifier.
104-
105- series/charm-revision
106-
107- where revision can currently (API v2) be any numeric placeholder.
108-
109- @method normalizeCharmId
110- @param {String} charmId to normalize.
111- @param {String} [defaultSeries='precise'] The series to use if none is
112- specified in the charm ID.
113- @return {String} normalized id.
114- */
115- normalizeCharmId: function(charmId, defaultSeries) {
116- var result = charmId;
117- if (/^(cs:|local:)/.exec(result)) {
118- result = result.slice(result.indexOf(':') + 1);
119- }
120-
121- if (result.indexOf('/') === -1) {
122- if (!defaultSeries) {
123- console.warn('No default series provided when normalizing charm ' +
124- 'ID. Using "precise".');
125- defaultSeries = 'precise';
126- }
127- result = defaultSeries + '/' + result;
128- }
129- return result;
130- }
131-
132- }, {
133- ATTRS: {
134- }
135- });
136-
137- /**
138- * Charmworld API version 3 interface.
139- *
140- * @class APIv3
141- * @extends {Base}
142- *
143- */
144- ns.APIv3 = Y.Base.create('APIv3', Y.Base, [], {
145- _apiRoot: 'api/3',
146-
147- /**
148- * Send the actual request and handle response from the API.
149- *
150- * @method _makeRequest
151- * @param {Object} args any query params and arguments required.
152- * @private
153- *
154- */
155- _makeRequest: function(apiEndpoint, callbacks, args) {
156- // If we're in the noop state, just call the error callback.
157- if (this.get('noop')) {
158- callbacks.failure('noop failure');
159- return;
160- }
161- // Delegate the request making to the helper object.
162- this.apiHelper.makeRequest(apiEndpoint, callbacks, args);
163- },
164-
165- /**
166- * API call to fetch autocomplete suggestions based on the current term.
167- *
168- * @method autocomplete
169- * @param {Object} query the filters data object for search.
170- * @param {Object} filters the filters data object for search.
171- * @param {Object} callbacks the success/failure callbacks to use.
172- * @param {Object} bindScope the scope of *this* in the callbacks.
173- */
174- autocomplete: function(filters, callbacks, bindScope) {
175- var endpoint = 'search';
176- // Force that this is an autocomplete call to perform matching on the
177- // start of names vs a fulltext search.
178- filters.autocomplete = 'true';
179- filters.limit = 5;
180- if (bindScope) {
181- callbacks.success = Y.bind(callbacks.success, bindScope);
182- callbacks.failure = Y.bind(callbacks.failure, bindScope);
183- }
184- this._makeRequest(endpoint, callbacks, filters);
185- },
186-
187-
188- /**
189- * API call to fetch a charm's details.
190- *
191- * @method charm
192- * @param {String} charmID the charm to fetch.
193- * @param {Object} callbacks the success/failure callbacks to use.
194- * @param {Object} bindScope the scope of *this* in the callbacks.
195- *
196- */
197- _charm: function(charmID, callbacks, bindScope) {
198- var endpoint = 'charm/' + charmID;
199- if (bindScope) {
200- callbacks.success = Y.bind(callbacks.success, bindScope);
201- callbacks.failure = Y.bind(callbacks.failure, bindScope);
202- }
203-
204- this._makeRequest(endpoint, callbacks);
205- },
206-
207- /**
208- * API call to fetch a charm's details, with an optional local cache.
209- *
210- * @method charmWithCache
211- * @param {String} charmID The charm to fetch This is the fully qualified
212- * charm name in the format scheme:series/charm-revision.
213- * @param {Object} callbacks The success/failure callbacks to use.
214- * @param {Object} bindScope The scope of "this" in the callbacks.
215- * @param {ModelList} [cache] a local cache of browser charms.
216- * @param {String} [defaultSeries='precise'] The series to use if none is
217- * specified in the charm ID.
218- */
219- charm: function(charmID, callbacks, bindScope, cache, defaultSeries) {
220- if (bindScope) {
221- callbacks.success = Y.bind(callbacks.success, bindScope);
222- }
223- if (cache) {
224- var charm = cache.getById(charmID);
225- if (charm) {
226- // If the charm was found in the cache, then we can declare success
227- // without ever making a request to charmworld.
228- Y.soon(function() {
229- // Since there wasn't really a request, there is no data, so we
230- // pass an empty object as the "data" parameter.
231- callbacks.success({}, charm);
232- });
233- return;
234- } else {
235- var successCB = callbacks.success;
236- callbacks.success = function(data) {
237- var charm = new Y.juju.models.Charm(data.charm);
238- if (data.metadata) {
239- charm.set('metadata', data.metadata);
240- }
241- cache.add(charm);
242- successCB(data, charm);
243- };
244- }
245- }
246- this._charm(this.apiHelper.normalizeCharmId(charmID, defaultSeries),
247- callbacks, bindScope);
248- },
249-
250- /**
251- Like the "charm" method but returning a Promise.
252-
253- @method promiseCharm
254- @param {String} charmId The ID of the charm to fetch.
255- @param {ModelList} cache A local cache of browser charms.
256- @param {String} [defaultSeries='precise'] The series to use if none is
257- specified in the charm ID.
258- @return {Promise} Returns a promise. Triggered with the result of calling
259- this.charm.
260- */
261- promiseCharm: function(charmId, cache, defaultSeries) {
262- var self = this;
263- return Y.Promise(function(resolve, reject) {
264- self.charm(charmId, { 'success': resolve, 'failure': reject },
265- self, cache, defaultSeries);
266- });
267- },
268-
269- /**
270- Promises to return the latest charm ID for a given charm if a newer one
271- exists; this also caches the newer charm if one is available.
272-
273- @method promiseUpgradeAvailability
274- @param {Charm} charm An existing charm potentially in need of an upgrade.
275- @param {ModelList} cache A local cache of browser charms.
276- @return {Promise} A promise for a newer charm ID or undefined.
277- */
278- promiseUpgradeAvailability: function(charm, cache) {
279- // Get the charm's store ID, then remove the version number to retrieve
280- // the latest version of the charm.
281- var storeId, revision;
282- if (charm instanceof Y.Model) {
283- storeId = charm.get('storeId');
284- revision = parseInt(charm.get('revision'), 10);
285- } else {
286- storeId = charm.url;
287- revision = parseInt(charm.revision, 10);
288- }
289- storeId = storeId.replace(/-\d+$/, '');
290- // XXX By using a cache we hide charm versions that have become available
291- // since we last requested the most recent version.
292- return this.promiseCharm(storeId, cache)
293- .then(function(latest) {
294- var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10);
295- if (latestVersion > revision) {
296- return latest.charm.id;
297- }
298- }, function(e) {
299- throw e;
300- });
301- },
302-
303- /**
304- * API call to search charms
305- *
306- * @method search
307- * @param {Object} filters the filters data object for search.
308- * @param {Object} callbacks the success/failure callbacks to use.
309- * @param {Object} bindScope the scope of *this* in the callbacks.
310- */
311- search: function(filters, callbacks, bindScope) {
312- var endpoint = 'search';
313- if (bindScope) {
314- callbacks.success = Y.bind(callbacks.success, bindScope);
315- callbacks.failure = Y.bind(callbacks.failure, bindScope);
316- }
317- this._makeRequest(endpoint, callbacks, filters);
318- },
319-
320- /**
321- * Fetch the contents of a charm's file.
322- *
323- * @method file
324- * @param {String} charmID The id of the charm's file we want.
325- * @param {String} filename The path/name of the file to fetch content.
326- * @param {Object} callbacks The success/failure callbacks.
327- * @param {Object} bindScope The scope for this in the callbacks.
328- *
329- */
330- file: function(charmID, filename, callbacks, bindScope) {
331- // If we're in the noop state, just call the error callback.
332- if (this.get('noop')) {
333- callbacks.failure('noop failure');
334- return;
335- }
336-
337- var endpoint = 'charm/' + charmID + '/file/' + filename;
338- if (bindScope) {
339- callbacks.success = Y.bind(callbacks.success, bindScope);
340- callbacks.failure = Y.bind(callbacks.failure, bindScope);
341- }
342-
343- this.get('datasource').sendRequest({
344- request: endpoint,
345- callback: {
346- success: function(io_request) {
347- callbacks.success(io_request.response.results[0].responseText);
348- },
349- 'failure': function(io_request) {
350- var respText = io_request.response.results[0].responseText,
351- res;
352- if (respText) {
353- res = Y.JSON.parse(respText);
354- }
355- callbacks.failure(res, io_request);
356- }
357- }
358- });
359- },
360-
361- /**
362- Generate the API path to a charm icon.
363- This is useful when generating links and references in HTML to the
364- charm's icon and is constructing the correct icon based on reviewed
365- status and categories on the charm.
366-
367- @method iconpath
368- @param {String} charmID The id of the charm to grab the icon for.
369- @return {String} The URL of the charm's icon.
370- */
371- iconpath: function(charmID) {
372- // If this is a local charm, then we need use a hard coded path to the
373- // default icon since we cannot fetch its category data or its own
374- // icon.
375- // XXX: #1202703 - this is a short term fix for the bug. Need longer
376- // term solution.
377- if (charmID.indexOf('local:') === 0) {
378- return this.get('apiHost') +
379- 'static/img/charm_160.svg';
380-
381- } else {
382- // Get the charm ID from the service. In some cases, this will be
383- // the charm URL with a protocol, which will need to be removed.
384- // The following regular expression removes everything up to the
385- // colon portion of the quote and leaves behind a charm ID.
386- charmID = charmID.replace(/^[^:]+:/, '');
387- return this.get('apiHost') + [
388- this._apiRoot,
389- 'charm',
390- charmID,
391- 'file',
392- 'icon.svg'].join('/');
393- }
394- },
395-
396- /**
397- * Generate the url to an icon for the category specified.
398- *
399- * @method buildCategoryIconPath
400- * @param {String} categoryID the id of the category to load an icon for.
401- *
402- */
403- buildCategoryIconPath: function(categoryID) {
404- return [
405- this.get('apiHost'),
406- 'static/img/category-',
407- categoryID,
408- '-bw.svg'
409- ].join('');
410- },
411-
412- /**
413- * Load the QA data for a specific charm.
414- *
415- * @method qa
416- * @param {String} charmID The charm to fetch QA data for.
417- * @param {Object} callbacks The success/failure callbacks to use.
418- * @param {Object} bindScope The scope for 'this' in the callbacks.
419- *
420- */
421- qa: function(charmID, callbacks, bindScope) {
422- var endpoint = 'charm/' + charmID + '/qa';
423- if (bindScope) {
424- callbacks.success = Y.bind(callbacks.success, bindScope);
425- callbacks.failure = Y.bind(callbacks.failure, bindScope);
426- }
427- this._makeRequest(endpoint, callbacks);
428- },
429-
430- /**
431- * Given a result list, append metadata is appended to the charm or bundle
432- * as the 'metadata' attribute and convert to objects.
433- *
434- * @method transformResults
435- * @param {Object} JSON decoded data from response.
436- * @return {Array} List of charm and bundle objects.
437- *
438- */
439- transformResults: function(data) {
440- // Append the metadata to the actual token object.
441- var token;
442- var tokens;
443- tokens = Y.Array.map(data, function(entity) {
444- if (Y.Lang.isValue(entity.charm)) {
445- token = new Y.juju.models.Charm(entity.charm);
446- } else {
447- token = new Y.juju.models.Bundle(entity.bundle);
448- }
449- if (entity.metadata) {
450- token.set('metadata', entity.metadata);
451- }
452- return token;
453- });
454- return tokens;
455- },
456-
457- /**
458- * Initialize the API helper. Constructs a reusable datasource for all
459- * calls.
460- *
461- * @method initializer
462- * @param {Object} cfg configuration object.
463- *
464- */
465- initializer: function(cfg) {
466- // XXX This isn't set on initial load so we have to manually hit the
467- // setter to get datasource filled in. Must be a better way.
468- this.set('apiHost', cfg.apiHost);
469- },
470-
471- /**
472- * Fetch the interesting landing content from the charmworld API.
473- *
474- * @method interesting
475- * @return {Object} data loaded from the API call.
476- *
477- */
478- interesting: function(callbacks, bindScope) {
479- if (bindScope) {
480- callbacks.success = Y.bind(callbacks.success, bindScope);
481- callbacks.failure = Y.bind(callbacks.failure, bindScope);
482- }
483-
484- this._makeRequest('search/interesting', callbacks);
485- },
486-
487- /**
488- Fetch the related charm info from the charmworld API.
489-
490- @method related
491- @param {String} charmID The charm to find related charms for.
492- @param {Object} callbacks The success/failure callbacks to use.
493- @param {Object} bindscope An object scope to perform callbacks in.
494- @return {Object} data loaded from the API call.
495-
496- */
497- related: function(charmID, callbacks, bindScope) {
498- var endpoint = 'charm/' + charmID + '/related';
499- if (bindScope) {
500- callbacks.success = Y.bind(callbacks.success, bindScope);
501- callbacks.failure = Y.bind(callbacks.failure, bindScope);
502- }
503- this._makeRequest(endpoint, callbacks);
504- }
505- }, {
506- ATTRS: {
507- /**
508- * Required attribute for the host to talk to for API calls.
509- *
510- * @attribute apiHost
511- * @default undefined
512- * @type {String}
513- *
514- */
515- apiHost: {
516- required: true,
517- setter: function(val) {
518- if (val && !val.match(/\/$/)) {
519- val = val + '/';
520- }
521- // Make sure we update the datasource if our apiHost changes.
522- var source = val + this._apiRoot + '/';
523- this.set('datasource', new Y.DataSource.IO({ source: source }));
524- return val;
525- }
526- },
527-
528- /**
529- * Auto constructed datasource object based on the apiHost attribute.
530- * @attribute datasource
531- * @type {Datasource}
532- *
533- */
534- datasource: {
535- setter: function(datasource) {
536- // Construct an API helper using the new datasource.
537- this.apiHelper = new ns.ApiHelper({
538- sendRequest: Y.bind(datasource.sendRequest, datasource)
539- });
540- return datasource;
541- }
542- },
543-
544- /**
545- If there's no config we end up setting noop on the store so that tests
546- that don't need to worry about the browser can safely ignore it.
547-
548- We do log a console error, so those will occur on these tests to help
549- make it easy to catch an issue when you don't mean to noop the store.
550-
551- @attribute noop
552- @default false
553- @type {Boolean}
554-
555- */
556- noop: {
557- value: false
558- }
559- }
560- });
561-
562- /**
563- * Charmworld API version 2 interface.
564- *
565- * @class APIv2
566- * @extends {APIv3}
567- *
568- * This class inherits from the v3 version of the API so that removing v2
569- * once it is no longer needed will be easy (just delete this class, the one
570- * or two places it is referenced in the code, and its associated tests).
571- *
572- */
573- ns.APIv2 = Y.Base.create('APIv2', ns.APIv3, [], {
574- _apiRoot: 'api/2',
575-
576- /**
577- * API call to fetch autocomplete suggestions based on the current term.
578- *
579- * @method autocomplete
580- * @param {Object} query the filters data object for search.
581- * @param {Object} filters the filters data object for search.
582- * @param {Object} callbacks the success/failure callbacks to use.
583- * @param {Object} bindScope the scope of *this* in the callbacks.
584- */
585- autocomplete: function(filters, callbacks, bindScope) {
586- var endpoint = 'charms';
587- // Force that this is an autocomplete call to perform matching on the
588- // start of names vs a fulltext search.
589- filters.autocomplete = 'true';
590- filters.limit = 5;
591- if (bindScope) {
592- callbacks.success = Y.bind(callbacks.success, bindScope);
593- callbacks.failure = Y.bind(callbacks.failure, bindScope);
594- }
595- this._makeRequest(endpoint, callbacks, filters);
596- },
597-
598- /**
599- * API call to fetch a charm's details, with an optional local cache.
600- *
601- * @method charmWithCache
602- * @param {String} charmID The charm to fetch This is the fully qualified
603- * charm name in the format scheme:series/charm-revision.
604- * @param {Object} callbacks The success/failure callbacks to use.
605- * @param {Object} bindScope The scope of "this" in the callbacks.
606- * @param {ModelList} [cache] a local cache of browser charms.
607- * @param {String} [defaultSeries='precise'] The series to use if none is
608- * specified in the charm ID.
609- */
610- charm: function(charmID, callbacks, bindScope, cache, defaultSeries) {
611- if (bindScope) {
612- callbacks.success = Y.bind(callbacks.success, bindScope);
613- }
614- if (cache) {
615- var charm = cache.getById(charmID);
616- if (charm) {
617- // If the charm was found in the cache, then we can declare success
618- // without ever making a request to charmworld.
619- Y.soon(function() {
620- // Since there wasn't really a request, there is no data, so we
621- // pass an empty object as the "data" parameter.
622- callbacks.success({}, charm);
623- });
624- return;
625- } else {
626- var successCB = callbacks.success;
627- callbacks.success = function(data) {
628- var charm = new Y.juju.models.Charm(data.charm);
629- if (data.metadata) {
630- charm.set('metadata', data.metadata);
631- }
632- cache.add(charm);
633- successCB(data, charm);
634- };
635- }
636- }
637- charmID = this.apiHelper.normalizeCharmId(charmID, defaultSeries);
638- // If the charm ID does not have a revision number (or "HEAD"), add one.
639- if (/\-(\d+|HEAD)/.exec(charmID) === null) {
640- // Add in a revision placeholder. Any value will do, v2 of the
641- // charmworld API ignores revision numbers.
642- charmID = charmID + '-1';
643- }
644- this._charm(charmID, callbacks, bindScope);
645- },
646-
647- /**
648- Promises to return the latest charm ID for a given charm if a newer one
649- exists; this also caches the newer charm if one is available.
650-
651- @method promiseUpgradeAvailability
652- @param {Charm} charm An existing charm potentially in need of an upgrade.
653- @param {ModelList} cache A local cache of browser charms.
654- @return {Promise} A promise for a newer charm ID or undefined.
655- */
656- promiseUpgradeAvailability: function(charm, cache) {
657- // Get the charm's store ID, then replace the version number
658- // with '-HEAD' to retrieve the latest version of the charm.
659- var storeId, revision;
660- if (charm instanceof Y.Model) {
661- storeId = charm.get('storeId');
662- revision = parseInt(charm.get('revision'), 10);
663- } else {
664- storeId = charm.url;
665- revision = parseInt(charm.revision, 10);
666- }
667- storeId = storeId.replace(/-\d+$/, '-HEAD');
668- // XXX By using a cache we hide charm versions that have become available
669- // since we last requested the most recent version.
670- return this.promiseCharm(storeId, cache)
671- .then(function(latest) {
672- var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10);
673- if (latestVersion > revision) {
674- return latest.charm.id;
675- }
676- }, function(e) {
677- throw e;
678- });
679- },
680-
681- /**
682- * API call to search charms
683- *
684- * @method search
685- * @param {Object} filters the filters data object for search.
686- * @param {Object} callbacks the success/failure callbacks to use.
687- * @param {Object} bindScope the scope of *this* in the callbacks.
688- */
689- search: function(filters, callbacks, bindScope) {
690- var endpoint = 'charms';
691- if (bindScope) {
692- callbacks.success = Y.bind(callbacks.success, bindScope);
693- callbacks.failure = Y.bind(callbacks.failure, bindScope);
694- }
695- this._makeRequest(endpoint, callbacks, filters);
696- },
697-
698- /**
699- Generate the API path to a charm icon.
700- This is useful when generating links and references in HTML to the
701- charm's icon and is constructing the correct icon based on reviewed
702- status and categories on the charm.
703-
704- @method iconpath
705- @param {String} charmID The id of the charm to grab the icon for.
706- @return {String} The URL of the charm's icon.
707- */
708- iconpath: function(charmID) {
709- // If this is a local charm, then we need use a hard coded path to the
710- // default icon since we cannot fetch its category data or its own
711- // icon.
712- // XXX: #1202703 - this is a short term fix for the bug. Need longer
713- // term solution.
714- if (charmID.indexOf('local:') === 0) {
715- return this.get('apiHost') +
716- 'static/img/charm_160.svg';
717-
718- } else {
719- // Get the charm ID from the service. In some cases, this will be
720- // the charm URL with a protocol, which will need to be removed.
721- // The following regular expression removes everything up to the
722- // colon portion of the quote and leaves behind a charm ID.
723- charmID = charmID.replace(/^[^:]+:/, '');
724- return this.get('apiHost') + [
725- this._apiRoot,
726- 'charm',
727- charmID,
728- 'icon.svg'].join('/');
729- }
730- },
731-
732- /**
733- * Given a result list, turn that into an array of charm objects for the
734- * application to use. Metadata is appended to the charm or bundle as the
735- * 'metadata' attribute.
736- *
737- * @method transformResults
738- * @param {Object} JSON decoded data from response.
739- * @return {Array} List of charm objects.
740- *
741- */
742- transformResults: function(data) {
743- // Remove non-charms (bundles) from the data.
744- data = Y.Array.filter(data, function(charmData) {
745- return Y.Lang.isValue(charmData.charm);
746- });
747- // Append the metadata to the actual charm object.
748- data = Y.Array.map(data, function(charmData) {
749- var charm = new Y.juju.models.Charm(charmData.charm);
750- if (charmData.metadata) {
751- charm.set('metadata', charmData.metadata);
752- }
753- return charm;
754- });
755- return data;
756- },
757-
758- /**
759- * Fetch the interesting landing content from the charmworld API.
760- *
761- * @method interesting
762- * @return {Object} data loaded from the API call.
763- *
764- */
765- interesting: function(callbacks, bindScope) {
766- if (bindScope) {
767- callbacks.success = Y.bind(callbacks.success, bindScope);
768- callbacks.failure = Y.bind(callbacks.failure, bindScope);
769- }
770-
771- this._makeRequest('charms/interesting', callbacks);
772- }
773- }, {
774- ATTRS: {}
775- });
776-
777-}, '0.1.0', {
778- requires: [
779- 'datasource-io',
780- 'json-parse',
781- 'juju-charm-models',
782- 'juju-bundle-models',
783- 'promise',
784- 'querystring-stringify'
785- ]
786-});
787
788=== added file 'app/store/charmworld.js'
789--- app/store/charmworld.js 1970-01-01 00:00:00 +0000
790+++ app/store/charmworld.js 2013-09-30 21:13:03 +0000
791@@ -0,0 +1,798 @@
792+/*
793+This file is part of the Juju GUI, which lets users view and manage Juju
794+environments within a graphical interface (https://launchpad.net/juju-gui).
795+Copyright (C) 2012-2013 Canonical Ltd.
796+
797+This program is free software: you can redistribute it and/or modify it under
798+the terms of the GNU Affero General Public License version 3, as published by
799+the Free Software Foundation.
800+
801+This program is distributed in the hope that it will be useful, but WITHOUT
802+ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
803+SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
804+General Public License for more details.
805+
806+You should have received a copy of the GNU Affero General Public License along
807+with this program. If not, see <http://www.gnu.org/licenses/>.
808+*/
809+
810+'use strict';
811+
812+/**
813+ * Provide the Charmworld API datastore class.
814+ *
815+ * @module store
816+ * @submodule store.charm
817+ */
818+
819+YUI.add('juju-charm-store', function(Y) {
820+ var juju = Y.namespace('juju'),
821+ ns = Y.namespace('juju.charmworld'),
822+ models = Y.namespace('juju.models');
823+
824+ // Make Y.juju.charmworld reference the "juju.charmworld" namespace.
825+ juju.charmworld = ns;
826+
827+ ns.ApiHelper = Y.Base.create('ApiHelper', Y.Base, [], {
828+
829+ /**
830+ Initializer for the class.
831+
832+ @method initializer
833+ @param {Object} cfg The configuration for the API interface.
834+ @return {undefined} Nothing.
835+ */
836+ initializer: function(cfg) {
837+ this.sendRequest = cfg.sendRequest;
838+ },
839+
840+ /**
841+ Send a request and handle the response from the API.
842+
843+ @method makeRequest
844+ @param {Object} args any query params and arguments required.
845+ @return {undefined} Nothing.
846+ */
847+ makeRequest: function(apiEndpoint, callbacks, args) {
848+ // Any query string args need to be put onto the endpoint for calling.
849+ if (args) {
850+ apiEndpoint = apiEndpoint + '?' + Y.QueryString.stringify(args);
851+ }
852+
853+ this.sendRequest({
854+ request: apiEndpoint,
855+ callback: {
856+ 'success': function(io_request) {
857+ var res = Y.JSON.parse(
858+ io_request.response.results[0].responseText
859+ );
860+ callbacks.success(res);
861+ },
862+
863+ 'failure': function(io_request) {
864+ var respText = io_request.response.results[0].responseText,
865+ res;
866+ if (respText) {
867+ res = Y.JSON.parse(respText);
868+ }
869+ callbacks.failure(res, io_request);
870+ }
871+ }
872+ });
873+ },
874+
875+ /**
876+ Normalize a charm name so we can request its full data. Charm lookup
877+ requires a very specific form of the charm identifier.
878+
879+ series/charm-revision
880+
881+ where revision can currently (API v2) be any numeric placeholder.
882+
883+ @method normalizeCharmId
884+ @param {String} charmId to normalize.
885+ @param {String} [defaultSeries='precise'] The series to use if none is
886+ specified in the charm ID.
887+ @return {String} normalized id.
888+ */
889+ normalizeCharmId: function(charmId, defaultSeries) {
890+ var result = charmId;
891+ if (/^(cs:|local:)/.exec(result)) {
892+ result = result.slice(result.indexOf(':') + 1);
893+ }
894+
895+ if (result.indexOf('/') === -1) {
896+ if (!defaultSeries) {
897+ console.warn('No default series provided when normalizing charm ' +
898+ 'ID. Using "precise".');
899+ defaultSeries = 'precise';
900+ }
901+ result = defaultSeries + '/' + result;
902+ }
903+ return result;
904+ }
905+
906+ }, {
907+ ATTRS: {
908+ }
909+ });
910+
911+ /**
912+ * Charmworld API version 3 interface.
913+ *
914+ * @class APIv3
915+ * @extends {Base}
916+ *
917+ */
918+ ns.APIv3 = Y.Base.create('APIv3', Y.Base, [], {
919+ _apiRoot: 'api/3',
920+
921+ /**
922+ * Send the actual request and handle response from the API.
923+ *
924+ * @method _makeRequest
925+ * @param {Object} args any query params and arguments required.
926+ * @private
927+ *
928+ */
929+ _makeRequest: function(apiEndpoint, callbacks, args) {
930+ // If we're in the noop state, just call the error callback.
931+ if (this.get('noop')) {
932+ callbacks.failure('noop failure');
933+ return;
934+ }
935+ // Delegate the request making to the helper object.
936+ this.apiHelper.makeRequest(apiEndpoint, callbacks, args);
937+ },
938+
939+ /**
940+ * API call to fetch autocomplete suggestions based on the current term.
941+ *
942+ * @method autocomplete
943+ * @param {Object} query the filters data object for search.
944+ * @param {Object} filters the filters data object for search.
945+ * @param {Object} callbacks the success/failure callbacks to use.
946+ * @param {Object} bindScope the scope of *this* in the callbacks.
947+ */
948+ autocomplete: function(filters, callbacks, bindScope) {
949+ var endpoint = 'search';
950+ // Force that this is an autocomplete call to perform matching on the
951+ // start of names vs a fulltext search.
952+ filters.autocomplete = 'true';
953+ filters.limit = 5;
954+ if (bindScope) {
955+ callbacks.success = Y.bind(callbacks.success, bindScope);
956+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
957+ }
958+ this._makeRequest(endpoint, callbacks, filters);
959+ },
960+
961+
962+ /**
963+ * API call to fetch a charm's details.
964+ *
965+ * @method charm
966+ * @param {String} charmID the charm to fetch.
967+ * @param {Object} callbacks the success/failure callbacks to use.
968+ * @param {Object} bindScope the scope of *this* in the callbacks.
969+ *
970+ */
971+ _charm: function(charmID, callbacks, bindScope) {
972+ var endpoint = 'charm/' + charmID;
973+ if (bindScope) {
974+ callbacks.success = Y.bind(callbacks.success, bindScope);
975+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
976+ }
977+
978+ this._makeRequest(endpoint, callbacks);
979+ },
980+
981+ /**
982+ * API call to fetch a charm's details, with an optional local cache.
983+ *
984+ * @method charm
985+ * @param {String} charmID The charm to fetch This is the fully qualified
986+ * charm name in the format scheme:series/charm-revision.
987+ * @param {Object} callbacks The success/failure callbacks to use.
988+ * @param {Object} bindScope The scope of "this" in the callbacks.
989+ * @param {ModelList} [cache] a local cache of browser charms.
990+ * @param {String} [defaultSeries='precise'] The series to use if none is
991+ * specified in the charm ID.
992+ */
993+ charm: function(charmID, callbacks, bindScope, cache, defaultSeries) {
994+ if (bindScope) {
995+ callbacks.success = Y.bind(callbacks.success, bindScope);
996+ }
997+ if (cache) {
998+ var charm = cache.getById(charmID);
999+ if (charm) {
1000+ // If the charm was found in the cache, then we can declare success
1001+ // without ever making a request to charmworld.
1002+ Y.soon(function() {
1003+ // Since there wasn't really a request, there is no data, so we
1004+ // pass an empty object as the "data" parameter.
1005+ callbacks.success({}, charm);
1006+ });
1007+ return;
1008+ } else {
1009+ var successCB = callbacks.success;
1010+ callbacks.success = function(data) {
1011+ var charm = new Y.juju.models.Charm(data.charm);
1012+ if (data.metadata) {
1013+ charm.set('metadata', data.metadata);
1014+ }
1015+ cache.add(charm);
1016+ successCB(data, charm);
1017+ };
1018+ }
1019+ }
1020+ this._charm(this.apiHelper.normalizeCharmId(charmID, defaultSeries),
1021+ callbacks, bindScope);
1022+ },
1023+
1024+ /**
1025+ Public method to make an API call to fetch a bundle from Charmworld.
1026+
1027+ @method bundle
1028+ @param {String} bundleID The bundle to fetch This is the fully qualified
1029+ bundle id.
1030+ @param {Object} callbacks The success/failure callbacks to use.
1031+ @param {Object} bindScope The scope of "this" in the callbacks.
1032+ */
1033+ bundle: function(bundleID, callbacks, bindScope) {
1034+ this._bundle(bundleID, callbacks, bindScope);
1035+ },
1036+
1037+ /**
1038+ API call to fetch a charm's details.
1039+
1040+ @method _bundle
1041+ @param {String} bundleID the bundle to fetch.
1042+ @param {Object} callbacks the success/failure callbacks to use.
1043+ @param {Object} bindScope the scope of *this* in the callbacks.
1044+ */
1045+ _bundle: function(bundleID, callbacks, bindScope) {
1046+ if (bindScope) {
1047+ callbacks.success = Y.bind(callbacks.success, bindScope);
1048+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1049+ }
1050+ this._makeRequest(bundleID, callbacks);
1051+ },
1052+
1053+ /**
1054+ Like the "charm" method but returning a Promise.
1055+
1056+ @method promiseCharm
1057+ @param {String} charmId The ID of the charm to fetch.
1058+ @param {ModelList} cache A local cache of browser charms.
1059+ @param {String} [defaultSeries='precise'] The series to use if none is
1060+ specified in the charm ID.
1061+ @return {Promise} Returns a promise. Triggered with the result of calling
1062+ this.charm.
1063+ */
1064+ promiseCharm: function(charmId, cache, defaultSeries) {
1065+ var self = this;
1066+ return Y.Promise(function(resolve, reject) {
1067+ self.charm(charmId, { 'success': resolve, 'failure': reject },
1068+ self, cache, defaultSeries);
1069+ });
1070+ },
1071+
1072+ /**
1073+ Promises to return the latest charm ID for a given charm if a newer one
1074+ exists; this also caches the newer charm if one is available.
1075+
1076+ @method promiseUpgradeAvailability
1077+ @param {Charm} charm An existing charm potentially in need of an upgrade.
1078+ @param {ModelList} cache A local cache of browser charms.
1079+ @return {Promise} A promise for a newer charm ID or undefined.
1080+ */
1081+ promiseUpgradeAvailability: function(charm, cache) {
1082+ // Get the charm's store ID, then remove the version number to retrieve
1083+ // the latest version of the charm.
1084+ var storeId, revision;
1085+ if (charm instanceof Y.Model) {
1086+ storeId = charm.get('storeId');
1087+ revision = parseInt(charm.get('revision'), 10);
1088+ } else {
1089+ storeId = charm.url;
1090+ revision = parseInt(charm.revision, 10);
1091+ }
1092+ storeId = storeId.replace(/-\d+$/, '');
1093+ // XXX By using a cache we hide charm versions that have become available
1094+ // since we last requested the most recent version.
1095+ return this.promiseCharm(storeId, cache)
1096+ .then(function(latest) {
1097+ var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10);
1098+ if (latestVersion > revision) {
1099+ return latest.charm.id;
1100+ }
1101+ }, function(e) {
1102+ throw e;
1103+ });
1104+ },
1105+
1106+ /**
1107+ * API call to search charms
1108+ *
1109+ * @method search
1110+ * @param {Object} filters the filters data object for search.
1111+ * @param {Object} callbacks the success/failure callbacks to use.
1112+ * @param {Object} bindScope the scope of *this* in the callbacks.
1113+ */
1114+ search: function(filters, callbacks, bindScope) {
1115+ var endpoint = 'search';
1116+ if (bindScope) {
1117+ callbacks.success = Y.bind(callbacks.success, bindScope);
1118+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1119+ }
1120+ this._makeRequest(endpoint, callbacks, filters);
1121+ },
1122+
1123+ /**
1124+ * Fetch the contents of a charm's file.
1125+ *
1126+ * @method file
1127+ * @param {String} charmID The id of the charm's file we want.
1128+ * @param {String} filename The path/name of the file to fetch content.
1129+ * @param {Object} callbacks The success/failure callbacks.
1130+ * @param {Object} bindScope The scope for this in the callbacks.
1131+ *
1132+ */
1133+ file: function(charmID, filename, callbacks, bindScope) {
1134+ // If we're in the noop state, just call the error callback.
1135+ if (this.get('noop')) {
1136+ callbacks.failure('noop failure');
1137+ return;
1138+ }
1139+
1140+ var endpoint = 'charm/' + charmID + '/file/' + filename;
1141+ if (bindScope) {
1142+ callbacks.success = Y.bind(callbacks.success, bindScope);
1143+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1144+ }
1145+
1146+ this.get('datasource').sendRequest({
1147+ request: endpoint,
1148+ callback: {
1149+ 'success': function(io_request) {
1150+ callbacks.success(io_request.response.results[0].responseText);
1151+ },
1152+ 'failure': function(io_request) {
1153+ var respText = io_request.response.results[0].responseText,
1154+ res;
1155+ if (respText) {
1156+ res = Y.JSON.parse(respText);
1157+ }
1158+ callbacks.failure(res, io_request);
1159+ }
1160+ }
1161+ });
1162+ },
1163+
1164+ /**
1165+ Generate the API path to a charm icon.
1166+ This is useful when generating links and references in HTML to the
1167+ charm's icon and is constructing the correct icon based on reviewed
1168+ status and categories on the charm.
1169+
1170+ @method iconpath
1171+ @param {String} charmID The id of the charm to grab the icon for.
1172+ @return {String} The URL of the charm's icon.
1173+ */
1174+ iconpath: function(charmID) {
1175+ // If this is a local charm, then we need use a hard coded path to the
1176+ // default icon since we cannot fetch its category data or its own
1177+ // icon.
1178+ // XXX: #1202703 - this is a short term fix for the bug. Need longer
1179+ // term solution.
1180+ if (charmID.indexOf('local:') === 0) {
1181+ return this.get('apiHost') +
1182+ 'static/img/charm_160.svg';
1183+
1184+ } else {
1185+ // Get the charm ID from the service. In some cases, this will be
1186+ // the charm URL with a protocol, which will need to be removed.
1187+ // The following regular expression removes everything up to the
1188+ // colon portion of the quote and leaves behind a charm ID.
1189+ charmID = charmID.replace(/^[^:]+:/, '');
1190+ return this.get('apiHost') + [
1191+ this._apiRoot,
1192+ 'charm',
1193+ charmID,
1194+ 'file',
1195+ 'icon.svg'].join('/');
1196+ }
1197+ },
1198+
1199+ /**
1200+ * Generate the url to an icon for the category specified.
1201+ *
1202+ * @method buildCategoryIconPath
1203+ * @param {String} categoryID the id of the category to load an icon for.
1204+ *
1205+ */
1206+ buildCategoryIconPath: function(categoryID) {
1207+ return [
1208+ this.get('apiHost'),
1209+ 'static/img/category-',
1210+ categoryID,
1211+ '-bw.svg'
1212+ ].join('');
1213+ },
1214+
1215+ /**
1216+ * Load the QA data for a specific charm.
1217+ *
1218+ * @method qa
1219+ * @param {String} charmID The charm to fetch QA data for.
1220+ * @param {Object} callbacks The success/failure callbacks to use.
1221+ * @param {Object} bindScope The scope for 'this' in the callbacks.
1222+ *
1223+ */
1224+ qa: function(charmID, callbacks, bindScope) {
1225+ var endpoint = 'charm/' + charmID + '/qa';
1226+ if (bindScope) {
1227+ callbacks.success = Y.bind(callbacks.success, bindScope);
1228+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1229+ }
1230+ this._makeRequest(endpoint, callbacks);
1231+ },
1232+
1233+ /**
1234+ * Given a result list, append metadata is appended to the charm or bundle
1235+ * as the 'metadata' attribute and convert to objects.
1236+ *
1237+ * @method transformResults
1238+ * @param {Object} JSON decoded data from response.
1239+ * @return {Array} List of charm and bundle objects.
1240+ *
1241+ */
1242+ transformResults: function(data) {
1243+ // Append the metadata to the actual token object.
1244+ var token;
1245+ var tokens;
1246+ tokens = Y.Array.map(data, function(entity) {
1247+ if (Y.Lang.isValue(entity.charm)) {
1248+ token = new Y.juju.models.Charm(entity.charm);
1249+ } else {
1250+ token = new Y.juju.models.Bundle(entity.bundle);
1251+ }
1252+ if (entity.metadata) {
1253+ token.set('metadata', entity.metadata);
1254+ }
1255+ return token;
1256+ });
1257+ return tokens;
1258+ },
1259+
1260+ /**
1261+ * Initialize the API helper. Constructs a reusable datasource for all
1262+ * calls.
1263+ *
1264+ * @method initializer
1265+ * @param {Object} cfg configuration object.
1266+ *
1267+ */
1268+ initializer: function(cfg) {
1269+ // XXX This isn't set on initial load so we have to manually hit the
1270+ // setter to get datasource filled in. Must be a better way.
1271+ this.set('apiHost', cfg.apiHost);
1272+ },
1273+
1274+ /**
1275+ * Fetch the interesting landing content from the charmworld API.
1276+ *
1277+ * @method interesting
1278+ * @return {Object} data loaded from the API call.
1279+ *
1280+ */
1281+ interesting: function(callbacks, bindScope) {
1282+ if (bindScope) {
1283+ callbacks.success = Y.bind(callbacks.success, bindScope);
1284+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1285+ }
1286+
1287+ this._makeRequest('search/interesting', callbacks);
1288+ },
1289+
1290+ /**
1291+ Fetch the related charm info from the charmworld API.
1292+
1293+ @method related
1294+ @param {String} charmID The charm to find related charms for.
1295+ @param {Object} callbacks The success/failure callbacks to use.
1296+ @param {Object} bindscope An object scope to perform callbacks in.
1297+ @return {Object} data loaded from the API call.
1298+
1299+ */
1300+ related: function(charmID, callbacks, bindScope) {
1301+ var endpoint = 'charm/' + charmID + '/related';
1302+ if (bindScope) {
1303+ callbacks.success = Y.bind(callbacks.success, bindScope);
1304+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1305+ }
1306+ this._makeRequest(endpoint, callbacks);
1307+ }
1308+ }, {
1309+ ATTRS: {
1310+ /**
1311+ * Required attribute for the host to talk to for API calls.
1312+ *
1313+ * @attribute apiHost
1314+ * @default undefined
1315+ * @type {String}
1316+ *
1317+ */
1318+ apiHost: {
1319+ required: true,
1320+ 'setter': function(val) {
1321+ if (val && !val.match(/\/$/)) {
1322+ val = val + '/';
1323+ }
1324+ // Make sure we update the datasource if our apiHost changes.
1325+ var source = val + this._apiRoot + '/';
1326+ this.set('datasource', new Y.DataSource.IO({ source: source }));
1327+ return val;
1328+ }
1329+ },
1330+
1331+ /**
1332+ * Auto constructed datasource object based on the apiHost attribute.
1333+ * @attribute datasource
1334+ * @type {Datasource}
1335+ *
1336+ */
1337+ datasource: {
1338+ 'setter': function(datasource) {
1339+ // Construct an API helper using the new datasource.
1340+ this.apiHelper = new ns.ApiHelper({
1341+ sendRequest: Y.bind(datasource.sendRequest, datasource)
1342+ });
1343+ return datasource;
1344+ }
1345+ },
1346+
1347+ /**
1348+ If there's no config we end up setting noop on the store so that tests
1349+ that don't need to worry about the browser can safely ignore it.
1350+
1351+ We do log a console error, so those will occur on these tests to help
1352+ make it easy to catch an issue when you don't mean to noop the store.
1353+
1354+ @attribute noop
1355+ @default false
1356+ @type {Boolean}
1357+
1358+ */
1359+ noop: {
1360+ value: false
1361+ }
1362+ }
1363+ });
1364+
1365+ /**
1366+ * Charmworld API version 2 interface.
1367+ *
1368+ * @class APIv2
1369+ * @extends {APIv3}
1370+ *
1371+ * This class inherits from the v3 version of the API so that removing v2
1372+ * once it is no longer needed will be easy (just delete this class, the one
1373+ * or two places it is referenced in the code, and its associated tests).
1374+ *
1375+ */
1376+ ns.APIv2 = Y.Base.create('APIv2', ns.APIv3, [], {
1377+ _apiRoot: 'api/2',
1378+
1379+ /**
1380+ * API call to fetch autocomplete suggestions based on the current term.
1381+ *
1382+ * @method autocomplete
1383+ * @param {Object} query the filters data object for search.
1384+ * @param {Object} filters the filters data object for search.
1385+ * @param {Object} callbacks the success/failure callbacks to use.
1386+ * @param {Object} bindScope the scope of *this* in the callbacks.
1387+ */
1388+ autocomplete: function(filters, callbacks, bindScope) {
1389+ var endpoint = 'charms';
1390+ // Force that this is an autocomplete call to perform matching on the
1391+ // start of names vs a fulltext search.
1392+ filters.autocomplete = 'true';
1393+ filters.limit = 5;
1394+ if (bindScope) {
1395+ callbacks.success = Y.bind(callbacks.success, bindScope);
1396+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1397+ }
1398+ this._makeRequest(endpoint, callbacks, filters);
1399+ },
1400+
1401+ /**
1402+ * API call to fetch a charm's details, with an optional local cache.
1403+ *
1404+ * @method charmWithCache
1405+ * @param {String} charmID The charm to fetch This is the fully qualified
1406+ * charm name in the format scheme:series/charm-revision.
1407+ * @param {Object} callbacks The success/failure callbacks to use.
1408+ * @param {Object} bindScope The scope of "this" in the callbacks.
1409+ * @param {ModelList} [cache] a local cache of browser charms.
1410+ * @param {String} [defaultSeries='precise'] The series to use if none is
1411+ * specified in the charm ID.
1412+ */
1413+ charm: function(charmID, callbacks, bindScope, cache, defaultSeries) {
1414+ if (bindScope) {
1415+ callbacks.success = Y.bind(callbacks.success, bindScope);
1416+ }
1417+ if (cache) {
1418+ var charm = cache.getById(charmID);
1419+ if (charm) {
1420+ // If the charm was found in the cache, then we can declare success
1421+ // without ever making a request to charmworld.
1422+ Y.soon(function() {
1423+ // Since there wasn't really a request, there is no data, so we
1424+ // pass an empty object as the "data" parameter.
1425+ callbacks.success({}, charm);
1426+ });
1427+ return;
1428+ } else {
1429+ var successCB = callbacks.success;
1430+ callbacks.success = function(data) {
1431+ var charm = new Y.juju.models.Charm(data.charm);
1432+ if (data.metadata) {
1433+ charm.set('metadata', data.metadata);
1434+ }
1435+ cache.add(charm);
1436+ successCB(data, charm);
1437+ };
1438+ }
1439+ }
1440+ charmID = this.apiHelper.normalizeCharmId(charmID, defaultSeries);
1441+ // If the charm ID does not have a revision number (or "HEAD"), add one.
1442+ if (/\-(\d+|HEAD)/.exec(charmID) === null) {
1443+ // Add in a revision placeholder. Any value will do, v2 of the
1444+ // charmworld API ignores revision numbers.
1445+ charmID = charmID + '-1';
1446+ }
1447+ this._charm(charmID, callbacks, bindScope);
1448+ },
1449+
1450+ /**
1451+ Promises to return the latest charm ID for a given charm if a newer one
1452+ exists; this also caches the newer charm if one is available.
1453+
1454+ @method promiseUpgradeAvailability
1455+ @param {Charm} charm An existing charm potentially in need of an upgrade.
1456+ @param {ModelList} cache A local cache of browser charms.
1457+ @return {Promise} A promise for a newer charm ID or undefined.
1458+ */
1459+ promiseUpgradeAvailability: function(charm, cache) {
1460+ // Get the charm's store ID, then replace the version number
1461+ // with '-HEAD' to retrieve the latest version of the charm.
1462+ var storeId, revision;
1463+ if (charm instanceof Y.Model) {
1464+ storeId = charm.get('storeId');
1465+ revision = parseInt(charm.get('revision'), 10);
1466+ } else {
1467+ storeId = charm.url;
1468+ revision = parseInt(charm.revision, 10);
1469+ }
1470+ storeId = storeId.replace(/-\d+$/, '-HEAD');
1471+ // XXX By using a cache we hide charm versions that have become available
1472+ // since we last requested the most recent version.
1473+ return this.promiseCharm(storeId, cache)
1474+ .then(function(latest) {
1475+ var latestVersion = parseInt(latest.charm.id.split('-').pop(), 10);
1476+ if (latestVersion > revision) {
1477+ return latest.charm.id;
1478+ }
1479+ }, function(e) {
1480+ throw e;
1481+ });
1482+ },
1483+
1484+ /**
1485+ * API call to search charms
1486+ *
1487+ * @method search
1488+ * @param {Object} filters the filters data object for search.
1489+ * @param {Object} callbacks the success/failure callbacks to use.
1490+ * @param {Object} bindScope the scope of *this* in the callbacks.
1491+ */
1492+ search: function(filters, callbacks, bindScope) {
1493+ var endpoint = 'charms';
1494+ if (bindScope) {
1495+ callbacks.success = Y.bind(callbacks.success, bindScope);
1496+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1497+ }
1498+ this._makeRequest(endpoint, callbacks, filters);
1499+ },
1500+
1501+ /**
1502+ Generate the API path to a charm icon.
1503+ This is useful when generating links and references in HTML to the
1504+ charm's icon and is constructing the correct icon based on reviewed
1505+ status and categories on the charm.
1506+
1507+ @method iconpath
1508+ @param {String} charmID The id of the charm to grab the icon for.
1509+ @return {String} The URL of the charm's icon.
1510+ */
1511+ iconpath: function(charmID) {
1512+ // If this is a local charm, then we need use a hard coded path to the
1513+ // default icon since we cannot fetch its category data or its own
1514+ // icon.
1515+ // XXX: #1202703 - this is a short term fix for the bug. Need longer
1516+ // term solution.
1517+ if (charmID.indexOf('local:') === 0) {
1518+ return this.get('apiHost') +
1519+ 'static/img/charm_160.svg';
1520+
1521+ } else {
1522+ // Get the charm ID from the service. In some cases, this will be
1523+ // the charm URL with a protocol, which will need to be removed.
1524+ // The following regular expression removes everything up to the
1525+ // colon portion of the quote and leaves behind a charm ID.
1526+ charmID = charmID.replace(/^[^:]+:/, '');
1527+ return this.get('apiHost') + [
1528+ this._apiRoot,
1529+ 'charm',
1530+ charmID,
1531+ 'icon.svg'].join('/');
1532+ }
1533+ },
1534+
1535+ /**
1536+ * Given a result list, turn that into an array of charm objects for the
1537+ * application to use. Metadata is appended to the charm or bundle as the
1538+ * 'metadata' attribute.
1539+ *
1540+ * @method transformResults
1541+ * @param {Object} JSON decoded data from response.
1542+ * @return {Array} List of charm objects.
1543+ *
1544+ */
1545+ transformResults: function(data) {
1546+ // Remove non-charms (bundles) from the data.
1547+ data = Y.Array.filter(data, function(charmData) {
1548+ return Y.Lang.isValue(charmData.charm);
1549+ });
1550+ // Append the metadata to the actual charm object.
1551+ data = Y.Array.map(data, function(charmData) {
1552+ var charm = new Y.juju.models.Charm(charmData.charm);
1553+ if (charmData.metadata) {
1554+ charm.set('metadata', charmData.metadata);
1555+ }
1556+ return charm;
1557+ });
1558+ return data;
1559+ },
1560+
1561+ /**
1562+ * Fetch the interesting landing content from the charmworld API.
1563+ *
1564+ * @method interesting
1565+ * @return {Object} data loaded from the API call.
1566+ *
1567+ */
1568+ interesting: function(callbacks, bindScope) {
1569+ if (bindScope) {
1570+ callbacks.success = Y.bind(callbacks.success, bindScope);
1571+ callbacks.failure = Y.bind(callbacks.failure, bindScope);
1572+ }
1573+
1574+ this._makeRequest('charms/interesting', callbacks);
1575+ }
1576+ }, {
1577+ ATTRS: {}
1578+ });
1579+
1580+}, '0.1.0', {
1581+ requires: [
1582+ 'datasource-io',
1583+ 'json-parse',
1584+ 'juju-charm-models',
1585+ 'juju-bundle-models',
1586+ 'promise',
1587+ 'querystring-stringify'
1588+ ]
1589+});
1590
1591=== modified file 'app/subapps/browser/browser.js'
1592--- app/subapps/browser/browser.js 2013-09-17 13:27:43 +0000
1593+++ app/subapps/browser/browser.js 2013-09-30 21:13:03 +0000
1594@@ -921,8 +921,7 @@
1595 var idBits = req.path.replace(/^\//, '').replace(/\/$/, '').split('/'),
1596 id = null;
1597
1598- if ((idBits.length === 3 && idBits[0][0] === '~') || // new charms
1599- (idBits.length === 2)) { // reviewed charms
1600+ if (idBits.length > 1) {
1601 id = this._stripViewMode(req.path);
1602 }
1603 if (!id) {
1604
1605=== modified file 'app/subapps/browser/views/charm.js'
1606--- app/subapps/browser/views/charm.js 2013-09-24 15:34:52 +0000
1607+++ app/subapps/browser/views/charm.js 2013-09-30 21:13:03 +0000
1608@@ -39,7 +39,8 @@
1609 views.utils.apiFailingView
1610 ], {
1611
1612- template: views.Templates.browser_charm,
1613+ charmTemplate: views.Templates.browser_charm,
1614+ bundleTemplate: views.Templates.bundle,
1615 qatemplate: views.Templates.browser_qa,
1616
1617 /**
1618@@ -706,7 +707,7 @@
1619 tplData.provides = false;
1620 }
1621
1622- var tpl = this.template(tplData);
1623+ var tpl = this.charmTemplate(tplData);
1624 var tplNode = container.setHTML(tpl);
1625
1626 // Set the content then update the container so that it reload
1627@@ -752,35 +753,68 @@
1628 },
1629
1630 /**
1631+ Handles rendering the bundle view into the view's container and then
1632+ appending that container into the DOM.
1633+
1634+ @method _renderBundleView
1635+ @param {Y.Model} bundle the bundle model.
1636+ */
1637+ _renderBundleView: function(bundle) {
1638+ var template = this.bundleTemplate(bundle.getAttrs());
1639+ var container = this.get('container').setHTML(template);
1640+ this.get('renderTo').setHTML(container);
1641+ },
1642+
1643+ /**
1644 Render out the view to the DOM.
1645
1646- The View might be given either a charmID, which means go fetch the
1647- charm data, or a charm model instance, in which case the view has the
1648+ The View might be given either a charmID, or bundleID, which means go
1649+ fetch the data, or a charm model instance, in which case the view has the
1650 data it needs to render.
1651
1652 @method render
1653
1654 */
1655 render: function() {
1656- var isFullscreen = this.get('isFullscreen');
1657- this.showIndicator(this.get('renderTo'));
1658+ var isFullscreen = this.get('isFullscreen'),
1659+ renderTo = this.get('renderTo'),
1660+ charmID = this.get('charmID'),
1661+ charmworld = this.get('store');
1662+
1663+ this.showIndicator(renderTo);
1664
1665 if (this.get('charm')) {
1666 this._renderCharmView(this.get('charm'), isFullscreen);
1667- this.hideIndicator(this.get('renderTo'));
1668+ this.hideIndicator(renderTo);
1669+ } else if (this.get('bundle')) {
1670+ this._renderBundleView(this.get('bundle'), isFullscreen);
1671+ this.hideIndicator(renderTo);
1672 } else {
1673- this.get('store').charm(this.get('charmID'), {
1674- 'success': function(data) {
1675- var charm = new models.Charm(data.charm);
1676- if (data.metadata) {
1677- charm.set('metadata', data.metadata);
1678- }
1679- this.set('charm', charm);
1680- this._renderCharmView(this.get('charm'), isFullscreen);
1681- this.hideIndicator(this.get('renderTo'));
1682- },
1683- 'failure': this.apiFailure
1684- }, this);
1685+ if (charmID.indexOf('bundle') === -1) {
1686+ charmworld.charm(this.get('charmID'), {
1687+ 'success': function(data) {
1688+ var charm = new models.Charm(data.charm);
1689+ if (data.metadata) {
1690+ charm.set('metadata', data.metadata);
1691+ }
1692+ this.set('charm', charm);
1693+ this._renderCharmView(this.get('charm'), isFullscreen);
1694+ this.hideIndicator(renderTo);
1695+ },
1696+ 'failure': this.apiFailure
1697+ }, this);
1698+ } else {
1699+ charmworld.bundle(charmID, {
1700+ 'success': function(data) {
1701+ var bundle = new models.Bundle(data);
1702+ this.set('bundle', bundle);
1703+ this._renderBundleView(bundle);
1704+ this.hideIndicator(renderTo);
1705+ },
1706+ 'failure': this.apiFailure
1707+ }, this);
1708+ }
1709+
1710 }
1711 }
1712 }, {
1713
1714=== modified file 'app/templates/bundle.handlebars'
1715--- app/templates/bundle.handlebars 2013-09-26 03:50:03 +0000
1716+++ app/templates/bundle.handlebars 2013-09-30 21:13:03 +0000
1717@@ -4,7 +4,7 @@
1718 <div class="header">
1719 <a href="" class="back icon"></a>
1720 <div class="block-icon">
1721- <img src="{{charmIconPath storeId}}" alt="{{ name }} icon" class="icon">
1722+ <img src="{{storeId}}" alt="{{ name }} icon" class="icon">
1723 </div>
1724 <div class="details">
1725 <h1>{{ name }}</h1>
1726@@ -96,7 +96,7 @@
1727 <span class="date">
1728 {{prettyDate}}
1729 </span>
1730- <strong>{{author.name}}</strong>
1731+ <strong>{{author.name}}</strong>
1732 <span class="respect-whitespace">{{message}}</span> -
1733 <a href="{{ revnoLink }}">
1734 REVNO{{revno}}
1735
1736=== modified file 'undocumented'
1737--- undocumented 2013-09-30 17:47:40 +0000
1738+++ undocumented 2013-09-30 21:13:03 +0000
1739@@ -4,10 +4,6 @@
1740 app/app.js:214 "callback"
1741 app/app.js:225 "callback"
1742 app/app.js:239 "callback"
1743-app/store/charm.js:518 "setter"
1744-app/store/charm.js:65 "success"
1745-app/store/charm.js:500 "setter"
1746-app/store/charm.js:329 "success"
1747 app/store/notifications.js:167 "level"
1748 app/store/notifications.js:50 "title"
1749 app/store/notifications.js:156 "title"

Subscribers

People subscribed via source and target branches