Merge lp:~gary/juju-gui/simplifycharmstore into lp:juju-gui/experimental

Proposed by Gary Poster
Status: Merged
Merged at revision: 206
Proposed branch: lp:~gary/juju-gui/simplifycharmstore
Merge into: lp:juju-gui/experimental
Diff against target: 1329 lines (+365/-462)
14 files modified
app/app.js (+2/-1)
app/models/charm.js (+172/-259)
app/modules.js (+3/-1)
app/store/charm.js (+31/-51)
app/templates/charm-search-result.handlebars (+3/-6)
app/views/charm-search.js (+31/-15)
test/data/search_results.json (+16/-9)
test/data/series_search_results.json (+7/-2)
test/test_app.js (+1/-1)
test/test_charm_configuration.js (+2/-2)
test/test_charm_search.js (+4/-4)
test/test_charm_store.js (+17/-14)
test/test_model.js (+74/-95)
test/test_service_view.js (+2/-2)
To merge this branch: bzr merge lp:~gary/juju-gui/simplifycharmstore
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+131086@code.launchpad.net

Description of the change

change charm store data structures

This change is hopefully the last round of changes, at least for a long while, to the underlying charm store infrastructure. It is more deletes than additions, and changes the code to take advantage of the changes Kapil made to the charm store.

The sorting code is simplified yet again.

https://codereview.appspot.com/6733067/

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Reviewers: mp+131086_code.launchpad.net,

Message:
Please take a look.

Description:
Last change round of charm store data structures

This change is hopefully the last round of changes, at least for a long
while, to the underlying charm store infrastructure. It is more deletes
than additions, and changes the code to take advantage of the changes
Kapil made to the charm store.

I made some decisions as to how to factor some of these things and would
be happy to describe my rationale for changes if desired. I'm thinking
particularly of the immediate creation of charm objects as search
results. That allowed for some simplifications and I think is mostly a
win.

The sorting code is simplified yet again.

Thanks

Gary

https://code.launchpad.net/~gary/juju-gui/simplifycharmstore/+merge/131086

(do not edit description out of merge proposal)

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

Affected files:
   A [revision details]
   M app/app.js
   M app/models/charm.js
   M app/modules.js
   M app/store/charm.js
   M app/templates/charm-search-result.handlebars
   M app/views/charm-search.js
   M test/data/search_results.json
   M test/data/series_search_results.json
   M test/test_app.js
   M test/test_charm_configuration.js
   M test/test_charm_search.js
   M test/test_charm_store.js
   M test/test_model.js
   M test/test_service_view.js

Revision history for this message
Benjamin Saller (bcsaller) wrote :

Even though this is a large branch I think it removes more code than it
adds which is always a plus. I had some minor feedback but this LGTM.

I'd still rather Kapil get a chance to look this over but I think the
general architectural issues that were present before (and which I
didn't take into account either) are no longer present.

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js
File app/models/charm.js (right):

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js#newcode56
app/models/charm.js:56: this.on('load', function() { this.loaded = true;
});
Don't you need to either use self here or pass this as context to the
'on' call?

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js#newcode84
app/models/charm.js:84: options.get_charm(
Why the two naming styles on these_twoMethods? get_charm, loadByPath?

https://codereview.appspot.com/6733067/diff/1/app/templates/charm-search-result.handlebars
File app/templates/charm-search-result.handlebars (right):

https://codereview.appspot.com/6733067/diff/1/app/templates/charm-search-result.handlebars#newcode13
app/templates/charm-search-result.handlebars:13: {{#if
owner}}{{owner}}/{{/if}}{{package_name}}</a>
I thought you didn't need the #if when there is no content other than
the var which can default to null, no?

https://codereview.appspot.com/6733067/

Revision history for this message
Gary Poster (gary) wrote :

*** Submitted:

change charm store data structures

This change is hopefully the last round of changes, at least for a long
while, to the underlying charm store infrastructure. It is more deletes
than additions, and changes the code to take advantage of the changes
Kapil made to the charm store.

The sorting code is simplified yet again.

R=benjamin.saller
CC=
https://codereview.appspot.com/6733067

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js
File app/models/charm.js (right):

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js#newcode56
app/models/charm.js:56: this.on('load', function() { this.loaded = true;
});
On 2012/10/26 07:56:19, benjamin.saller wrote:
> Don't you need to either use self here or pass this as context to the
'on' call?
No, because the listener is on "this" so the context is what I want. I
already have a test that verifies, and I doublechecked in the chromium
debugger as well.

https://codereview.appspot.com/6733067/diff/1/app/models/charm.js#newcode84
app/models/charm.js:84: options.get_charm(
On 2012/10/26 07:56:19, benjamin.saller wrote:
> Why the two naming styles on these_twoMethods? get_charm, loadByPath?
As we discussed, it's because we haven't standardized one way or the
other across all our files. Within a file, we are consistent. This
uses get_charm, from the older env js, and loadByPath, from the newer
charm store js. I got your agreement in person that this is fine.
OTOH I switched charmIDRe and idElements in this file, based on your
reminder.

https://codereview.appspot.com/6733067/diff/1/app/templates/charm-search-result.handlebars
File app/templates/charm-search-result.handlebars (right):

https://codereview.appspot.com/6733067/diff/1/app/templates/charm-search-result.handlebars#newcode13
app/templates/charm-search-result.handlebars:13: {{#if
owner}}{{owner}}/{{/if}}{{package_name}}</a>
On 2012/10/26 07:56:19, benjamin.saller wrote:
> I thought you didn't need the #if when there is no content other than
the var
> which can default to null, no?
Yes, but I have the slash after the owner.

https://codereview.appspot.com/6733067/

Revision history for this message
Gary Poster (gary) wrote :

Thank you for the review, Ben. I have comments in-line, largely in line
with what we discussed in person.

You gave me permission to go ahead and land this. Kapil mentioned that
he questioned the use of the Model load/sync code given that it is of
minimal value to our use case, but if I need to address this I will do
it in a subsequent branch.

Thanks

Gary

https://codereview.appspot.com/6733067/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'app/app.js'
--- app/app.js 2012-10-19 01:44:45 +0000
+++ app/app.js 2012-10-23 20:09:21 +0000
@@ -261,7 +261,8 @@
261 show_unit: function(req) {261 show_unit: function(req) {
262 console.log(262 console.log(
263 'App: Route: Unit', req.params.id, req.path, req.pendingRoutes);263 'App: Route: Unit', req.params.id, req.path, req.pendingRoutes);
264 var unit_id = req.params.id.replace('-', '/');264 // This replacement honors service names that have a hyphen in them.
265 var unit_id = req.params.id.replace(/^(\S+)-(\d+)$/, '$1/$2');
265 var unit = this.db.units.getById(unit_id);266 var unit = this.db.units.getById(unit_id);
266 if (unit) {267 if (unit) {
267 // Once the unit is loaded we need to get the full details of the268 // Once the unit is loaded we need to get the full details of the
268269
=== modified file 'app/models/charm.js'
--- app/models/charm.js 2012-10-19 01:53:03 +0000
+++ app/models/charm.js 2012-10-23 20:09:21 +0000
@@ -2,67 +2,12 @@
22
3YUI.add('juju-charm-models', function(Y) {3YUI.add('juju-charm-models', function(Y) {
44
5
5 var models = Y.namespace('juju.models');6 var models = Y.namespace('juju.models');
67
7 // This is how the charm_id_re regex works for various inputs. The first8 // Charms, once instantiated and loaded with data from their respective
8 // element is always the initial string, which we have elided in the9 // sources, are immutable and read-only. This reflects the reality of how we
9 // examples.10 // interact with them.
10 // 'cs:~marcoceppi/precise/word-press-17' ->
11 // [..."cs", "marcoceppi", "precise", "word-press", "17"]
12 // 'cs:~marcoceppi/precise/word-press' ->
13 // [..."cs", "marcoceppi", "precise", "word-press", undefined]
14 // 'cs:precise/word-press' ->
15 // [..."cs", undefined, "precise", "word-press", undefined]
16 // 'cs:precise/word-press-17'
17 // [..."cs", undefined, "precise", "word-press", "17"]
18 var charm_id_re = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)(?:-(\d+))?$/,
19 parse_charm_id = function(id) {
20 var parts = charm_id_re.exec(id),
21 result = {};
22 if (parts) {
23 parts.shift();
24 Y.each(
25 Y.Array.zip(
26 ['scheme', 'owner', 'series', 'package_name', 'revision'],
27 parts),
28 function(pair) { result[pair[0]] = pair[1]; });
29 if (!Y.Lang.isValue(result.scheme)) {
30 result.scheme = 'cs'; // This is the default.
31 }
32 return result;
33 }
34 // return undefined;
35 },
36 _calculate_full_charm_name = function(elements) {
37 var tmp = [elements.series, elements.package_name];
38 if (elements.owner) {
39 tmp.unshift('~' + elements.owner);
40 }
41 return tmp.join('/');
42 },
43 _calculate_charm_store_path = function(elements) {
44 return [(elements.owner ? '~' + elements.owner : 'charms'),
45 elements.series, elements.package_name, 'json'].join('/');
46 },
47 _calculate_base_charm_id = function(elements) {
48 return elements.scheme + ':' + _calculate_full_charm_name(elements);
49 },
50 _reconsititute_charm_id = function(elements) {
51 return _calculate_base_charm_id(elements) + '-' + elements.revision;
52 },
53 _clean_charm_data = function(data) {
54 data.is_subordinate = data.subordinate;
55 Y.each(['subordinate', 'name', 'revision', 'store_revision'],
56 function(nm) { delete data[nm]; });
57 return data;
58 };
59 // This is exposed for testing purposes.
60 models.parse_charm_id = parse_charm_id;
61
62 // For simplicity and uniformity, there is a single Charm class and a
63 // single CharmList class. Charms, once instantiated and loaded with data
64 // from their respective sources, are immutable and read-only. This reflects
65 // the reality of how we interact with them.
6611
67 // Charm instances can represent both environment charms and charm store12 // Charm instances can represent both environment charms and charm store
68 // charms. A charm id is reliably and uniquely associated with a given13 // charms. A charm id is reliably and uniquely associated with a given
@@ -70,127 +15,155 @@
7015
71 // Therefore, the database keeps these charms separate in two different16 // Therefore, the database keeps these charms separate in two different
72 // CharmList instances. One is db.charms, representing the environment17 // CharmList instances. One is db.charms, representing the environment
73 // charms. The other is maintained by and within the persistent charm panel18 // charms. The other, from the charm store, is maintained by and within the
74 // instance. As you'd expect, environment charms are what to use when19 // persistent charm panel instance. As you'd expect, environment charms are
75 // viewing or manipulating the environment. Charm store charms are what we20 // what to use when viewing or manipulating the environment. Charm store
76 // can browse to select and deploy new charms to the environment.21 // charms are what we can browse to select and deploy new charms to the
22 // environment.
7723
78 // Environment charms begin their lives with full charm ids, as provided by24 // Charms begin their lives with full charm ids, as provided by
79 // services in the environment:25 // services in the environment and the charm store:
8026
81 // [SCHEME]:(~[OWNER]/)?[SERIES]/[PACKAGE NAME]-[REVISION].27 // [SCHEME]:(~[OWNER]/)?[SERIES]/[PACKAGE NAME]-[REVISION].
8228
83 // With an id, we can instantiate a charm: typically we use29 // With an id, we can instantiate a charm: typically we use
84 // "db.charms.add({id: [ID]})". Finally, we load the charm's data from the30 // "db.charms.add({id: [ID]})". Finally, we load the charm's data over the
85 // environment using the standard YUI Model method "load," providing an31 // network using the standard YUI Model method "load," providing an object
86 // object with a get_charm callable, and an optional callback (see YUI32 // with a get_charm callable, and an optional callback (see YUI docs). Both
87 // docs). The env has a get_charm method, so, by design, it works nicely:33 // the env and the charm store have a get_charm method, so, by design, it
88 // "charm.load(env, optionalCallback)". The get_charm method is expected to34 // works easily: "charm.load(env, optionalCallback)" or
89 // return what the env version does: either an object with a "result" object35 // "charm.load(charm_store, optionalCallback)". The get_charm method must
90 // containing the charm data, or an object with an "err" attribute.36 // either callback using the default YUI approach for this code, a boolean
9137 // indicating failure, and a result; or it must return what the env version
92 // The charms in the charm store have a significant difference, beyond the38 // does: an object with a "result" object containing the charm data, or an
93 // source of their data: they are addressed in the charm store by a path39 // object with an "err" attribute.
94 // that does not include the revision number, and charm store searches do
95 // not include revision numbers in the results. Therefore, we cannot
96 // immediately instantiate a charm, because it requires a full id in order
97 // to maintain the idea of an immutable charm associated with a unique charm
98 // id. However, the charm information that returns does have a revision
99 // number (the most recent); moreover, over time the charm may be updated,
100 // leading to a new charm revision. We model this by creating a new charm.
101
102 // Since we cannot create or search for charms without a revision number
103 // using the normal methods, the charm list has a couple of helpers for this
104 // story. The workhorse is "loadOneByBaseId". A "base id" is an id without
105 // a revision.
106
107 // The arguments to "loadOneById" are a base id and a hash of other options.
108 // The hash must have a "charm_store" attribute, that itself loadByPath
109 // method, like the one in app/store/charm.js. It may have zero or more of
110 // the following: a success callback, accepting the fully loaded charm with
111 // the newest revision for the given base id; a failure callback, accepting
112 // the Y.io response object after a failure; and a "force" attribute that,
113 // if it is a Javascript boolean truth-y value, forces a load even if a
114 // charm with the given id already is in the charm list.
115
116 // "getOneByBaseId" simply returns the charm with the highest revision and
117 // "the given base id from the charm list, without trying to load
118 // "information.
11940
120 // In both cases, environment charms and charm store charms, a charm's41 // In both cases, environment charms and charm store charms, a charm's
121 // "loaded" attribute is set to true once it has all the data from its42 // "loaded" attribute is set to true once it has all the data from its
122 // environment.43 // environment.
12344
124 var Charm = Y.Base.create('charm', Y.Model, [], {45 var charm_id_re = /^(?:(\w+):)?(?:~(\S+)\/)?(\w+)\/(\S+?)-(\d+)$/,
125 initializer: function() {46 id_elements = ['scheme', 'owner', 'series', 'package_name', 'revision'],
126 this.loaded = false;47 Charm = Y.Base.create('charm', Y.Model, [], {
127 this.on('load', function() { this.loaded = true; });48 initializer: function() {
128 },49 var id = this.get('id'),
129 sync: function(action, options, callback) {50 parts = id && charm_id_re.exec(id),
130 if (action !== 'read') {51 self = this;
131 throw (52 if (!Y.Lang.isValue(id) || !parts) {
132 'Only use the "read" action; "' + action + '" not supported.');53 throw 'Developers must initialize charms with a well-formed id.';
133 }54 }
134 if (!Y.Lang.isValue(options.get_charm)) {55 this.loaded = false;
135 throw 'You must supply a get_charm function.';56 this.on('load', function() { this.loaded = true; });
136 }57 parts.shift();
137 options.get_charm(58 Y.each(
138 this.get('id'),59 Y.Array.zip(id_elements, parts),
139 // This is the success callback, or the catch-all callback for60 function(pair) { self.set(pair[0], pair[1]); });
140 // get_charm.61 // full_name
141 function(response) {62 var tmp = [this.get('series'), this.get('package_name')],
142 // Handle the env.get_charm response specially, for ease of use. If63 owner = this.get('owner');
143 // it doesn't match that pattern, pass it through.64 if (owner) {
144 if (response.err) {65 tmp.unshift('~' + owner);
145 callback(true, response);66 }
146 } else if (response.result) {67 this.set('full_name', tmp.join('/'));
147 callback(false, response.result);68 // charm_store_path
148 } else { // This would typically be a string.69 this.set(
149 callback(false, response);70 'charm_store_path',
71 [(owner ? '~' + owner : 'charms'),
72 this.get('series'),
73 (this.get('package_name') + '-' + this.get('revision')),
74 'json'
75 ].join('/'));
76 },
77 sync: function(action, options, callback) {
78 if (action !== 'read') {
79 throw (
80 'Only use the "read" action; "' + action + '" not supported.');
81 }
82 if (Y.Lang.isValue(options.get_charm)) {
83 // This is an env.
84 options.get_charm(
85 this.get('id'),
86 function(response) {
87 if (response.err) {
88 callback(true, response);
89 } else if (response.result) {
90 callback(false, response.result);
91 } else {
92 // What's going on? This does not look like either of our
93 // expected signatures. Declare a loading error.
94 callback(true, response);
95 }
96 }
97 );
98 } else if (Y.Lang.isValue(options.loadByPath)) {
99 // This is a charm store.
100 options.loadByPath(
101 this.get('charm_store_path'),
102 { success: function(response) {
103 callback(false, response);
104 },
105 failure: function(response) {
106 callback(true, response);
107 }
108 });
109 } else {
110 throw 'You must supply a get_charm or loadByPath function.';
111 }
112 },
113 parse: function() {
114 var data = Charm.superclass.parse.apply(this, arguments),
115 self = this;
116 data.is_subordinate = data.subordinate;
117 Y.each(data, function(value, key) {
118 if (!value ||
119 !self.attrAdded(key) ||
120 Y.Lang.isValue(self.get(key))) {
121 delete data[key];
150 }122 }
151 },
152 // This is the optional error callback.
153 function(response) {
154 callback(true, response);
155 }
156 );
157 },
158 parse: function() {
159 return _clean_charm_data(Charm.superclass.parse.apply(this, arguments));
160 }
161 }, {
162 ATTRS: {
163 id: {
164 lazyAdd: false,
165 setter: function(val) {
166 if (!val) {
167 return val;
168 }
169 var parts = parse_charm_id(val),
170 self = this;
171 parts.revision = parseInt(parts.revision, 10);
172 Y.each(parts, function(value, key) {
173 self._set(key, value);
174 });123 });
175 this._set(124 if (data.owner === 'charmers') {
176 'charm_store_path', _calculate_charm_store_path(parts));125 delete data.owner;
177 this._set('full_name', _calculate_full_charm_name(parts));126 }
178 return _reconsititute_charm_id(parts);127 return data;
179 },128 },
180 validator: function(val) {129 compare: function(other, relevance, otherRelevance) {
181 var parts = parse_charm_id(val);130 // Official charms sort before owned charms.
182 return (parts && Y.Lang.isValue(parts.revision));131 // If !X.owner, that means it is owned by charmers.
132 var owner = this.get('owner'),
133 otherOwner = other.get('owner');
134 if (!owner && otherOwner) {
135 return -1;
136 } else if (owner && !otherOwner) {
137 return 1;
138 // Relevance is next most important.
139 } else if (relevance && (relevance !== otherRelevance)) {
140 // Higher relevance comes first.
141 return otherRelevance - relevance;
142 // Otherwise sort by package name, then by owner, then by revision.
143 } else {
144 return (
145 (this.get('package_name').localeCompare(
146 other.get('package_name'))) ||
147 (owner ? owner.localeCompare(otherOwner) : 0) ||
148 (this.get('revision') - other.get('revision')));
149 }
183 }150 }
184 },151 }, {
185 // All of the below are loaded except as noted.152 ATTRS: {
186 bzr_branch: {writeOnce: true},153 id: {
187 charm_store_path: {readOnly: true}, // calculated154 writeOnce: true,
188 config: {writeOnce: true},155 validator: function(val) {
189 description: {writeOnce: true},156 return Y.Lang.isString(val) && !!charm_id_re.exec(val);
190 full_name: {readOnly: true}, // calculated157 }
191 is_subordinate: {writeOnce: true},158 },
192 last_change:159 bzr_branch: {writeOnce: true},
193 { writeOnce: true,160 charm_store_path: {writeOnce: true},
161 config: {writeOnce: true},
162 description: {writeOnce: true},
163 full_name: {writeOnce: true},
164 is_subordinate: {writeOnce: true},
165 last_change: {
166 writeOnce: true,
194 setter: function(val) {167 setter: function(val) {
195 // Normalize created value from float to date object.168 // Normalize created value from float to date object.
196 if (val && val.created) {169 if (val && val.created) {
@@ -201,102 +174,42 @@
201 return val;174 return val;
202 }175 }
203 },176 },
204 maintainer: {writeOnce: true},177 maintainer: {writeOnce: true},
205 metadata: {writeOnce: true},178 metadata: {writeOnce: true},
206 package_name: {readOnly: true}, // calculated179 package_name: {writeOnce: true},
207 owner: {readOnly: true}, // calculated180 owner: {writeOnce: true},
208 peers: {writeOnce: true},181 peers: {writeOnce: true},
209 proof: {writeOnce: true},182 proof: {writeOnce: true},
210 provides: {writeOnce: true},183 provides: {writeOnce: true},
211 requires: {writeOnce: true},184 requires: {writeOnce: true},
212 revision: {readOnly: true}, // calculated185 revision: {
213 scheme: {readOnly: true}, // calculated186 writeOnce: true,
214 series: {readOnly: true}, // calculated187 setter: function(val) {
215 summary: {writeOnce: true},188 if (Y.Lang.isValue(val)) {
216 url: {writeOnce: true}189 val = parseInt(val, 10);
217 }190 }
218 });191 return val;
192 }
193 },
194 scheme: {
195 value: 'cs',
196 writeOnce: true,
197 setter: function(val) {
198 if (!Y.Lang.isValue(val)) {
199 val = 'cs';
200 }
201 return val;
202 }
203 },
204 series: {writeOnce: true},
205 summary: {writeOnce: true},
206 url: {writeOnce: true}
207 }
208 });
219 models.Charm = Charm;209 models.Charm = Charm;
220210
221 var CharmList = Y.Base.create('charmList', Y.ModelList, [], {211 var CharmList = Y.Base.create('charmList', Y.ModelList, [], {
222 model: Charm,212 model: Charm
223
224 initializer: function() {
225 this._baseIdHash = {}; // base id (without revision) to array of charms.
226 },
227
228 _addToBaseIdHash: function(charm) {
229 var baseId = charm.get('scheme') + ':' + charm.get('full_name'),
230 matches = this._baseIdHash[baseId];
231 if (!matches) {
232 matches = this._baseIdHash[baseId] = [];
233 }
234 matches.push(charm);
235 // Note that we don't handle changing baseIds or removed charms because
236 // that should not happen.
237 // Sort on newest charms first.
238 matches.sort(function(a, b) {
239 var revA = parseInt(a.get('revision'), 10),
240 revB = parseInt(b.get('revision'), 10);
241 return revB - revA;
242 });
243 },
244
245 add: function() {
246 var result = CharmList.superclass.add.apply(this, arguments);
247 if (Y.Lang.isArray(result)) {
248 Y.each(result, this._addToBaseIdHash, this);
249 } else {
250 this._addToBaseIdHash(result);
251 }
252 return result;
253 },
254
255 getOneByBaseId: function(id) {
256 var match = parse_charm_id(id),
257 baseId = match && _calculate_base_charm_id(match),
258 charms = baseId && this._baseIdHash[baseId];
259 return charms && charms[0];
260 },
261
262 loadOneByBaseId: function(id, options) {
263 var match = parse_charm_id(id);
264 if (match) {
265 if (!options.force) {
266 var charm = this.getOneByBaseId(_calculate_base_charm_id(match));
267 if (charm) {
268 if (options.success) {
269 options.success(charm);
270 }
271 return;
272 }
273 }
274 var path = _calculate_charm_store_path(match),
275 self = this;
276 options.charm_store.loadByPath(
277 path,
278 { success: function(data) {
279 // We fall back to 0 for revision. Some records do not have one
280 // still in the charm store, such as
281 // http://jujucharms.com/charms/precise/appflower/json (as of this
282 // writing).
283 match.revision = data.store_revision || 0;
284 id = _reconsititute_charm_id(match);
285 charm = self.getById(id);
286 if (!charm) {
287 charm = self.add({id: id});
288 charm.setAttrs(_clean_charm_data(data));
289 charm.loaded = true;
290 }
291 if (options.success) {
292 options.success(charm);
293 }
294 },
295 failure: options.failure });
296 } else {
297 throw id + ' is not a valid base charm id';
298 }
299 }
300 }, {213 }, {
301 ATTRS: {}214 ATTRS: {}
302 });215 });
303216
=== modified file 'app/modules.js'
--- app/modules.js 2012-10-19 01:58:34 +0000
+++ app/modules.js 2012-10-23 20:09:21 +0000
@@ -1,6 +1,6 @@
1GlobalConfig = {1GlobalConfig = {
2 // Uncomment for debug versions of YUI.2 // Uncomment for debug versions of YUI.
3 //filter: 'debug',3 filter: 'debug',
4 // Uncomment for verbose logging of YUI4 // Uncomment for verbose logging of YUI
5 debug: false,5 debug: false,
66
@@ -83,6 +83,7 @@
83 },83 },
8484
85 'juju-charm-models': {85 'juju-charm-models': {
86 requires: ['juju-charm-id'],
86 fullpath: '/juju-ui/models/charm.js'87 fullpath: '/juju-ui/models/charm.js'
87 },88 },
8889
@@ -103,6 +104,7 @@
103 },104 },
104105
105 'juju-charm-store': {106 'juju-charm-store': {
107 requires: ['juju-charm-id'],
106 fullpath: '/juju-ui/store/charm.js'108 fullpath: '/juju-ui/store/charm.js'
107 },109 },
108110
109111
=== modified file 'app/store/charm.js'
--- app/store/charm.js 2012-10-19 01:53:03 +0000
+++ app/store/charm.js 2012-10-23 20:09:21 +0000
@@ -16,9 +16,11 @@
16 }16 }
17 });17 });
18 },18 },
19 // The query can be a string that is passed directly to the search url, or a19 // The query can be a string that is passed directly to the search url, or
20 // hash that is marshalled to the correct format (e.g., {series:precise,20 // a hash that is marshalled to the correct format (e.g.,
21 // owner:charmers}).21 // {series:precise owner:charmers}). The method returns CharmId instances
22 // grouped by series and ordered within the groups according to the
23 // CharmId compare function.
22 find: function(query, options) {24 find: function(query, options) {
23 if (!Y.Lang.isString(query)) {25 if (!Y.Lang.isString(query)) {
24 var tmp = [];26 var tmp = [];
@@ -38,32 +40,33 @@
38 console.log('results update', result_set);40 console.log('results update', result_set);
39 options.success(41 options.success(
40 this._normalizeCharms(42 this._normalizeCharms(
41 result_set.results, options.defaultSeries));43 result_set.results, options.list, options.defaultSeries));
42 }, this),44 }, this),
43 'failure': options.failure45 'failure': options.failure
44 }});46 }});
45 },47 },
46 // Stash the base id on each charm, convert the official "charmers" owner to48 // Convert the charm data into Charm instances, using only id and
47 // an empty owner, and group the charms within series. The series are49 // relevance. Group them into series. The series are arranged with first
48 // arranged with first the defaultSeries, if any, and then all other50 // the defaultSeries, if any, and then all other available series arranged
49 // available series arranged from newest to oldest. Within each series,51 // from newest to oldest. Within each series, official charms come first,
50 // official charms come first, sorted by relevance if available and package52 // sorted by relevance if available and package name otherwise; and then
51 // name otherwise; and then owned charms follow, sorted again by relevance53 // owned charms follow, sorted again by relevance if available and package
52 // if available and package name otherwise.54 // name otherwise.
53 _normalizeCharms: function(charms, defaultSeries) {55 _normalizeCharms: function(results, list, defaultSeries) {
54 var hash = {};56 var hash = {},
55 Y.each(charms, function(charm) {57 relevances = {};
56 charm.baseId = charm.series + '/' + charm.name;58 Y.each(results, function(result) {
57 if (charm.owner === 'charmers') {59 var charm = list.getById(result.store_url);
58 charm.owner = null;60 if (!charm) {
59 } else {61 charm = list.add(
60 charm.baseId = '~' + charm.owner + '/' + charm.baseId;62 { id: result.store_url, summary: result.summary });
61 }63 }
62 charm.baseId = 'cs:' + charm.baseId;64 var series = charm.get('series');
63 if (!Y.Lang.isValue(hash[charm.series])) {65 if (!Y.Lang.isValue(hash[series])) {
64 hash[charm.series] = [];66 hash[series] = [];
65 }67 }
66 hash[charm.series].push(charm);68 hash[series].push(charm);
69 relevances[charm.get('id')] = result.relevance;
67 });70 });
68 var series_names = Y.Object.keys(hash);71 var series_names = Y.Object.keys(hash);
69 series_names.sort(function(a, b) {72 series_names.sort(function(a, b) {
@@ -71,42 +74,19 @@
71 return -1;74 return -1;
72 } else if (a !== defaultSeries && b === defaultSeries) {75 } else if (a !== defaultSeries && b === defaultSeries) {
73 return 1;76 return 1;
74 } else if (a > b) {
75 return -1;
76 } else if (a < b) {
77 return 1;
78 } else {77 } else {
79 return 0;78 return -a.localeCompare(b);
80 }79 }
81 });80 });
82 return Y.Array.map(series_names, function(name) {81 return Y.Array.map(series_names, function(name) {
83 var charms = hash[name];82 var charms = hash[name];
84 charms.sort(function(a, b) {83 charms.sort(function(a, b) {
85 // If !a.owner, that means it is owned by charmers.84 return a.compare(
86 if (!a.owner && b.owner) {85 b, relevances[a.get('id')], relevances[b.get('id')]);
87 return -1;
88 } else if (a.owner && !b.owner) {
89 return 1;
90 } else if (a.relevance < b.relevance) {
91 return 1; // Higher relevance comes first.
92 } else if (a.relevance > b.relevance) {
93 return -1;
94 } else if (a.name < b.name) {
95 return -1;
96 } else if (a.name > b.name) {
97 return 1;
98 } else if (a.owner < b.owner) {
99 return -1;
100 } else if (a.owner > b.owner) {
101 return 1;
102 } else {
103 return 0;
104 }
105 });86 });
106 return {series: name, charms: hash[name]};87 return {series: name, charms: hash[name]};
107 });88 });
108 }89 }
109
110 }, {90 }, {
111 ATTRS: {91 ATTRS: {
112 datasource: {92 datasource: {
11393
=== modified file 'app/templates/charm-search-result.handlebars'
--- app/templates/charm-search-result.handlebars 2012-10-19 01:44:45 +0000
+++ app/templates/charm-search-result.handlebars 2012-10-23 20:09:21 +0000
@@ -7,14 +7,11 @@
7 {{#charms}}7 {{#charms}}
8 <li class="charm-entry">8 <li class="charm-entry">
9 <div>9 <div>
10
11 <button class="btn btn-primary deploy"10 <button class="btn btn-primary deploy"
12 data-url="{{baseId}}">Deploy</button>11 data-url="{{id}}">Deploy</button>
13 <a class="charm-detail" href="{{baseId}}">12 <a class="charm-detail" href="{{id}}">
14 {{#if owner}}{{owner}}/{{/if}}{{name}}</a>13 {{#if owner}}{{owner}}/{{/if}}{{package_name}}</a>
15
16 <div class="charm-summary">{{summary}}</div>14 <div class="charm-summary">{{summary}}</div>
17
18 </div>15 </div>
19 </li>16 </li>
20 {{/charms}}17 {{/charms}}
2118
=== modified file 'app/views/charm-search.js'
--- app/views/charm-search.js 2012-10-19 01:53:03 +0000
+++ app/views/charm-search.js 2012-10-23 20:09:21 +0000
@@ -50,7 +50,8 @@
50 self.set('resultEntries', charms);50 self.set('resultEntries', charms);
51 },51 },
52 failure: Y.bind(this._showErrors, this),52 failure: Y.bind(this._showErrors, this),
53 defaultSeries: this.get('defaultSeries')53 defaultSeries: this.get('defaultSeries'),
54 list: this.get('charms')
54 });55 });
55 }56 }
56 });57 });
@@ -63,7 +64,8 @@
63 self.set('defaultEntries', charms);64 self.set('defaultEntries', charms);
64 },65 },
65 failure: Y.bind(this._showErrors, this),66 failure: Y.bind(this._showErrors, this),
66 defaultSeries: this.get('defaultSeries')67 defaultSeries: this.get('defaultSeries'),
68 list: this.get('charms')
67 });69 });
68 }70 }
69 });71 });
@@ -81,8 +83,17 @@
81 searchText = this.get('searchText'),83 searchText = this.get('searchText'),
82 defaultEntries = this.get('defaultEntries'),84 defaultEntries = this.get('defaultEntries'),
83 resultEntries = this.get('resultEntries'),85 resultEntries = this.get('resultEntries'),
84 entries = searchText ? resultEntries : defaultEntries;86 raw_entries = searchText ? resultEntries : defaultEntries,
85 container.setHTML(this.template({charms: entries}));87 entries = raw_entries && raw_entries.map(
88 function(data) {
89 return {
90 series: data.series,
91 charms: data.charms.map(
92 function(charm) { return charm.getAttrs(); })
93 };
94 }
95 );
96 container.setHTML(this.template({ charms: entries }));
86 return this;97 return this;
87 },98 },
88 showDetails: function(ev) {99 showDetails: function(ev) {
@@ -156,7 +167,7 @@
156 views.CharmDescriptionView = CharmDescriptionView;167 views.CharmDescriptionView = CharmDescriptionView;
157168
158 var CharmConfigurationView = Y.Base.create(169 var CharmConfigurationView = Y.Base.create(
159 'CharmCollectionView', Y.View, [views.JujuBaseView], {170 'CharmConfigurationView', Y.View, [views.JujuBaseView], {
160 template: views.Templates['charm-pre-configuration'],171 template: views.Templates['charm-pre-configuration'],
161 tooltip: null,172 tooltip: null,
162 configFileContent: null,173 configFileContent: null,
@@ -399,7 +410,8 @@
399 charmsSearchPanel = new CharmCollectionView(410 charmsSearchPanel = new CharmCollectionView(
400 { container: charmsSearchPanelNode,411 { container: charmsSearchPanelNode,
401 app: app,412 app: app,
402 charmStore: charmStore }),413 charmStore: charmStore,
414 charms: charms }),
403 descriptionPanelNode = Y.Node.create(),415 descriptionPanelNode = Y.Node.create(),
404 descriptionPanel = new CharmDescriptionView(416 descriptionPanel = new CharmDescriptionView(
405 { container: descriptionPanelNode,417 { container: descriptionPanelNode,
@@ -432,15 +444,19 @@
432 contentNode.append(panels[config.name].get('container'));444 contentNode.append(panels[config.name].get('container'));
433 if (config.charmId) {445 if (config.charmId) {
434 newPanel.set('model', null); // Clear out the old.446 newPanel.set('model', null); // Clear out the old.
435 charms.loadOneByBaseId(447 var charm = charms.getById(config.charmId);
436 config.charmId,448 if (charm.loaded) {
437 { success: function(charm) {newPanel.set('model', charm);},449 newPanel.set('model', charm);
438 failure: function(data) {450 } else {
439 console.log('error loading charm', data);451 charm.load(charmStore, function(err, response) {
440 newPanel.fire('changePanel', {name: 'charms'});452 if (err) {
441 },453 console.log('error loading charm', response);
442 charm_store: charmStore454 newPanel.fire('changePanel', {name: 'charms'});
443 });455 } else {
456 newPanel.set('model', charm);
457 }
458 });
459 }
444 } else { // This is the search panel.460 } else { // This is the search panel.
445 newPanel.render();461 newPanel.render();
446 }462 }
447463
=== modified file 'test/data/search_results.json'
--- test/data/search_results.json 2012-10-19 01:44:45 +0000
+++ test/data/search_results.json 2012-10-23 20:09:21 +0000
@@ -1,63 +1,70 @@
1{1{
2 "matches": 7, 2 "matches": 7,
3 "charm_total": 466, 3 "charm_total": 469,
4 "results_size": 7, 4 "results_size": 7,
5 "search_time": 0.0011610984802246094, 5 "search_time": 0.001168966293334961,
6 "results": [6 "results": [
7 {7 {
8 "data_url": "/charms/precise/cassandra/json", 8 "data_url": "/charms/precise/cassandra/json",
9 "name": "cassandra", 9 "name": "cassandra",
10 "store_url": "cs:precise/cassandra-2",
10 "series": "precise", 11 "series": "precise",
11 "summary": "distributed storage system for structured data", 12 "summary": "distributed storage system for structured data",
12 "relevance": 29.552706339212623, 13 "relevance": 29.58930787782692,
13 "owner": "charmers"14 "owner": "charmers"
14 }, 15 },
15 {16 {
16 "data_url": "/charms/oneiric/cassandra/json", 17 "data_url": "/charms/oneiric/cassandra/json",
17 "name": "cassandra", 18 "name": "cassandra",
19 "store_url": "cs:~charmers/oneiric/cassandra-0",
18 "series": "oneiric", 20 "series": "oneiric",
19 "summary": "distributed storage system for structured data", 21 "summary": "distributed storage system for structured data",
20 "relevance": 29.429741180715094, 22 "relevance": 29.46580425464922,
21 "owner": "charmers"23 "owner": "charmers"
22 }, 24 },
23 {25 {
24 "data_url": "/~jjo/precise/cassandra/json", 26 "data_url": "/~jjo/precise/cassandra/json",
25 "name": "cassandra", 27 "name": "cassandra",
28 "store_url": "cs:~jjo/precise/cassandra-12",
26 "series": "precise", 29 "series": "precise",
27 "summary": "distributed storage system for structured data", 30 "summary": "distributed storage system for structured data",
28 "relevance": 28.060048153642214, 31 "relevance": 28.090375175505876,
29 "owner": "jjo"32 "owner": "jjo"
30 }, 33 },
31 {34 {
32 "data_url": "/~ev/precise/errors/json", 35 "data_url": "/~ev/precise/errors/json",
33 "name": "errors", 36 "name": "errors",
37 "store_url": "cs:~ev/precise/errors-0",
34 "series": "precise", 38 "series": "precise",
35 "summary": "https://errors.ubuntu.com", 39 "summary": "https://errors.ubuntu.com",
36 "relevance": 13.168754948011127, 40 "relevance": 13.18957931651269,
37 "owner": "ev"41 "owner": "ev"
38 }, 42 },
39 {43 {
40 "data_url": "/~ev/precise/daisy/json", 44 "data_url": "/~ev/precise/daisy/json",
41 "name": "daisy", 45 "name": "daisy",
46 "store_url": "cs:~ev/precise/daisy-15",
42 "series": "precise", 47 "series": "precise",
43 "summary": "Daisy error reporting server", 48 "summary": "Daisy error reporting server",
44 "relevance": 12.721871518319244, 49 "relevance": 12.767880577035612,
45 "owner": "ev"50 "owner": "ev"
46 }, 51 },
47 {52 {
48 "data_url": "/~ev/precise/daisy-retracer/json", 53 "data_url": "/~ev/precise/daisy-retracer/json",
49 "name": "daisy-retracer", 54 "name": "daisy-retracer",
55 "store_url": "cs:~ev/precise/daisy-retracer-8",
50 "series": "precise", 56 "series": "precise",
51 "summary": "Daisy error reporting server retracer", 57 "summary": "Daisy error reporting server retracer",
52 "relevance": 12.662102672474589, 58 "relevance": 12.539732674900428,
53 "owner": "ev"59 "owner": "ev"
54 }, 60 },
55 {61 {
56 "data_url": "/~negronjl/oneiric/cassandra/json", 62 "data_url": "/~negronjl/oneiric/cassandra/json",
57 "name": "cassandra", 63 "name": "cassandra",
64 "store_url": "cs:~negronjl/oneiric/cassandra-0",
58 "series": "oneiric", 65 "series": "oneiric",
59 "summary": "distributed storage system for structured data", 66 "summary": "distributed storage system for structured data",
60 "relevance": 30.40622159052724, 67 "relevance": 30.451103781408868,
61 "owner": "negronjl"68 "owner": "negronjl"
62 }69 }
63 ]70 ]
6471
=== modified file 'test/data/series_search_results.json'
--- test/data/series_search_results.json 2012-10-19 01:44:45 +0000
+++ test/data/series_search_results.json 2012-10-23 20:09:21 +0000
@@ -1,12 +1,13 @@
1{1{
2 "matches": 5, 2 "matches": 5,
3 "charm_total": 466, 3 "charm_total": 469,
4 "results_size": 5, 4 "results_size": 5,
5 "search_time": 0.0008029937744140625, 5 "search_time": 0.0007879734039306641,
6 "results": [6 "results": [
7 {7 {
8 "data_url": "/charms/quantal/glance/json", 8 "data_url": "/charms/quantal/glance/json",
9 "name": "glance", 9 "name": "glance",
10 "store_url": "cs:~charmers/quantal/glance-2",
10 "series": "quantal", 11 "series": "quantal",
11 "summary": "OpenStack Image Registry and Delivery Service", 12 "summary": "OpenStack Image Registry and Delivery Service",
12 "relevance": 0.0, 13 "relevance": 0.0,
@@ -15,6 +16,7 @@
15 {16 {
16 "data_url": "/charms/quantal/nova-cloud-controller/json", 17 "data_url": "/charms/quantal/nova-cloud-controller/json",
17 "name": "nova-cloud-controller", 18 "name": "nova-cloud-controller",
19 "store_url": "cs:~charmers/quantal/nova-cloud-controller-1",
18 "series": "quantal", 20 "series": "quantal",
19 "summary": "Openstack nova controller node.", 21 "summary": "Openstack nova controller node.",
20 "relevance": 0.0, 22 "relevance": 0.0,
@@ -23,6 +25,7 @@
23 {25 {
24 "data_url": "/charms/quantal/nova-volume/json", 26 "data_url": "/charms/quantal/nova-volume/json",
25 "name": "nova-volume", 27 "name": "nova-volume",
28 "store_url": "cs:~charmers/quantal/nova-volume-0",
26 "series": "quantal", 29 "series": "quantal",
27 "summary": "OpenStack Compute - storage", 30 "summary": "OpenStack Compute - storage",
28 "relevance": 0.0, 31 "relevance": 0.0,
@@ -31,6 +34,7 @@
31 {34 {
32 "data_url": "/charms/quantal/nova-compute/json", 35 "data_url": "/charms/quantal/nova-compute/json",
33 "name": "nova-compute", 36 "name": "nova-compute",
37 "store_url": "cs:~charmers/quantal/nova-compute-1",
34 "series": "quantal", 38 "series": "quantal",
35 "summary": "OpenStack compute", 39 "summary": "OpenStack compute",
36 "relevance": 0.0, 40 "relevance": 0.0,
@@ -39,6 +43,7 @@
39 {43 {
40 "data_url": "/charms/quantal/nyancat/json", 44 "data_url": "/charms/quantal/nyancat/json",
41 "name": "nyancat", 45 "name": "nyancat",
46 "store_url": "cs:~charmers/quantal/nyancat-0",
42 "series": "quantal", 47 "series": "quantal",
43 "summary": "Nyancat telnet server", 48 "summary": "Nyancat telnet server",
44 "relevance": 0.0, 49 "relevance": 0.0,
4550
=== modified file 'test/test_app.js'
--- test/test_app.js 2012-10-19 01:53:03 +0000
+++ test/test_app.js 2012-10-23 20:09:21 +0000
@@ -88,7 +88,7 @@
8888
89 // charms also require a mapping but only a name, not a function89 // charms also require a mapping but only a name, not a function
90 app.getModelURL(wp_charm).should.equal(90 app.getModelURL(wp_charm).should.equal(
91 '/charms/charms/precise/wordpress/json');91 '/charms/charms/precise/wordpress-6/json');
92 });92 });
9393
94 it('should display the configured environment name', function() {94 it('should display the configured environment name', function() {
9595
=== modified file 'test/test_charm_configuration.js'
--- test/test_charm_configuration.js 2012-10-20 18:17:23 +0000
+++ test/test_charm_configuration.js 2012-10-23 20:09:21 +0000
@@ -91,7 +91,7 @@
91 received_charm_url = charm_url;91 received_charm_url = charm_url;
92 received_service_name = service_name;92 received_service_name = service_name;
93 }},93 }},
94 charm = new models.Charm({id: 'precise/mysql-7'}),94 charm = new models.Charm({id: 'cs:precise/mysql-7'}),
95 view = new views.CharmConfigurationView(95 view = new views.CharmConfigurationView(
96 { container: container,96 { container: container,
97 model: charm,97 model: charm,
@@ -119,7 +119,7 @@
119 received_config = config;119 received_config = config;
120 received_num_units = num_units;120 received_num_units = num_units;
121 }},121 }},
122 charm = new models.Charm({id: 'precise/mysql-7'}),122 charm = new models.Charm({id: 'cs:precise/mysql-7'}),
123 view = new views.CharmConfigurationView(123 view = new views.CharmConfigurationView(
124 { container: container,124 { container: container,
125 model: charm,125 model: charm,
126126
=== modified file 'test/test_charm_search.js'
--- test/test_charm_search.js 2012-10-19 01:53:03 +0000
+++ test/test_charm_search.js 2012-10-23 20:09:21 +0000
@@ -5,7 +5,7 @@
5 searchResult = '{"results": [{"data_url": "this is my URL", ' +5 searchResult = '{"results": [{"data_url": "this is my URL", ' +
6 '"name": "membase", "series": "precise", "summary": ' +6 '"name": "membase", "series": "precise", "summary": ' +
7 '"Membase Server", "relevance": 8.728194117350437, ' +7 '"Membase Server", "relevance": 8.728194117350437, ' +
8 '"owner": "charmers"}]}';8 '"owner": "charmers", "store_url": "cs:precise/membase-6"}]}';
99
10 before(function(done) {10 before(function(done) {
11 Y = YUI(GlobalConfig).use(11 Y = YUI(GlobalConfig).use(
@@ -85,7 +85,7 @@
8585
86 searchTriggered.should.equal(true);86 searchTriggered.should.equal(true);
87 node.one('.charm-entry .btn.deploy').getData('url').should.equal(87 node.one('.charm-entry .btn.deploy').getData('url').should.equal(
88 'cs:precise/membase');88 'cs:precise/membase-6');
89 });89 });
9090
91 it('must be able to trigger charm details', function() {91 it('must be able to trigger charm details', function() {
@@ -107,7 +107,7 @@
107 testing: true107 testing: true
108 }),108 }),
109 node = panel.node;109 node = panel.node;
110 db.charms.add({id: 'cs:precise/membase'});110 db.charms.add({id: 'cs:precise/membase-6'});
111111
112 panel.show();112 panel.show();
113 var field = Y.one('#charm-search-field');113 var field = Y.one('#charm-search-field');
@@ -138,7 +138,7 @@
138 testing: true138 testing: true
139 }),139 }),
140 node = panel.node,140 node = panel.node,
141 charm = db.charms.add({id: 'cs:precise/membase'});141 charm = db.charms.add({id: 'cs:precise/membase-6'});
142 charm.loaded = true;142 charm.loaded = true;
143 panel.show();143 panel.show();
144 var field = Y.one('#charm-search-field');144 var field = Y.one('#charm-search-field');
145145
=== modified file 'test/test_charm_store.js'
--- test/test_charm_store.js 2012-10-19 01:53:03 +0000
+++ test/test_charm_store.js 2012-10-23 20:09:21 +0000
@@ -8,9 +8,10 @@
8 before(function(done) {8 before(function(done) {
9 Y = YUI(GlobalConfig).use(9 Y = YUI(GlobalConfig).use(
10 'datasource-local', 'json-stringify', 'juju-charm-store',10 'datasource-local', 'json-stringify', 'juju-charm-store',
11 'datasource-io', 'io', 'array-extras',11 'datasource-io', 'io', 'array-extras', 'juju-charm-models',
12 function(Y) {12 function(Y) {
13 juju = Y.namespace('juju');13 juju = Y.namespace('juju');
14 models = Y.namespace('juju.models');
14 done();15 done();
15 });16 });
16 });17 });
@@ -108,19 +109,20 @@
108 results.length.should.equal(2);109 results.length.should.equal(2);
109 results[0].series.should.equal('precise');110 results[0].series.should.equal('precise');
110 Y.Array.map(results[0].charms, function(charm) {111 Y.Array.map(results[0].charms, function(charm) {
111 return charm.owner;112 return charm.get('owner');
112 }).should.eql([null, 'jjo', 'ev', 'ev', 'ev']);113 }).should.eql([undefined, 'jjo', 'ev', 'ev', 'ev']);
113 Y.Array.map(results[0].charms, function(charm) {114 Y.Array.map(results[0].charms, function(charm) {
114 return charm.baseId;115 return charm.get('id');
115 }).should.eql([116 }).should.eql([
116 'cs:precise/cassandra',117 'cs:precise/cassandra-2',
117 'cs:~jjo/precise/cassandra',118 'cs:~jjo/precise/cassandra-12',
118 'cs:~ev/precise/errors',119 'cs:~ev/precise/errors-0',
119 'cs:~ev/precise/daisy',120 'cs:~ev/precise/daisy-15',
120 'cs:~ev/precise/daisy-retracer']);121 'cs:~ev/precise/daisy-retracer-8']);
121 done();122 done();
122 },123 },
123 failure: assert.fail124 failure: assert.fail,
125 list: new models.CharmList()
124 });126 });
125 });127 });
126128
@@ -135,7 +137,8 @@
135 results[0].series.should.equal('oneiric');137 results[0].series.should.equal('oneiric');
136 done();138 done();
137 },139 },
138 failure: assert.fail140 failure: assert.fail,
141 list: new models.CharmList()
139 });142 });
140 });143 });
141144
@@ -149,7 +152,7 @@
149 results.length.should.equal(1);152 results.length.should.equal(1);
150 results[0].series.should.equal('quantal');153 results[0].series.should.equal('quantal');
151 Y.Array.map(results[0].charms, function(charm) {154 Y.Array.map(results[0].charms, function(charm) {
152 return charm.name;155 return charm.get('package_name');
153 }).should.eql([156 }).should.eql([
154 'glance',157 'glance',
155 'nova-cloud-controller',158 'nova-cloud-controller',
@@ -158,9 +161,9 @@
158 'nyancat']);161 'nyancat']);
159 done();162 done();
160 },163 },
161 failure: assert.fail164 failure: assert.fail,
165 list: new models.CharmList()
162 });166 });
163 });167 });
164
165 });168 });
166})();169})();
167170
=== modified file 'test/test_model.js'
--- test/test_model.js 2012-10-19 01:53:03 +0000
+++ test/test_model.js 2012-10-23 20:09:21 +0000
@@ -19,56 +19,19 @@
19 var _ = expect(charm.get('owner')).to.not.exist;19 var _ = expect(charm.get('owner')).to.not.exist;
20 charm.get('full_name').should.equal('precise/openstack-dashboard');20 charm.get('full_name').should.equal('precise/openstack-dashboard');
21 charm.get('charm_store_path').should.equal(21 charm.get('charm_store_path').should.equal(
22 'charms/precise/openstack-dashboard/json');22 'charms/precise/openstack-dashboard-0/json');
23 });23 });
2424
25 it('must convert timestamps into time objects', function() {25 it('must convert timestamps into time objects', function() {
26 var time = 1349797266.032,26 var time = 1349797266.032,
27 date = new Date(time),27 date = new Date(time),
28 charm = new models.Charm(28 charm = new models.Charm(
29 { id: 'precise/foo', last_change: {created: time / 1000} });29 { id: 'cs:precise/foo-9', last_change: {created: time / 1000} });
30 charm.get('last_change').created.should.eql(date);30 charm.get('last_change').created.should.eql(date);
31 });31 });
3232
33 });33 });
3434
35 describe('charm id helper functions', function() {
36 var Y, models;
37
38 before(function(done) {
39 Y = YUI(GlobalConfig).use('juju-models', function(Y) {
40 models = Y.namespace('juju.models');
41 done();
42 });
43 });
44
45 it('must parse fully qualified names', function() {
46 // undefined never equals undefined.
47 var res = models.parse_charm_id('cs:precise/openstack-dashboard-0');
48 res.scheme.should.equal('cs');
49 var _ = expect(res.owner).to.not.exist;
50 res.series.should.equal('precise');
51 res.package_name.should.equal('openstack-dashboard');
52 res.revision.should.equal('0');
53 });
54
55 it('must parse names without revisions', function() {
56 var res = models.parse_charm_id('cs:precise/openstack-dashboard'),
57 _ = expect(res.revision).to.not.exist;
58 });
59
60 it('must parse fully qualified names with owners', function() {
61 models.parse_charm_id('cs:~bac/precise/openstack-dashboard-0').owner
62 .should.equal('bac');
63 });
64
65 it('must parse fully qualified names with hyphenated owners', function() {
66 models.parse_charm_id('cs:~alt-bac/precise/openstack-dashboard-0').owner
67 .should.equal('alt-bac');
68 });
69
70 });
71
72 describe('juju models', function() {35 describe('juju models', function() {
73 var Y, models;36 var Y, models;
7437
@@ -81,15 +44,16 @@
8144
82 it('must be able to create charm', function() {45 it('must be able to create charm', function() {
83 var charm = new models.Charm(46 var charm = new models.Charm(
84 {id: 'cs:~bac/precise/openstack-dashboard-0'});47 {id: 'cs:~alt-bac/precise/openstack-dashboard-0'});
85 charm.get('scheme').should.equal('cs');48 charm.get('scheme').should.equal('cs');
86 charm.get('owner').should.equal('bac');49 charm.get('owner').should.equal('alt-bac');
87 charm.get('series').should.equal('precise');50 charm.get('series').should.equal('precise');
88 charm.get('package_name').should.equal('openstack-dashboard');51 charm.get('package_name').should.equal('openstack-dashboard');
89 charm.get('revision').should.equal(0);52 charm.get('revision').should.equal(0);
90 charm.get('full_name').should.equal('~bac/precise/openstack-dashboard');53 charm.get('full_name').should.equal(
54 '~alt-bac/precise/openstack-dashboard');
91 charm.get('charm_store_path').should.equal(55 charm.get('charm_store_path').should.equal(
92 '~bac/precise/openstack-dashboard/json');56 '~alt-bac/precise/openstack-dashboard-0/json');
93 });57 });
9458
95 it('must be able to parse real-world charm names', function() {59 it('must be able to parse real-world charm names', function() {
@@ -97,7 +61,12 @@
97 charm.get('full_name').should.equal('precise/openstack-dashboard');61 charm.get('full_name').should.equal('precise/openstack-dashboard');
98 charm.get('package_name').should.equal('openstack-dashboard');62 charm.get('package_name').should.equal('openstack-dashboard');
99 charm.get('charm_store_path').should.equal(63 charm.get('charm_store_path').should.equal(
100 'charms/precise/openstack-dashboard/json');64 'charms/precise/openstack-dashboard-0/json');
65 charm.get('scheme').should.equal('cs');
66 var _ = expect(charm.get('owner')).to.not.exist;
67 charm.get('series').should.equal('precise');
68 charm.get('package_name').should.equal('openstack-dashboard');
69 charm.get('revision').should.equal(0);
101 });70 });
10271
103 it('must be able to parse individually owned charms', function() {72 it('must be able to parse individually owned charms', function() {
@@ -108,14 +77,28 @@
108 charm.get('full_name').should.equal('~marco-ceppi/precise/wordpress');77 charm.get('full_name').should.equal('~marco-ceppi/precise/wordpress');
109 charm.get('package_name').should.equal('wordpress');78 charm.get('package_name').should.equal('wordpress');
110 charm.get('charm_store_path').should.equal(79 charm.get('charm_store_path').should.equal(
111 '~marco-ceppi/precise/wordpress/json');80 '~marco-ceppi/precise/wordpress-17/json');
81 charm.get('revision').should.equal(17);
112 });82 });
11383
114 it('must reject bad charm ids.', function() {84 it('must reject bad charm ids.', function() {
115 var charm = new models.Charm({id: 'foobar'});85 try {
116 var _ = expect(charm.get('id')).to.not.exist;86 var charm = new models.Charm({id: 'foobar'});
117 charm.set('id', 'barfoo');87 assert.fail('Should have thrown an error');
118 _ = expect(charm.get('id')).to.not.exist;88 } catch (e) {
89 e.should.equal(
90 'Developers must initialize charms with a well-formed id.');
91 }
92 });
93
94 it('must reject missing charm ids at initialization.', function() {
95 try {
96 var charm = new models.Charm();
97 assert.fail('Should have thrown an error');
98 } catch (e) {
99 e.should.equal(
100 'Developers must initialize charms with a well-formed id.');
101 }
119 });102 });
120103
121 it('must be able to create charm list', function() {104 it('must be able to create charm list', function() {
@@ -403,7 +386,7 @@
403 });386 });
404387
405 it('will throw an exception with non-read sync', function() {388 it('will throw an exception with non-read sync', function() {
406 var charm = new models.Charm({id: 'local:precise/foo'});389 var charm = new models.Charm({id: 'local:precise/foo-4'});
407 try {390 try {
408 charm.sync('create');391 charm.sync('create');
409 assert.fail('Should have thrown an error');392 assert.fail('Should have thrown an error');
@@ -424,39 +407,40 @@
424 }407 }
425 });408 });
426409
427 it('throws an error if you do not pass a get_charm function', function() {410 it('throws an error if you do not pass get_charm or loadByPath function',
428 var charm = new models.Charm({id: 'local:precise/foo'});411 function() {
429 try {412 var charm = new models.Charm({id: 'local:precise/foo-4'});
430 charm.sync('read', {});413 try {
431 assert.fail('Should have thrown an error');414 charm.sync('read', {});
432 } catch (e) {415 assert.fail('Should have thrown an error');
433 e.should.equal(416 } catch (e) {
434 'You must supply a get_charm function.');417 e.should.equal(
435 }418 'You must supply a get_charm or loadByPath function.');
436 try {419 }
437 charm.sync('read', {env: 42});420 try {
438 assert.fail('Should have thrown an error');421 charm.sync('read', {env: 42});
439 } catch (e) {422 assert.fail('Should have thrown an error');
440 e.should.equal(423 } catch (e) {
441 'You must supply a get_charm function.');424 e.should.equal(
442 }425 'You must supply a get_charm or loadByPath function.');
443 try {426 }
444 charm.sync('read', {charm_store: 42});427 try {
445 assert.fail('Should have thrown an error');428 charm.sync('read', {charm_store: 42});
446 } catch (e) {429 assert.fail('Should have thrown an error');
447 e.should.equal(430 } catch (e) {
448 'You must supply a get_charm function.');431 e.should.equal(
449 }432 'You must supply a get_charm or loadByPath function.');
450 });433 }
434 });
451435
452 it('must send request to juju environment for local charms', function() {436 it('must send request to juju environment for local charms', function() {
453 var charm = new models.Charm({id: 'local:precise/foo'}).load(env);437 var charm = new models.Charm({id: 'local:precise/foo-4'}).load(env);
454 assert(!charm.loaded);438 assert(!charm.loaded);
455 conn.last_message().op.should.equal('get_charm');439 conn.last_message().op.should.equal('get_charm');
456 });440 });
457441
458 it('must handle success from local charm request', function(done) {442 it('must handle success from local charm request', function(done) {
459 var charm = new models.Charm({id: 'local:precise/foo'}).load(443 var charm = new models.Charm({id: 'local:precise/foo-4'}).load(
460 env,444 env,
461 function(err, response) {445 function(err, response) {
462 assert(!err);446 assert(!err);
@@ -471,7 +455,7 @@
471 });455 });
472456
473 it('must handle failure from local charm request', function(done) {457 it('must handle failure from local charm request', function(done) {
474 var charm = new models.Charm({id: 'local:precise/foo'}).load(458 var charm = new models.Charm({id: 'local:precise/foo-4'}).load(
475 env,459 env,
476 function(err, response) {460 function(err, response) {
477 assert(err);461 assert(err);
@@ -490,22 +474,18 @@
490 { responseText: Y.JSON.stringify(474 { responseText: Y.JSON.stringify(
491 { summary: 'wowza', subordinate: true, store_revision: 7 })});475 { summary: 'wowza', subordinate: true, store_revision: 7 })});
492476
493 var list = new models.CharmList();477 var charm = new models.Charm({id: 'cs:precise/foo-7'});
494 list.loadOneByBaseId(478 charm.load(
495 'cs:precise/foo',479 charm_store,
496 { success: function(charm) {480 function(err, data) {
481 if (err) { assert.fail('should succeed!'); }
497 assert(charm.loaded);482 assert(charm.loaded);
498 charm.get('summary').should.equal('wowza');483 charm.get('summary').should.equal('wowza');
499 charm.get('is_subordinate').should.equal(true);484 charm.get('is_subordinate').should.equal(true);
500 charm.get('scheme').should.equal('cs');485 charm.get('scheme').should.equal('cs');
501 charm.get('revision').should.equal(7);486 charm.get('revision').should.equal(7);
502 charm.get('id').should.equal('cs:precise/foo-7');487 charm.get('id').should.equal('cs:precise/foo-7');
503 list.getById('cs:precise/foo-7').should.equal(charm);
504 list.getOneByBaseId('cs:precise/foo').should.equal(charm);
505 done();488 done();
506 },
507 failure: assert.fail,
508 charm_store: charm_store
509 });489 });
510 });490 });
511491
@@ -521,15 +501,14 @@
521 original.apply(datasource, [e]);501 original.apply(datasource, [e]);
522 };502 };
523 data.push({responseText: Y.JSON.stringify({darn_it: 'uh oh!'})});503 data.push({responseText: Y.JSON.stringify({darn_it: 'uh oh!'})});
524 list.loadOneByBaseId(504 var charm = new models.Charm({id: 'cs:precise/foo-7'});
525 'cs:precise/foo',505 charm.load(
526 { success: assert.fail,506 charm_store,
527 failure: function(err) {507 function(err, data) {
528 var _ = expect(list.getOneByBaseId('cs:precise/foo'))508 if (!err) {
529 .to.not.exist;509 assert.fail('should fail!');
530 done();510 }
531 },511 done();
532 charm_store: charm_store
533 });512 });
534 });513 });
535514
536515
=== modified file 'test/test_service_view.js'
--- test/test_service_view.js 2012-10-12 18:27:36 +0000
+++ test/test_service_view.js 2012-10-23 20:09:21 +0000
@@ -30,7 +30,7 @@
30 app = { env: env, db: db,30 app = { env: env, db: db,
31 getModelURL: function(model, intent) {31 getModelURL: function(model, intent) {
32 return model.get('name'); }};32 return model.get('name'); }};
33 charm = new models.Charm({id: 'cs:precise/mysql',33 charm = new models.Charm({id: 'cs:precise/mysql-5',
34 description: 'A DB'});34 description: 'A DB'});
35 db.charms.add([charm]);35 db.charms.add([charm]);
36 // Add units sorted by id as that is what we expect from the server.36 // Add units sorted by id as that is what we expect from the server.
@@ -40,7 +40,7 @@
40 ]);40 ]);
41 service = new models.Service({41 service = new models.Service({
42 id: 'mysql',42 id: 'mysql',
43 charm: 'cs:precise/mysql',43 charm: 'cs:precise/mysql-5',
44 unit_count: db.units.size(),44 unit_count: db.units.size(),
45 exposed: false});45 exposed: false});
4646

Subscribers

People subscribed via source and target branches