← Back to team overview

yellow team mailing list archive

[Merge] lp:~benji/juju-gui/tweak-show_environment-2 into lp:juju-gui

 

Benji York has proposed merging lp:~benji/juju-gui/tweak-show_environment-2 into lp:juju-gui.

Requested reviews:
  Juju GUI Hackers (juju-gui)
Related bugs:
  Bug #1068653 in juju-ui: "Stop sending the app object to views."
  https://bugs.launchpad.net/juju-gui/+bug/1068653
  Bug #1068659 in juju-ui: "Stop sending the app object to views."
  https://bugs.launchpad.net/juju-gui/+bug/1068659

For more details, see:
https://code.launchpad.net/~benji/juju-gui/tweak-show_environment-2/+merge/130552

Stop sending the app object to views.

This refactoring is intended to flatten the namespace views use and make the
values available to views both more constrained and better specified.

https://codereview.appspot.com/6735048/

-- 
https://code.launchpad.net/~benji/juju-gui/tweak-show_environment-2/+merge/130552
Your team Juju GUI Hackers is requested to review the proposed merge of lp:~benji/juju-gui/tweak-show_environment-2 into lp:juju-gui.
=== modified file 'app/app.js'
--- app/app.js	2012-10-19 01:44:45 +0000
+++ app/app.js	2012-10-25 10:13:22 +0000
@@ -273,7 +273,7 @@
           'unit',
           // The querystring is used to handle highlighting relation rows in
           // links from notifications about errors.
-          { unit: unit, db: this.db, env: this.env, app: this,
+          { unit: unit, db: this.db, env: this.env,
             querystring: req.query });
     },
 
@@ -287,7 +287,7 @@
       if (Y.Lang.isValue(service)) {
         if (!service.get('loaded')) {
           this.env.get_service(
-              service.get('id'), Y.bind(this.load_service, this));
+              service.get('id'), Y.bind(this.loadService, this));
         }
         var charm_id = service.get('charm'),
             self = this;
@@ -308,7 +308,9 @@
       this._prefetch_service(service);
       this.showView(viewName, {
         model: service,
-        app: this,
+        db: this.db,
+        env: this.env,
+        getModelURL: Y.bind(this.getModelURL, this),
         querystring: req.query
       }, {}, function(view) {
         // If the view contains a method call fitToWindow,
@@ -355,7 +357,6 @@
 
     show_notifications_overview: function(req) {
       this.showView('notifications_overview', {
-        app: this,
         env: this.env,
         notifications: this.db.notifications});
     },
@@ -373,7 +374,6 @@
       if (!instance) {
         view.instance = new views.NotificationsView(
             {container: Y.one('#notifications'),
-              app: this,
               env: this.env,
               notifications: this.db.notifications});
         view.instance.render();
@@ -397,14 +397,17 @@
 
     show_environment: function(req, res, next) {
       var view = this.getViewInfo('environment'),
-          instance = view.instance;
+          instance = view.instance,
+          self = this;
       if (!instance) {
         console.log('new env view');
-        this.showView('environment', {
-          app: this,
-          db: this.db,
-          env: this.env},
-        {render: true});
+        this.showView('environment',
+            { getModelURL: Y.bind(this.getModelURL, this),
+              getServiceEndpoints: function() {return self.serviceEndpoints;},
+              loadService: this.loadService,
+              db: this.db,
+              env: this.env},
+            {render: true});
       } else {
         /* The current impl makes extensive use of
          * event handlers which are not being properly rebound
@@ -412,23 +415,21 @@
          * to enable this but we have to land the basics of this branch
          * first.
          */
-        this.showView('environment', {app: this,
-          db: this.db,
-          env: this.env}, {
-          update: false,
-          render: true,
-          callback: function(view) {
-            //view.attachView();
-            view.postRender();
-            //view.updateCanvas();
-          }
-        });
+        this.showView('environment',
+            { getModelURL: Y.bind(this.getModelURL, this),
+              getServiceEndpoints: function() {return self.serviceEndpoints;},
+              loadService: this.loadService,
+              db: this.db,
+              env: this.env},
+            { update: false,
+              render: true,
+              callback: function(view) {view.postRender();}});
       }
       next();
     },
 
     // Model interactions -> move to db layer
-    load_service: function(evt) {
+    loadService: function(evt) {
       console.log('load service', evt);
       if (evt.err) {
         this.db.notifications.add(

=== modified file 'app/templates/service-footer-common-controls.partial'
--- app/templates/service-footer-common-controls.partial	2012-10-22 11:57:15 +0000
+++ app/templates/service-footer-common-controls.partial	2012-10-25 10:13:22 +0000
@@ -4,7 +4,7 @@
    <div class="inline"><span>Unit count</span></div>
    <div class="inline">
      <input type="text" id="num-service-units" value="{{service.unit_count}}">
-     <img class="divider" 
+     <img class="divider"
            src="/juju-ui/assets/images/bottom_bar_small_div.png" />
    </div>
  </div>
@@ -25,4 +25,4 @@
    {{/if}}
    </div>
   </div>
-</div>
\ No newline at end of file
+</div>

=== modified file 'app/views/charm-search.js'
--- app/views/charm-search.js	2012-10-25 07:38:57 +0000
+++ app/views/charm-search.js	2012-10-25 10:13:22 +0000
@@ -101,9 +101,80 @@
           { name: 'configuration',
             charmId: ev.currentTarget.getData('url')});
     },
+    // Create a data structure friendly to the view
+    normalizeCharms: function(charms) {
+      var hash = {},
+          defaultSeries = this.get('defaultSeries');
+      Y.each(charms, function(charm) {
+        charm.url = charm.series + '/' + charm.name;
+        if (charm.owner === 'charmers') {
+          charm.owner = null;
+        } else {
+          charm.url = '~' + charm.owner + '/' + charm.url;
+        }
+        charm.url = 'cs:' + charm.url;
+        if (!Y.Lang.isValue(hash[charm.series])) {
+          hash[charm.series] = [];
+        }
+        hash[charm.series].push(charm);
+      });
+      var series_names = Y.Object.keys(hash);
+      series_names.sort(function(a, b) {
+        if ((a === defaultSeries && b !== defaultSeries) || a > b) {
+          return -1;
+        } else if ((a !== defaultSeries && b === defaultSeries) || a < b) {
+          return 1;
+        } else {
+          return 0;
+        }
+      });
+      return Y.Array.map(series_names, function(name) {
+        var charms = hash[name];
+        charms.sort(function(a, b) {
+          // If !a.owner, that means it is owned by charmers.
+          if ((!a.owner && b.owner) || (a.owner < b.owner)) {
+            return -1;
+          } else if ((a.owner && !b.owner) || (a.owner > b.owner)) {
+            return 1;
+          } else if (a.name < b.name) {
+            return -1;
+          } else if (a.name > b.name) {
+            return 1;
+          } else {
+            return 0;
+          }
+        });
+        return {series: name, charms: hash[name]};
+      });
+    },
+    findCharms: function(query, callback) {
+      var charmStore = this.get('charmStore'),
+          db = this.get('db');
+      charmStore.sendRequest({
+        request: 'search/json?search_text=' + query,
+        callback: {
+          'success': Y.bind(function(io_request) {
+            // To see an example of what is being obtained, look at
+            // http://jujucharms.com/search/json?search_text=mysql .
+            var result_set = Y.JSON.parse(
+                io_request.response.results[0].responseText);
+            console.log('results update', result_set);
+            callback(this.normalizeCharms(result_set.results));
+          }, this),
+          'failure': function er(e) {
+            console.error(e.error);
+            db.notifications.add(
+                new models.Notification({
+                  title: 'Could not retrieve charms',
+                  message: e.error,
+                  level: 'error'
+                })
+            );
+          }}});
+    },
     _showErrors: function(e) {
       console.error(e.error);
-      this.get('app').db.notifications.add(
+      this.get('db').notifications.add(
           new models.Notification({
             title: 'Could not retrieve charms',
             message: e.error,
@@ -268,8 +339,8 @@
             // Some file read errors don't go through the error handler as
             // expected but instead return an empty string.  Warn the user if
             // this happens.
-            var app = this.get('app');
-            app.db.notifications.add(
+            var db = this.get('db');
+            db.notifications.add(
                 new models.Notification({
                   title: 'Configuration file error',
                   message: 'The configuration file loaded is empty.  ' +
@@ -298,8 +369,8 @@
               msg = 'An error occurred reading this file.';
           }
           if (msg) {
-            var app = this.get('app');
-            app.db.notifications.add(
+            var db = this.get('db');
+            db.notifications.add(
                 new models.Notification({
                   title: 'Error reading configuration file',
                   message: msg,
@@ -314,7 +385,8 @@
         },
         onCharmDeployClicked: function(evt) {
           var container = this.get('container'),
-              app = this.get('app'),
+              db = this.get('db'),
+              env = this.get('env'),
               serviceName = container.one('#service-name').get('value'),
               numUnits = container.one('#number-units').get('value'),
               charm = this.get('model'),
@@ -323,11 +395,11 @@
                   '#service-config .config-field');
           // The service names must be unique.  It is an error to deploy a
           // service with same name.
-          var existing_service = app.db.services.getById(serviceName);
+          var existing_service = db.services.getById(serviceName);
           if (Y.Lang.isValue(existing_service)) {
             console.log('Attempting to add service of the same name: ' +
                         serviceName);
-            app.db.notifications.add(
+            db.notifications.add(
                 new models.Notification({
                   title: 'Attempting to deploy service ' + serviceName,
                   message: 'A service with that name already exists.',
@@ -339,11 +411,11 @@
             config = null;
           }
           numUnits = parseInt(numUnits, 10);
-          app.env.deploy(url, serviceName, config, this.configFileContent,
-                         numUnits, function(ev) {
+          env.deploy(url, serviceName, config, this.configFileContent,
+              numUnits, function(ev) {
                 if (ev.err) {
                   console.log(url + ' deployment failed');
-                  app.db.notifications.add(
+                  db.notifications.add(
                       new models.Notification({
                         title: 'Error deploying ' + serviceName,
                         message: 'Could not deploy the requested service.',
@@ -351,7 +423,7 @@
                       }));
                 } else {
                   console.log(url + ' deployed');
-                  app.db.notifications.add(
+                  db.notifications.add(
                       new models.Notification({
                         title: 'Deployed ' + serviceName,
                         message: 'Successfully deployed the requested service.',
@@ -367,9 +439,9 @@
                     loaded: false,
                     config: config
                   });
-                  app.db.services.add([service]);
+                  db.services.add([service]);
                   // Force refresh.
-                  app.db.fire('update');
+                  db.fire('update');
                 }
               });
           this.goBack(evt);
@@ -400,16 +472,19 @@
         charmsSearchPanelNode = Y.Node.create(),
         charmsSearchPanel = new CharmCollectionView(
               { container: charmsSearchPanelNode,
-                app: app,
+                env: app.env,
+                db: app.db,
                 charmStore: charmStore }),
         descriptionPanelNode = Y.Node.create(),
         descriptionPanel = new CharmDescriptionView(
               { container: descriptionPanelNode,
-                app: app }),
+                env: app.env,
+                db: app.db}),
         configurationPanelNode = Y.Node.create(),
         configurationPanel = new CharmConfigurationView(
               { container: configurationPanelNode,
-                app: app}),
+                env: app.env,
+                db: app.db}),
         panels =
               { charms: charmsSearchPanel,
                 description: descriptionPanel,

=== modified file 'app/views/environment.js'
--- app/views/environment.js	2012-10-20 18:16:19 +0000
+++ app/views/environment.js	2012-10-25 10:13:22 +0000
@@ -415,7 +415,6 @@
         updateData: function() {
           //model data
           var vis = this.vis,
-              app = this.get('app'),
               db = this.get('db'),
               relations = db.relations.toArray(),
               services = db.services.map(views.toBoundingBox);
@@ -1369,7 +1368,7 @@
           },
 
           _destroyCallback: function(service, view, btn, ev) {
-            var app = view.get('app'),
+            var getModelURL = view.get('getModelURL'),
                 db = view.get('db');
             if (ev.err) {
               db.notifications.add(
@@ -1377,7 +1376,7 @@
                     title: 'Error destroying service',
                     message: 'Service name: ' + ev.service_name,
                     level: 'error',
-                    link: app.getModelURL(service),
+                    link: getModelURL(service),
                     modelId: service
                   })
               );
@@ -1402,10 +1401,10 @@
             view.show(view.vis.selectAll('.service'));
 
             var db = view.get('db'),
-                app = view.get('app'),
+                getServiceEndpoints = view.get('getServiceEndpoints'),
                 service = view.serviceForBox(m),
                 endpoints = models.getEndpoints(
-                    service, app.serviceEndpoints, db),
+                    service, getServiceEndpoints(), db),
 
                 /* Transform endpoints into a list of
                  * relatable services (to the service in m)

=== modified file 'app/views/service.js'
--- app/views/service.js	2012-10-22 11:57:15 +0000
+++ app/views/service.js	2012-10-25 10:13:22 +0000
@@ -53,7 +53,7 @@
       var service = this.get('model'),
           unit_count = service.get('unit_count'),
           field = this.get('container').one('#num-service-units'),
-          env = this.get('app').env;
+          env = this.get('env');
 
       if (requested_unit_count < 1) {
         console.log('You must have at least one unit');
@@ -69,7 +69,7 @@
             Y.bind(this._addUnitCallback, this));
       } else if (delta < 0) {
         delta = Math.abs(delta);
-        var units = this.get('app').db.units.get_units_for_service(service),
+        var units = this.get('db').units.get_units_for_service(service),
             unit_ids_to_remove = [];
 
         for (var i = units.length - 1;
@@ -87,8 +87,8 @@
 
     _addUnitCallback: function(ev) {
       var service = this.get('model'),
-          app = this.get('app'),
-          db = this.get('app').db,
+          getModelURL = this.get('getModelURL'),
+          db = this.get('db'),
           unit_names = ev.result || [];
       if (ev.err) {
         db.notifications.add(
@@ -96,7 +96,7 @@
               title: 'Error adding unit',
               message: ev.num_units + ' units',
               level: 'error',
-              link: app.getModelURL(service),
+              link: getModelURL(service),
               modelId: service
             })
         );
@@ -115,8 +115,8 @@
 
     _removeUnitCallback: function(ev) {
       var service = this.get('model'),
-          app = this.get('app'),
-          db = this.get('app').db,
+          getModelURL = this.get('getModelURL'),
+          db = this.get('db'),
           unit_names = ev.unit_names;
       console.log('_removeUnitCallback with: ', arguments);
 
@@ -139,7 +139,7 @@
                 return 'Unit name: ' + ev.unit_names[0];
               })(),
               level: 'error',
-              link: app.getModelURL(service),
+              link: getModelURL(service),
               modelId: service
             })
         );
@@ -181,7 +181,7 @@
 
     destroyService: function(ev) {
       ev.preventDefault();
-      var env = this.get('app').env,
+      var env = this.get('env'),
           service = this.get('model');
       ev.target.set('disabled', true);
       env.destroy_service(
@@ -189,8 +189,8 @@
     },
 
     _destroyCallback: function(ev) {
-      var db = this.get('app').db,
-          app = this.get('app'),
+      var db = this.get('db'),
+          getModelURL = this.get('getModelURL'),
           service = this.get('model'),
           service_id = service.get('id');
 
@@ -200,7 +200,7 @@
               title: 'Error destroying service',
               message: 'Service name: ' + ev.service_name,
               level: 'error',
-              link: app.getModelURL(service),
+              link: getModelURL(service),
               modelId: service
             })
         );
@@ -229,22 +229,22 @@
 
     unexposeService: function() {
       var service = this.get('model'),
-          env = this.get('app').env;
+          env = this.get('env');
       env.unexpose(service.get('id'),
           Y.bind(this._unexposeServiceCallback, this));
     },
 
     _unexposeServiceCallback: function(ev) {
       var service = this.get('model'),
-          db = this.get('app').db,
-          app = this.get('app');
+          db = this.get('db'),
+          getModelURL = this.get('getModelURL');
       if (ev.err) {
         db.notifications.add(
             new models.Notification({
               title: 'Error un-exposing service',
               message: 'Service name: ' + ev.service_name,
               level: 'error',
-              link: app.getModelURL(service),
+              link: getModelURL(service),
               modelId: service
             })
         );
@@ -256,22 +256,22 @@
 
     exposeService: function() {
       var service = this.get('model'),
-          env = this.get('app').env;
+          env = this.get('env');
       env.expose(service.get('id'),
           Y.bind(this._exposeServiceCallback, this));
     },
 
     _exposeServiceCallback: function(ev) {
       var service = this.get('model'),
-          db = this.get('app').db,
-          app = this.get('app');
+          db = this.get('db'),
+          getModelURL = this.get('getModelURL');
       if (ev.err) {
         db.notifications.add(
             new models.Notification({
               title: 'Error exposing service',
               message: 'Service name: ' + ev.service_name,
               level: 'error',
-              link: app.getModelURL(service),
+              link: getModelURL(service),
               modelId: service
             })
         );
@@ -298,11 +298,11 @@
         },
 
         getServiceTabs: function(href) {
-          var app = this.get('app'),
+          var db = this.get('db'),
               service = this.get('model'),
               charmId = service.get('charm'),
-              charm = app.db.charms.getById(charmId),
-              charmUrl = (charm ? app.getModelURL(charm) : '#');
+              charm = db.charms.getById(charmId),
+              charmUrl = (charm ? this.get('getModelURL')(charm) : '#');
 
           var tabs = [{
             href: '.',
@@ -372,28 +372,32 @@
 
         render: function() {
           var container = this.get('container'),
-              app = this.get('app'),
+              getModelURL = this.get('getModelURL'),
               service = this.get('model'),
+              db = this.get('db'),
               querystring = this.get('querystring');
-          if (!service) {
+          if (!service || !service.get('loaded')) {
             container.setHTML('<div class="alert">Loading...</div>');
             console.log('waiting on service data');
             return this;
           }
-          var relation_data = utils.getRelationDataForService(app.db, service);
+          var relation_data = utils.getRelationDataForService(db, service);
           Y.each(relation_data, function(rel) {
             if (rel.relation_id === querystring.rel_id) {
               rel.highlight = true;
             }
           });
-
+          var charm_id = service.get('charm'),
+              charm = db.charms.getById(charm_id),
+              charm_attrs = charm ? charm.getAttrs() : undefined;
           container.setHTML(this.template(
               { viewName: 'relations',
                 tabs: this.getServiceTabs('relations'),
                 service: service.getAttrs(),
                 relations: relation_data,
-                charm: this.renderable_charm(service.get('charm'), app)}
-              ));
+                charm: charm_attrs,
+                charm_id: charm_id}));
+
         },
 
         confirmRemoved: function(ev) {
@@ -421,9 +425,10 @@
         doRemoveRelation: function(button, ev) {
           ev.preventDefault();
           var rel_id = button.get('value'),
-              app = this.get('app'),
+              db = this.get('db'),
+              env = this.get('env'),
               service = this.get('model'),
-              relation = app.db.relations.getById(rel_id),
+              relation = db.relations.getById(rel_id),
               endpoints = relation.get('endpoints'),
               endpoint_a = endpoints[0][0] + ':' + endpoints[0][1].name,
               endpoint_b;
@@ -437,7 +442,7 @@
 
           ev.target.set('disabled', true);
 
-          app.env.remove_relation(
+          env.remove_relation(
               endpoint_a,
               endpoint_b,
               Y.bind(this._removeRelationCallback, this,
@@ -446,23 +451,24 @@
 
         _removeRelationCallback: function(relation, rm_button,
             confirm_button, ev) {
-          var app = this.get('app'),
+          var db = this.get('db'),
+              getModelURL = this.get('getModelURL'),
               service = this.get('model');
           views.highlightRow(rm_button.ancestor('tr'), ev.err);
           if (ev.err) {
-            app.db.notifications.add(
+            db.notifications.add(
                 new models.Notification({
                   title: 'Error deleting relation',
                   message: 'Relation ' + ev.endpoint_a + ' to ' + ev.endpoint_b,
                   level: 'error',
-                  link: app.getModelURL(service) + 'relations?rel_id=' +
+                  link: getModelURL(service) + 'relations?rel_id=' +
                       relation.get('id'),
                   modelId: relation
                 })
             );
           } else {
-            app.db.relations.remove(relation);
-            app.db.fire('update');
+            db.relations.remove(relation);
+            db.fire('update');
           }
           confirm_button.set('disabled', false);
           this.remove_panel.hide();
@@ -483,7 +489,7 @@
         updateConstraints: function() {
           var service = this.get('model'),
               container = this.get('container'),
-              env = this.get('app').env;
+              env = this.get('env');
 
           var values = (function() {
             var result = [],
@@ -508,9 +514,10 @@
 
         _setConstraintsCallback: function(container, ev) {
           var service = this.get('model'),
-              env = this.get('app').env,
-              app = this.get('app'),
-              db = this.get('app').db;
+              env = this.get('env'),
+              getModelURL = this.get('getModelURL'),
+              loadService = this.get('loadService'),
+              db = this.get('db');
 
           if (ev.err) {
             db.notifications.add(
@@ -518,7 +525,7 @@
                   title: 'Error setting service constraints',
                   message: 'Service name: ' + ev.service_name,
                   level: 'error',
-                  link: app.getModelURL(service) + 'constraints',
+                  link: getModelURL(service) + 'constraints',
                   modelId: service
                 })
             );
@@ -527,7 +534,7 @@
 
           } else {
             env.get_service(
-                service.get('id'), Y.bind(app.load_service, app));
+                service.get('id'), loadService);
 
             // The usual result of a successful request is a page refresh.
             // Therefore, we need to set this delay in order to show the
@@ -540,11 +547,11 @@
 
         render: function() {
           var container = this.get('container'),
-              app = this.get('app'),
-              service = this.get('model');
-
-          var constraints = service.get('constraints');
-          var display_constraints = [];
+              service = this.get('model'),
+              getModelURL = this.get('getModelURL'),
+              db = this.get('db'),
+              constraints = service.get('constraints'),
+              display_constraints = [];
 
           //these are read-only values
           var readOnlyConstraints = {
@@ -601,7 +608,7 @@
 
         render: function() {
           var container = this.get('container'),
-              app = this.get('app'),
+              db = this.get('db'),
               service = this.get('model');
 
           if (!service || !service.get('loaded')) {
@@ -613,8 +620,9 @@
           console.log('config', service.get('config'));
 
           // combine the charm schema and the service values for display.
-          var charm = app.db.charms.getById(service.get('charm')),
+          var charm = db.charms.getById(service.get('charm')),
               config = service.get('config'),
+              getModelURL = this.get('getModelURL'),
               charm_config = charm.get('config'),
               schema = charm_config && charm_config.options,
               settings = [],
@@ -698,10 +706,12 @@
         },
 
         saveConfig: function() {
-          var app = this.get('app'),
+          var env = this.get('env'),
+              db = this.get('db'),
+              getModelURL = this.get('getModelURL'),
               service = this.get('model'),
               charm_url = service.get('charm'),
-              charm = app.db.charms.getById(charm_url),
+              charm = db.charms.getById(charm_url),
               charm_config = charm.get('config'),
               schema = charm_config && charm_config.options,
               container = this.get('container');
@@ -714,7 +724,7 @@
               errors = utils.validate(new_values, schema);
 
           if (Y.Object.isEmpty(errors)) {
-            app.env.set_config(
+            env.set_config(
                 service.get('id'),
                 new_values,
                 Y.bind(this._setConfigCallback, this, container)
@@ -727,9 +737,10 @@
 
         _setConfigCallback: function(container, ev) {
           var service = this.get('model'),
-              env = this.get('app').env,
-              app = this.get('app'),
-              db = this.get('app').db;
+              env = this.get('env'),
+              getModelURL = this.get('getModelURL'),
+              loadService = this.get('loadService'),
+              db = this.get('db');
 
           if (ev.err) {
             db.notifications.add(
@@ -737,7 +748,7 @@
                   title: 'Error setting service config',
                   message: 'Service name: ' + ev.service_name,
                   level: 'error',
-                  link: app.getModelURL(service) + 'config',
+                  link: getModelURL(service) + 'config',
                   modelId: service
                 })
             );
@@ -745,7 +756,7 @@
               .removeAttribute('disabled');
 
           } else {
-            env.get_service(service.get('id'), Y.bind(app.load_service, app));
+            env.get_service(service.get('id'), loadService);
 
             // The usual result of a successful request is a page refresh.
             // Therefore, we need to set this delay in order to show the
@@ -791,7 +802,8 @@
           console.log('service view render');
 
           var container = this.get('container'),
-              app = this.get('app'),
+              getModelURL = this.get('getModelURL'),
+              db = this.get('db'),
               service = this.get('model'),
               filter_state = this.get('querystring').state;
 
@@ -801,7 +813,7 @@
             return this;
           }
 
-          var units = app.db.units.get_units_for_service(service),
+          var units = db.units.get_units_for_service(service),
               state_data = [{
                 title: 'All',
                 link: '.',
@@ -817,15 +829,18 @@
               count: this.filterUnits(lower, units).length,
               link: '?state=' + lower});
           }, this);
-          container.setHTML(this.template({
-            viewName: 'units',
-            tabs: this.getServiceTabs('.'),
-            service: service.getAttrs(),
-            charm_id: service.get('charm'),
-            state: filter_state,
-            units: this.filterUnits(filter_state, units),
-            states: state_data
-          }));
+          var charm_id = service.get('charm'),
+              charm = db.charms.getById(charm_id),
+              charm_attrs = charm ? charm.getAttrs() : undefined;
+          container.setHTML(this.template(
+              { viewName: 'units',
+                tabs: this.getServiceTabs('.'),
+                service: service.getAttrs(),
+                charm_id: charm_id,
+                charm: charm_attrs,
+                state: filter_state,
+                units: this.filterUnits(filter_state, units),
+                states: state_data}));
           return this;
         },
 

=== modified file 'app/views/unit.js'
--- app/views/unit.js	2012-10-05 20:39:08 +0000
+++ app/views/unit.js	2012-10-25 10:13:22 +0000
@@ -123,7 +123,7 @@
     _resolvedUnitCallback: function(button, ev) {
       var unit = this.get('unit'),
           db = this.get('db'),
-          app = this.get('app'),
+          getModelURL = this.get('getModelURL'),
           service = db.services.getById(unit.service);
       if (ev.err) {
         db.notifications.add(
@@ -131,7 +131,7 @@
               title: 'Error resolving unit',
               message: 'Unit name: ' + ev.unit_name,
               level: 'error',
-              link: app.getModelURL(unit),
+              link: getModelURL(unit),
               modelId: unit
             })
         );
@@ -172,7 +172,7 @@
     _removeUnitCallback: function(btn, ev) {
       var unit = this.get('unit'),
           db = this.get('db'),
-          app = this.get('app'),
+          getModelURL = this.get('getModelURL'),
           service = db.services.getById(unit.service),
           unit_name = ev.unit_names[0];
 
@@ -182,7 +182,7 @@
               title: 'Error removing unit',
               message: 'Unit name: ' + unit_name,
               level: 'error',
-              link: app.getModelURL(unit),
+              link: getModelURL(unit),
               modelId: unit
             })
         );
@@ -201,7 +201,7 @@
       var env = this.get('env'),
           unit = this.get('unit'),
           db = this.get('db'),
-          app = this.get('app'),
+          getModelURL = this.get('getModelURL'),
           service = db.services.getById(unit.service),
           button = ev.target;
       button.set('disabled', true);
@@ -213,7 +213,7 @@
                     title: 'Error retrying unit',
                     message: 'Unit name: ' + ev.unit_name,
                     level: 'error',
-                    link: app.getModelURL(unit),
+                    link: getModelURL(unit),
                     modelId: unit
                   })
               );
@@ -257,7 +257,7 @@
       views.highlightRow(button.ancestor('tr'), ev.err);
       if (ev.err) {
         var db = this.get('db'),
-            app = this.get('app'),
+            getModelURL = this.get('getModelURL'),
             unit = this.get('unit'),
             relation_id = button.ancestor('tr').get('id'),
             relation = db.relations.getById(relation_id);
@@ -266,7 +266,7 @@
               title: 'Error resolving relation',
               message: 'Could not resolve a unit relation',
               level: 'error',
-              link: app.getModelURL(unit) + '?rel_id=' + relation_id,
+              link: getModelURL(unit) + '?rel_id=' + relation_id,
               modelId: relation
             })
         );
@@ -280,7 +280,7 @@
       var env = this.get('env'),
           unit = this.get('unit'),
           db = this.get('db'),
-          app = this.get('app'),
+          getModelURL = this.get('getModelURL'),
           button = ev.target,
           relation_name = button.ancestor('form').get('id');
       button.set('disabled', true);
@@ -296,7 +296,7 @@
                     title: 'Error retrying relation',
                     message: 'Could not retry a unit relation',
                     level: 'error',
-                    link: app.getModelURL(unit) + '?rel_id=' + relation_id,
+                    link: getModelURL(unit) + '?rel_id=' + relation_id,
                     modelId: relation
                   })
               );

=== modified file 'app/views/utils.js'
--- app/views/utils.js	2012-10-20 18:17:23 +0000
+++ app/views/utils.js	2012-10-25 10:13:22 +0000
@@ -130,11 +130,11 @@
       this.after(['*:change', '*:load'], this.render, this);
     },
 
-    renderable_charm: function(charm_name, app) {
-      var charm = app.db.charms.getById(charm_name);
+    renderable_charm: function(charm_name, db, getModelURL) {
+      var charm = db.charms.getById(charm_name);
       if (charm) {
         var result = charm.getAttrs();
-        result.app_url = app.getModelURL(charm);
+        result.app_url = getModelURL(charm);
         return result;
       }
       return null;

=== modified file 'test/test_application_notifications.js'
--- test/test_application_notifications.js	2012-10-19 17:33:36 +0000
+++ test/test_application_notifications.js	2012-10-25 10:13:22 +0000
@@ -50,20 +50,20 @@
 
       db = new models.Database();
 
-      var notificationsView = new views.NotificationsView({
-        container: notificationsContainer,
-        app: {},
-        env: {
-          on: NO_OP,
-          get: function(key) {
-            if (key === 'connected') {
-              return true;
-            }
-            return null;
-          }
-        },
-        notifications: db.notifications
-      });
+      var notificationsView = new views.NotificationsView(
+          { container: notificationsContainer,
+            db: db,
+            env: {
+              on: NO_OP,
+              get: function(key) {
+                if (key === 'connected') {
+                  return true;
+                }
+                return null;
+              }
+            },
+            notifications: db.notifications
+          });
 
       notificationsView.render();
 
@@ -90,20 +90,18 @@
 
   it('should show notification for "add_unit" and "remove_units" exceptions' +
      ' (service view)', function() {
-       var view = new views.service({
-         container: viewContainer,
-         app: {
-           getModelURL: function() {
-             return 'my url';
+       var view = new views.service(
+       { container: viewContainer,
+         getModelURL: function() {
+           return 'my url';
+         },
+         db: db,
+         env: {
+           add_unit: function(serviceId, delta, callback) {
+             callback(ERR_EV);
            },
-           db: db,
-           env: {
-             add_unit: function(serviceId, delta, callback) {
-               callback(ERR_EV);
-             },
-             remove_units: function(param, callback) {
-               callback(ERR_EV);
-             }
+           remove_units: function(param, callback) {
+             callback(ERR_EV);
            }
          },
          model: {
@@ -116,8 +114,7 @@
              return null;
            }
          },
-         querystring: {}
-       }).render();
+         querystring: {}}).render();
 
        db.units.get_units_for_service = function() {
          return [{
@@ -135,118 +132,109 @@
      });
 
   it('should show notification for "remove_units" and "resolved" exceptions' +
-     ' (unit view)', function() {
-       var view = new views.unit({
-         container: viewContainer,
-         app: {
-           getModelURL: function() {
-             return 'my url';
-           }
-         },
+      ' (unit view)', function()
+     {
+       var view = new views.unit(
+       { container: viewContainer,
+         getModelURL: function() {return 'my url';},
          db: db,
          env: {
            remove_units: function(param, callback) {
-             callback({
-               err: true,
-               unit_names: ['aaa']
-             });
+             callback(
+             { err: true,
+               unit_names: ['aaa']});
            },
            resolved: function(unit_name, relation_name, retry, callback) {
              callback(ERR_EV);
-           }
-         },
+           }},
          unit: {},
          querystring: {}
        });
 
-       // Used by "unit.js" inside the "render" function
-       db.services.getById = function() {
-         // Mock service
-         return {
-           get: function(key) {
-             if (key === 'loaded') {
-               return true;
-             }
-             return null;
-           }
-         };
-       };
-
-       view.render();
-
-       view.confirmRemoved({
-         preventDefault: NO_OP
-       });
-
-       view.remove_panel.footerNode.one('.btn-danger').simulate('click');
-       view.remove_panel.destroy();
-
-       assertNotificationNumber('1');
-
-       // Fake relation
-       db.relations.getById = function() {
-         return {name: ''};
-       };
-
-       view.retryRelation({
-         preventDefault: NO_OP,
-
-         // This is a mock object of the relation button
-         target: {
-           ancestor: function() {
-             return {get: NO_OP};
-           },
-           set: NO_OP
-         }
-       });
-
-       assertNotificationNumber('2');
-     });
+        // Used by "unit.js" inside the "render" function
+        db.services.getById = function() {
+          // Mock service
+          return {
+            get: function(key) {
+              if (key === 'loaded') {
+                return true;
+              }
+              return null;
+            }
+          };
+        };
+
+        view.render();
+
+        view.confirmRemoved({
+          preventDefault: NO_OP
+        });
+
+        view.remove_panel.footerNode.one('.btn-danger').simulate('click');
+        view.remove_panel.destroy();
+
+        assertNotificationNumber('1');
+
+        // Fake relation
+        db.relations.getById = function() {
+          return {name: ''};
+        };
+
+        view.retryRelation({
+          preventDefault: NO_OP,
+
+          // This is a mock object of the relation button
+          target: {
+            ancestor: function() {
+              return {get: NO_OP};
+            },
+            set: NO_OP
+          }
+        });
+
+        assertNotificationNumber('2');
+      });
 
   it('should show notification for "add_relation" and "remove_relation"' +
-     ' exceptions (environment view)', function() {
-       var view = new views.environment({
-         db: db,
-         container: viewContainer});
-       view.render();
-       db.relations.remove = NO_OP;
+      ' exceptions (environment view)', function() {
+        var view = new views.environment({
+          db: db,
+          container: viewContainer});
+        view.render();
+        db.relations.remove = NO_OP;
 
-       view.service_click_actions._addRelationCallback.apply(view,
+        view.service_click_actions._addRelationCallback.apply(view,
        [view, 'relation_id', ERR_EV]);
 
-       assertNotificationNumber('1');
-
-       //view, relationElement, relationId, confirmButton, ev
-       view._removeRelationCallback.apply(view, [{
-         get: function() {return {hide: NO_OP};},
-         removeSVGClass: NO_OP
-       }, {}, '', {
-         set: NO_OP
-       }, ERR_EV]);
-
-       assertNotificationNumber('2');
-     });
+        assertNotificationNumber('1');
+
+        //view, relationElement, relationId, confirmButton, ev
+        view._removeRelationCallback.apply(view, [{
+          get: function() {return {hide: NO_OP};},
+          removeSVGClass: NO_OP
+        }, {}, '', {
+          set: NO_OP
+        }, ERR_EV]);
+
+        assertNotificationNumber('2');
+      });
 
   it('should show notification for "add_relation" and "destroy_service"' +
-     ' exceptions (environment view)', function() {
-       var fakeLink = (function() {
-         var link = [{}, {}];
-         link.enter = function() {
-           return {
-             insert: function() {
-               return {
-                 attr: NO_OP
-               };
-             }
-           };
-         };
-         return link;
-       })(),
-       app = {
-               getModelURL: NO_OP,
-               updateEndpoints: NO_OP
-             },
-             env = {
+      ' exceptions (environment view)', function() {
+        var fakeLink = (function() {
+          var link = [{}, {}];
+          link.enter = function() {
+            return {
+              insert: function() {
+                return {
+                  attr: NO_OP
+                };
+              }
+            };
+          };
+          return link;
+        })(),
+       env = {
                destroy_service: function(service, callback) {
                  callback(ERR_EV);
                },
@@ -273,8 +261,11 @@
                },
                env: env,
                get: function(key) {
-                 if ('app' === key) {
-                   return app;
+                 if ('getModelURL' === key) {
+                   return NO_OP;
+                 }
+                 if ('updateEndpoints' === key) {
+                   return NO_OP;
                  }
                  if ('env' === key) {
                    return env;
@@ -309,71 +300,64 @@
                }
              };
 
-       views.environment.prototype.service_click_actions.addRelationEnd
+        views.environment.prototype.service_click_actions.addRelationEnd
            .apply(view, [
          [
           ['s1', {name: 'n', role: 'client'}],
           ['s2', {name: 'n', role: 'server'}]],
          view]);
 
-       assertNotificationNumber('1');
+        assertNotificationNumber('1');
 
-       views.environment.prototype.service_click_actions.destroyService.apply(
+        views.environment.prototype.service_click_actions.destroyService.apply(
        //destroyService function signature > (m, view, btn)
        view, [{}, view, {set: NO_OP}]);
 
-       assertNotificationNumber('2');
-     });
+        assertNotificationNumber('2');
+      });
 
   it('should show notification for "get_service" exceptions' +
-     ' (service constraints view)', function() {
-
-       var view = new views.service_constraints({
-         model: {
-           getAttrs: NO_OP,
-           get: function(key) {
-             if ('constraints' === key) {
-               return {};
-             }
-             return null;
-           }
-         },
-         app: {
-           getModelURL: NO_OP,
-           db: db,
-           env: {
-             set_constraints: function(id, values, callback) {
-               callback(ERR_EV);
-             }
-           }
-         },
-
-         container: viewContainer}).render();
-
-       view.updateConstraints();
-
-       assertNotificationNumber('1');
-     });
+      ' (service constraints view)', function() {
+
+        var view = new views.service_constraints(
+            { model:
+             { getAttrs: NO_OP,
+               get: function(key) {
+                 if ('constraints' === key) {
+                   return {};
+                 }
+                 return null;
+               }},
+              getModelURL: NO_OP,
+              db: db,
+              env:
+                  { set_constraints: function(id, values, callback) {
+                    callback(ERR_EV);
+                  }},
+              container: viewContainer}).render();
+
+        view.updateConstraints();
+
+        assertNotificationNumber('1');
+      });
 
   it('should show notification for "get_service", "expose" and "unexpose"' +
-     ' exceptions (service config view)', function() {
+      ' exceptions (service config view)', function() {
 
-       var view = new views.service_config({
-         app: {
-           db: db,
-           env: {
-             set_config: function(id, newValues, callback) {
-               callback(ERR_EV);
-             },
-             expose: function(id, callback) {
-               callback(ERR_EV);
-             },
-             unexpose: function(id, callback) {
-               callback({err: true, service_name: '1234'});
-             }
-           },
-           getModelURL: NO_OP
+        var view = new views.service_config(
+       { db: db,
+         env: {
+           set_config: function(id, newValues, callback) {
+             callback(ERR_EV);
+           },
+           expose: function(id, callback) {
+             callback(ERR_EV);
+           },
+           unexpose: function(id, callback) {
+             callback({err: true, service_name: '1234'});
+           }
          },
+         getModelURL: NO_OP,
          model: {
            getAttrs: NO_OP,
            get: function(key) {
@@ -384,84 +368,81 @@
                return {};
              }
              return null;
-           }
-         },
+           }},
          container: viewContainer});
 
-       db.services.getById = NO_OP;
-       db.charms.getById = function() {
-         return {
-           getAttrs: function() {
-             return {};
-           },
-           get: function(key) {
-             if ('config' === key) {
-               return {};
-             }
-             return null;
-           }
-         };
-       };
-       view.render();
-
-       view.saveConfig();
-       assertNotificationNumber('1');
-
-       view.exposeService();
-       assertNotificationNumber('2');
-
-       view.unexposeService();
-       assertNotificationNumber('3');
-     });
+        db.services.getById = NO_OP;
+        db.charms.getById = function() {
+          return {
+            getAttrs: function() {
+              return {};
+            },
+            get: function(key) {
+              if ('config' === key) {
+                return {};
+              }
+              return null;
+            }
+          };
+        };
+        view.render();
+
+        view.saveConfig();
+        assertNotificationNumber('1');
+
+        view.exposeService();
+        assertNotificationNumber('2');
+
+        view.unexposeService();
+        assertNotificationNumber('3');
+      });
 
   it('should show notification for "remove_relation"' +
-     ' exceptions (service relations view)', function() {
+      ' exceptions (service relations view)', function() {
 
-       var view = new views.service_relations({
-         app: {
-           db: db,
-           env: {
-             remove_relation: function(id, newValues, callback) {
-               callback(ERR_EV);
-             }
-           },
-           getModelURL: NO_OP
+        var view = new views.service_relations(
+       { db: db,
+         env: {
+           remove_relation: function(id, newValues, callback) {
+             callback(ERR_EV);
+           }
          },
+         getModelURL: NO_OP,
          container: viewContainer});
 
-       db.relations.getById = function() {
-         return {
-           get: function(key) {
-             if ('endpoints' === key) {
-               return [
-                 [{}, {name: ''}]
-               ];
-             }
-             return null;
-           }
-         };
-       };
-
-       view.render();
-
-       view.confirmRemoved({
-         preventDefault: NO_OP,
-
-         // This is a mock object of the relation button
-         target: {
-           ancestor: NO_OP,
-           get: NO_OP
-         }
-       });
-       view.remove_panel.footerNode.one('.btn-danger').simulate('click');
-       view.remove_panel.destroy();
-
-       assertNotificationNumber('1');
-     });
+        db.relations.getById = function() {
+          return {
+            get: function(key) {
+              if ('endpoints' === key) {
+                return [
+                  [{}, {name: ''}]
+                ];
+              }
+              return null;
+            }
+          };
+        };
+
+        view.render();
+
+        view.confirmRemoved({
+          preventDefault: NO_OP,
+
+          // This is a mock object of the relation button
+          target: {
+            ancestor: NO_OP,
+            get: NO_OP
+          }
+        });
+        view.remove_panel.footerNode.one('.btn-danger').simulate('click');
+        view.remove_panel.destroy();
+
+        assertNotificationNumber('1');
+      });
 
   it('should show notification for "deploy" exceptions (charm view)',
-     function() {
-       var notified = false,
+      function() {
+        var notified = false,
        db = {
          notifications: {
            add: function() {
@@ -510,66 +491,56 @@
          }
        };
 
-       views.charm.prototype.on_charm_deploy.apply(mockView, [ERR_EV]);
-       assert.isTrue(notified);
-     });
+        views.charm.prototype.on_charm_deploy.apply(mockView, [ERR_EV]);
+        assert.isTrue(notified);
+      });
 
   it('should show errors for no unit, one unit and multiple ' +
-     'units (service view)', function() {
-
-       //_removeUnitCallback
-       var mockView = {
-         get: function(key) {
-           if ('app' === key) {
-             return {
-               getModelURL: NO_OP,
-               db: {
-                 fire: NO_OP,
-                 notifications: {
-                   add: function(notification) {
-                     messages.push(notification.get('message'));
-                     titles.push(notification.get('title'));
-                   }
-                 }
-               }
-             };
-           }
-           return null;
-         }
-       },
+      'units (service view)', function() {
+        //_removeUnitCallback
+        var mockView =
+            { get: function(key) {
+              return {
+           getModelURL: NO_OP,
+           db:
+           { fire: NO_OP,
+             notifications:
+             { add: function(notification) {
+               messages.push(notification.get('message'));
+               titles.push(notification.get('title'));
+             }}}}[key];
+       }},
        messages = [],
        titles = [],
        baseView = new views.serviceBase({});
 
-
-
-       baseView._removeUnitCallback.apply(mockView, [{
-         err: true,
-         unit_names: null
-       }]);
-
-       baseView._removeUnitCallback.apply(mockView, [{
-         err: true,
-         unit_names: []
-       }]);
-
-       baseView._removeUnitCallback.apply(mockView, [{
-         err: true,
-         unit_names: ['a']
-       }]);
-
-       baseView._removeUnitCallback.apply(mockView, [{
-         err: true,
-         unit_names: ['b', 'c']
-       }]);
-
-       function assertTrace(expected, trace) {
-         assert.equal(expected.join(';'), trace.join(';'));
-       }
-
-       assertTrace(['Error removing unit', 'Error removing unit',
-         'Error removing unit', 'Error removing units'], titles);
-       assertTrace(['', '', 'Unit name: a', 'Unit names: b, c'], messages);
-     });
+        baseView._removeUnitCallback.apply(mockView, [{
+          err: true,
+          unit_names: null
+        }]);
+
+        baseView._removeUnitCallback.apply(mockView, [{
+          err: true,
+          unit_names: []
+        }]);
+
+        baseView._removeUnitCallback.apply(mockView, [{
+          err: true,
+          unit_names: ['a']
+        }]);
+
+        baseView._removeUnitCallback.apply(mockView, [{
+          err: true,
+          unit_names: ['b', 'c']
+        }]);
+
+        function assertTrace(expected, trace) {
+          assert.equal(expected.join(';'), trace.join(';'));
+        }
+
+        assertTrace(['Error removing unit', 'Error removing unit',
+          'Error removing unit', 'Error removing units'], titles);
+        assertTrace(['', '', 'Unit name: a', 'Unit names: b, c'], messages);
+      });
 
 });

=== 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-25 10:13:22 +0000
@@ -1,7 +1,7 @@
 'use strict';
 
 describe('charm configuration', function() {
-  var Y, juju, app, db, models, views, container,
+  var Y, juju, db, models, views, makeView, container,
       charmConfig =
           { config:
                 { options:
@@ -43,24 +43,29 @@
     container = Y.Node.create('<div id="juju-search-charm-panel" />');
     Y.one('#main').append(container);
     db = new models.Database();
-    app = { db: db };
+    makeView = function(charm, env) {
+      return new views.CharmConfigurationView(
+          { container: container,
+            db: db,
+            env: env,
+            model: charm,
+            tooltipDelay: 0 }).render();
+    };
   });
+
   afterEach(function() {
     container.remove(true);
   });
 
   it('must show loading message if the charm is not loaded', function() {
-    var view = new views.CharmConfigurationView({container: container});
-    view.render();
+    var view = makeView();
     container.one('div.alert').get('text').trim().should.equal(
         'Waiting on charm data...');
   });
 
   it('must have inputs for service and number of units', function() {
     var charm = new models.Charm({id: 'precise/mysql-7'}),
-        view = new views.CharmConfigurationView(
-        { container: container,
-          model: charm});
+        view = makeView(charm);
     charm.loaded = true;
     // If the charm has no config options it is still handled.
     assert.isTrue(!Y.Lang.isValue(charm.config));
@@ -92,11 +97,7 @@
           received_service_name = service_name;
         }},
         charm = new models.Charm({id: 'precise/mysql-7'}),
-        view = new views.CharmConfigurationView(
-        { container: container,
-          model: charm,
-          app: app});
-    app.env = env;
+        view = makeView(charm, env);
     charm.loaded = true;
     view.render();
     container.one('#service-name').get('value').should.equal('mysql');
@@ -120,11 +121,7 @@
             received_num_units = num_units;
           }},
         charm = new models.Charm({id: 'precise/mysql-7'}),
-        view = new views.CharmConfigurationView(
-        { container: container,
-          model: charm,
-          app: app});
-    app.env = env;
+        view = makeView(charm, env);
     charm.setAttrs(
         { config:
               { options:
@@ -149,17 +146,14 @@
   it('must not deploy a charm with same name as an existing service',
      function() {
        var deployed = false,
-       env = {deploy: function(charm_url, service_name, config, config_raw,
-                               num_units) {
+       env =
+       { deploy:
+         function(charm_url, service_name, config, config_raw, num_units) {
            deployed = true;
          }},
        charm = new models.Charm({id: 'precise/mysql-7'}),
-       view = new views.CharmConfigurationView(
-       { container: container,
-         model: charm,
-         app: app});
+       view = makeView(charm, env);
        db.services.add([{id: 'wordpress'}]);
-       app.env = env;
        charm.loaded = true;
        view.render();
        container.one('#service-name').set('value', 'wordpress');
@@ -352,17 +346,14 @@
     var received_config,
         received_config_raw,
         charm = new models.Charm({id: 'precise/mysql-7'}),
-        app = {db: {services: {getById: function(name) {return null;}}},
-          env: {deploy: function(charm_url, service_name, config, config_raw,
-                                 num_units) {
-              received_config = config;
-              received_config_raw = config_raw;
-            }}},
-        view = new views.CharmConfigurationView(
-        { container: container,
-          model: charm,
-          app: app,
-          tooltipDelay: 0 });
+        db = {services: {getById: function(name) {return null;}}},
+        env =
+        { deploy:
+              function(charm_url, service_name, config, config_raw, num_units) {
+                received_config = config;
+                received_config_raw = config_raw;
+              }},
+        view = makeView(charm, env, db);
     charm.setAttrs(
         { config:
               { options:

=== 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-25 10:13:22 +0000
@@ -46,7 +46,7 @@
 
   it('must be able to show and hide the panel', function() {
     var panel = Y.namespace('juju.views').CharmSearchPopup
-          .getInstance({testing: true}),
+          .getInstance({testing: true, app: {}}),
         container = panel.node;
     container.getStyle('display').should.equal('none');
     panel.show();
@@ -75,7 +75,8 @@
               });
             }
           }}),
-          testing: true
+          testing: true,
+          app: {}
         }),
         node = panel.node;
     panel.show(true);

=== modified file 'test/test_environment_view.js'
--- test/test_environment_view.js	2012-10-20 18:16:19 +0000
+++ test/test_environment_view.js	2012-10-25 10:13:22 +0000
@@ -301,7 +301,7 @@
        function() {
          var view = new views.environment({
            container: container,
-           app: {serviceEndpoints: {}},
+           getServiceEndpoints: function() {return {};},
            db: db,
            env: env
          }).render();

=== modified file 'test/test_notifications.js'
--- test/test_notifications.js	2012-10-05 13:31:20 +0000
+++ test/test_notifications.js	2012-10-25 10:13:22 +0000
@@ -232,7 +232,6 @@
         view = new views.NotificationsView({
           container: container,
           notifications: notifications,
-          app: app,
           env: app.env}).render();
 
 

=== modified file 'test/test_service_config_view.js'
--- test/test_service_config_view.js	2012-10-19 01:44:45 +0000
+++ test/test_service_config_view.js	2012-10-25 10:13:22 +0000
@@ -3,7 +3,7 @@
 (function() {
   describe('juju service config view', function() {
     var ServiceConfigView, models, Y, container, service, db, conn,
-        env, charm, ENTER, utils, app, testUtils;
+        env, charm, ENTER, utils, testUtils, makeView;
 
     before(function(done) {
       Y = YUI(GlobalConfig).use('juju-views', 'juju-models', 'base',
@@ -16,6 +16,14 @@
             ServiceConfigView = Y.namespace('juju.views').service_config;
             utils = Y.namespace('juju.views.utils');
             testUtils = Y.namespace('juju-tests.utils');
+            makeView = function() {
+              return new ServiceConfigView(
+                  { container: container,
+                    model: service,
+                    db: db,
+                    env: env,
+                    getModelURL: function() {}}).render();
+            };
             done();
           });
     });
@@ -61,9 +69,6 @@
                               type: 'float'}}}});
 
       db.charms.add([charm]);
-      app = { env: env, db: db,
-              getModelURL: function(model, intent) {
-                return model.get('name'); }};
       service = new models.Service({
         id: 'mysql',
         charm: 'cs:precise/mysql-7',
@@ -90,14 +95,8 @@
     });
 
     it('should display the configuration', function() {
-      var view = new ServiceConfigView({
-        container: container,
-        model: service,
-        app: app
-      });
-      view.render();
-
-      var config = service.get('config'),
+      var view = makeView(),
+          config = service.get('config'),
           serviceConfigDiv = container.one('#service-config');
 
       Y.Object.each(config, function(value, name) {
@@ -116,12 +115,7 @@
     });
 
     it('should let the user change a configuration value', function() {
-      var view = new ServiceConfigView({
-        container: container,
-        model: service,
-        app: app
-      }).render();
-
+      var view = makeView();
       container.one('#input-option0').set('value', 'new value');
       // In tests the events are not wired up right so we will call this
       // event handler manually.
@@ -144,14 +138,7 @@
       };
 
       var ev = {err: true},
-          view = new ServiceConfigView({
-            container: container,
-            model: service,
-            app: (function() {
-              app.getModelURL = function() {};
-              return app;
-            })()
-          }).render();
+          view = makeView();
 
       // Clicking on the "Update" button disables it until the RPC
       // callback returns, then it is re-enabled.
@@ -162,17 +149,12 @@
 
     it('should display a message when a server error occurs', function() {
       var ev = {err: true},
+          view = makeView(),
           alert_ = container.one('#message-area>.alert');
       env.set_config = function(service, config, callback) {
         callback(ev);
       };
 
-      var view = new ServiceConfigView({
-        container: container,
-        model: service,
-        app: app
-      }).render();
-
       // Before an erroneous event is processed, no alert exists.
       var _ = expect(alert_).to.not.exist;
       // Handle the error event.
@@ -186,13 +168,8 @@
 
     it('should display an error when addErrorMessage is called',
        function() {
-          var view = new ServiceConfigView({
-           container: container,
-           model: service,
-           app: app
-         }).render();
-
-         var error_message = utils.SERVER_ERROR_MESSAGE,
+         var view = makeView(),
+             error_message = utils.SERVER_ERROR_MESSAGE,
          alert_ = container.one('#message-area>.alert');
 
          // Before an erroneous event is processed, no alert exists.
@@ -213,16 +190,11 @@
         // Mock function
         // view.saveConfig() calls it as part of its internal
         // "success" callback
-        app.load_service = function() {};
         env.set_config = function(service, config, callback) {
           callback(ev);
         };
         var ev = {err: false},
-            view = new ServiceConfigView({
-              app: app,
-              container: container,
-              model: service
-            }).render();
+            view = makeView();
 
         container.one('#input-' + key).set('value', value);
 

=== 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-25 10:13:22 +0000
@@ -2,8 +2,8 @@
 
 (function() {
   describe('juju service view', function() {
-    var ServiceView, ServiceRelationsView, models, Y, container, service, db,
-        conn, env, app, charm, ENTER, ESC;
+    var models, Y, container, service, db, conn, env, charm, ENTER, ESC,
+        makeServiceView, makeServiceRelationsView, views, unit;
 
     before(function(done) {
       Y = YUI(GlobalConfig).use(
@@ -13,8 +13,7 @@
             ENTER = Y.Node.DOM_EVENTS.key.eventDef.KEY_MAP.enter;
             ESC = Y.Node.DOM_EVENTS.key.eventDef.KEY_MAP.esc;
             models = Y.namespace('juju.models');
-            ServiceView = Y.namespace('juju.views').service;
-            ServiceRelationsView = Y.namespace('juju.views').service_relations;
+            views = Y.namespace('juju.views');
             done();
           });
     });
@@ -27,24 +26,39 @@
       container = Y.Node.create('<div id="test-container" />');
       Y.one('#main').append(container);
       db = new models.Database();
-      app = { env: env, db: db,
-              getModelURL: function(model, intent) {
-                return model.get('name'); }};
-      charm = new models.Charm({id: 'cs:precise/mysql',
-        description: 'A DB'});
+      charm = new models.Charm({id: 'cs:precise/mysql-7', description: 'A DB'});
       db.charms.add([charm]);
       // Add units sorted by id as that is what we expect from the server.
       db.units.add([{id: 'mysql/0', agent_state: 'pending'},
                     {id: 'mysql/1', agent_state: 'pending'},
                     {id: 'mysql/2', agent_state: 'pending'}
           ]);
-      service = new models.Service({
-        id: 'mysql',
-        charm: 'cs:precise/mysql',
-        unit_count: db.units.size(),
-        exposed: false});
+      service = new models.Service(
+          { id: 'mysql',
+            charm: 'cs:precise/mysql-7',
+            unit_count: db.units.size(),
+            loaded: true,
+            exposed: false});
 
       db.services.add([service]);
+      var viewMakerMaker = function(ViewPrototype) {
+        return function(querystring) {
+          if (!Y.Lang.isValue(querystring)) {
+            querystring = {};
+          }
+          return new ViewPrototype(
+              { container: container,
+                model: service,
+                db: db,
+                env: env,
+                getModelURL: function(model, intent) {
+                  return model.get('name');
+                },
+                querystring: querystring}).render();
+        };
+      };
+      makeServiceView = viewMakerMaker(views.service);
+      makeServiceRelationsView = viewMakerMaker(views.service_relations);
       done();
     });
 
@@ -57,26 +71,20 @@
     });
 
     it('should show controls to modify units by default', function() {
-      var view = new ServiceView(
-          { container: container, model: service,
-            app: app, querystring: {}}).render();
+      var view = makeServiceView();
       container.one('#num-service-units').should.not.equal(null);
     });
 
     it('should not show controls if the charm is subordinate', function() {
       charm.set('is_subordinate', true);
-      var view = new ServiceView(
-          { container: container, service: service, app: app,
-            querystring: {}}).render();
+      var view = makeServiceView();
       // "var _ =" makes the linter happy.
       var _ = expect(container.one('#num-service-units')).to.not.exist;
     });
 
     it('should show the service units ordered by number', function() {
       // Note that the units are added in beforeEach in an ordered manner.
-      var view = new ServiceView(
-          { container: container, model: service, app: app,
-            querystring: {}}).render();
+      var view = makeServiceView();
       var rendered_names = container.one(
           'ul.thumbnails').all('div.unit').get('id');
       var expected_names = db.units.map(function(u) {return u.id;});
@@ -87,10 +95,8 @@
 
     it('should show unit details when a unit is clicked', function() {
       // Note that the units are added in beforeEach in an ordered manner.
-      var view = new ServiceView(
-          {container: container, model: service, app: app,
-            querystring: {}}).render();
-      var unit = container.one('ul.thumbnails').one('div.unit'),
+      var view = makeServiceView(),
+          unit = container.one('ul.thumbnails').one('div.unit'),
           showUnitCalled = false;
       view.on('*:showUnit', function() {
         showUnitCalled = true;
@@ -101,9 +107,7 @@
 
     it('should use the show_units_large template if required', function() {
       // Note that the units are added in beforeEach in an ordered manner.
-      var view = new ServiceView(
-          {container: container, model: service, app: app,
-            querystring: {}}).render();
+      var view = makeServiceView();
       assert.equal('unit-large', container.one('ul.thumbnails').get('id'));
     });
 
@@ -120,27 +124,21 @@
     it('should use the show_units_medium template if required', function() {
       // Note that the units are added in beforeEach in an ordered manner.
       addUnits(30);
-      var view = new ServiceView(
-          {container: container, model: service, app: app,
-            querystring: {}}).render();
+      var view = makeServiceView();
       assert.equal('unit-medium', container.one('ul.thumbnails').get('id'));
     });
 
     it('should use the show_units_small template if required', function() {
       // Note that the units are added in beforeEach in an ordered manner.
       addUnits(60);
-      var view = new ServiceView(
-          {container: container, model: service, app: app,
-            querystring: {}}).render();
+      var view = makeServiceView();
       assert.equal('unit-small', container.one('ul.thumbnails').get('id'));
     });
 
     it('should use the show_units_tiny template if required', function() {
       // Note that the units are added in beforeEach in an ordered manner.
       addUnits(260);
-      var view = new ServiceView(
-          {container: container, model: service, app: app,
-            querystring: {}}).render();
+      var view = makeServiceView();
       assert.equal('unit-tiny', container.one('ul.thumbnails').get('id'));
     });
 
@@ -149,9 +147,7 @@
       // with ``pending`` status.
       addUnits(1, 'started');
       addUnits(2, 'start-error');
-      var view = new ServiceView(
-          {container: container, model: service, app: app,
-            querystring: {}}).render();
+      var view = makeServiceView();
       var thumbnails = container.one('ul.thumbnails');
       assert.equal(1, thumbnails.all('.state-started').size());
       assert.equal(2, thumbnails.all('.state-error').size());
@@ -160,18 +156,14 @@
 
     it('should start with the proper number of units shown in the text field',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
          control.get('value').should.equal('3');
        });
 
     it('should remove multiple units when the text input changes',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
          control.set('value', 1);
          control.simulate('keydown', { keyCode: ENTER }); // Simulate Enter.
@@ -182,9 +174,7 @@
 
     it('should not do anything if requested is < 1',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
          control.set('value', 0);
          control.simulate('keydown', { keyCode: ENTER });
@@ -196,9 +186,7 @@
        function() {
          service.set('unit_count', 1);
          db.units.remove([1, 2]);
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
          control.set('value', 0);
          control.simulate('keydown', { keyCode: ENTER });
@@ -208,9 +196,7 @@
 
     it('should add the correct number of units when entered via text field',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
          control.set('value', 7);
          control.simulate('keydown', { keyCode: ENTER });
@@ -223,14 +209,12 @@
     it('should add pending units as soon as it gets a reply back ' +
        'from the server',
        function() {
-         var new_unit_id = 'mysql/5';
-         var expected_names = db.units.map(function(u) {return u.id;});
+         var new_unit_id = 'mysql/5',
+             view = makeServiceView(),
+             control = container.one('#num-service-units'),
+             expected_names = db.units.map(function(u) {return u.id;});
          expected_names.push(new_unit_id);
          expected_names.sort();
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
-         var control = container.one('#num-service-units');
          control.set('value', 4);
          control.simulate('keydown', { keyCode: ENTER });
          var callbacks = Y.Object.values(env._txn_callbacks);
@@ -251,9 +235,7 @@
     it('should remove units as soon as it gets a ' +
        'reply back from the server',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
          control.set('value', 2);
          control.simulate('keydown', { keyCode: ENTER });
@@ -265,9 +247,7 @@
 
     it('should reset values on the control when you press escape',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
          control.set('value', 2);
          control.simulate('keydown', { keyCode: ESC });
@@ -276,9 +256,7 @@
 
     it('should reset values on the control when you change focus',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
          control.set('value', 2);
          control.simulate('blur');
@@ -287,9 +265,7 @@
 
     it('should reset values on the control when you type invalid value',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#num-service-units');
 
          var pressKey = function(key) {
@@ -305,9 +281,7 @@
     // Test for destroying services.
     it('should destroy the service when "Destroy Service" is clicked',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          var control = container.one('#destroy-service');
          control.simulate('click');
          var destroy = container.one('#destroy-modal-panel .btn-danger');
@@ -319,9 +293,7 @@
 
     it('should remove the service from the db after server ack',
        function() {
-         var view = new ServiceView(
-         { container: container, model: service, app: app,
-           querystring: {}}).render();
+         var view = makeServiceView();
          db.relations.add(
          [new models.Relation({id: 'relation-0000000000',
             endpoints: [['mysql', {}], ['wordpress', {}]]}),
@@ -350,9 +322,7 @@
 
     it('should send an expose RPC call when exposeService is invoked',
        function() {
-          var view = new ServiceView({
-            container: container, model: service, app: app,
-            querystring: {}});
+          var view = makeServiceView();
 
           view.exposeService();
           conn.last_message().op.should.equal('expose');
@@ -360,9 +330,7 @@
 
     it('should send an unexpose RPC call when unexposeService is invoked',
        function() {
-          var view = new ServiceView({
-            container: container, model: service, app: app,
-            querystring: {}});
+          var view = makeServiceView();
 
           view.unexposeService();
           conn.last_message().op.should.equal('unexpose');
@@ -370,9 +338,7 @@
 
     it('should invoke callback when expose RPC returns',
        function() {
-          var view = new ServiceView({
-            container: container, model: service, app: app,
-            querystring: {}}).render();
+          var view = makeServiceView();
 
          var test = function(selectorBefore, selectorAfter, callback) {
            console.log('Service is exposed: ' + service.get('exposed'));
@@ -404,9 +370,7 @@
        });
 
     it('should show proper tabs initially', function() {
-      var view = new ServiceView(
-          { container: container, model: service, app: app,
-            querystring: {}}).render(),
+      var view = makeServiceView(),
           active_navtabs = [];
       container.all('.state-btn').each(
           function(n) {
@@ -422,9 +386,7 @@
 
     it('should show zero running units when filtered', function() {
       // All units are pending.
-      var view = new ServiceView(
-          { container: container, model: service, app: app,
-            querystring: {state: 'running'}}).render(),
+      var view = makeServiceView({state: 'running'}),
           active_navtabs = [];
       container.all('.state-btn').each(
           function(n) {
@@ -443,9 +405,7 @@
       db.units.getById('mysql/0').agent_state = 'started';
       // 1 is pending.
       db.units.getById('mysql/2').agent_state = 'started';
-      var view = new ServiceView(
-          { container: container, model: service, app: app,
-            querystring: {state: 'running'}}).render();
+      var view = makeServiceView({state: 'running'});
       var rendered_names = container.one(
           'ul.thumbnails').all('div.unit').get('id');
       rendered_names.should.eql(['mysql/0', 'mysql/2']);
@@ -455,9 +415,7 @@
       db.units.getById('mysql/0').agent_state = 'install-error';
       db.units.getById('mysql/1').agent_state = 'error';
       db.units.getById('mysql/2').agent_state = 'started';
-      var view = new ServiceView(
-          { container: container, model: service, app: app,
-            querystring: {state: 'pending'}}).render(),
+      var view = makeServiceView({state: 'pending'}),
           active_navtabs = [];
       container.all('.state-btn').each(
           function(n) {
@@ -477,18 +435,14 @@
       db.units.getById('mysql/1').agent_state = 'started';
       // We include  installed with pending.
       db.units.getById('mysql/2').agent_state = 'installed';
-      var view = new ServiceView(
-          { container: container, model: service, app: app,
-            querystring: {state: 'pending'}}).render();
+      var view = makeServiceView({state: 'pending'});
       var rendered_names = container.one(
           'ul.thumbnails').all('div.unit').get('id');
       rendered_names.should.eql(['mysql/0', 'mysql/2']);
     });
 
     it('should show zero error units when filtered', function() {
-      var view = new ServiceView(
-          { container: container, model: service, app: app,
-            querystring: {state: 'error'}}).render(),
+      var view = makeServiceView({state: 'error'}),
           active_navtabs = [];
       container.all('.state-btn').each(
           function(n) {
@@ -508,17 +462,14 @@
       db.units.getById('mysql/0').agent_state = 'install-error';
       // 1 is pending.
       db.units.getById('mysql/2').agent_state = 'foo-error';
-      var view = new ServiceView(
-          { container: container, model: service, app: app,
-            querystring: {state: 'error'}}).render();
-      var rendered_names = container.one(
+      var view = makeServiceView({state: 'error'}),
+          rendered_names = container.one(
           'ul.thumbnails').all('div.unit').get('id');
       rendered_names.should.eql(['mysql/0', 'mysql/2']);
     });
 
     it('should remove the relation when requested',
        function() {
-
          var service_name = service.get('id'),
              rel0 = new models.Relation(
          { id: 'relation-0',
@@ -538,11 +489,8 @@
 
          db.relations.add([rel0, rel1]);
 
-         var view = new ServiceRelationsView(
-              { container: container, model: service, app: app,
-                querystring: {}}).render();
-
-         var control = container.one('#relation-0');
+         var view = makeServiceRelationsView(),
+             control = container.one('#relation-0');
          control.simulate('click');
          var remove = container.one('#remove-modal-panel .btn-danger');
          remove.simulate('click');
@@ -573,11 +521,8 @@
 
           db.relations.add([rel0, rel1]);
 
-          var view = new ServiceRelationsView(
-              { container: container, model: service, app: app,
-                querystring: {}}).render();
-
-          var control = container.one('#relation-1');
+          var view = makeServiceRelationsView(),
+              control = container.one('#relation-1');
           control.simulate('click');
           var remove = container.one('#remove-modal-panel .btn-danger');
           remove.simulate('click');
@@ -611,11 +556,8 @@
           db.relations.get_relations_for_service(
          service).length.should.equal(2);
 
-          var view = new ServiceRelationsView(
-              { container: container, model: service, app: app,
-                querystring: {}}).render();
-
-          var control = container.one('#relation-0');
+          var view = makeServiceRelationsView(),
+              control = container.one('#relation-0');
           control.simulate('click');
           var remove = container.one('#remove-modal-panel .btn-danger');
           remove.simulate('click');
@@ -653,11 +595,8 @@
 
           db.relations.add([rel0, rel1]);
 
-          var view = new ServiceRelationsView(
-              { container: container, model: service, app: app,
-                querystring: {rel_id: 'relation-0'}}).render();
-
-          var row = container.one('.highlighted');
+          var view = makeServiceRelationsView({rel_id: 'relation-0'}),
+              row = container.one('.highlighted');
           row.one('a').getHTML().should.equal('squid');
           row.one('.btn').get('disabled').should.equal(false);
         });
@@ -681,11 +620,8 @@
                     scope: 'global'
                   });
           db.relations.add([rel0, rel1]);
-          var view = new ServiceRelationsView(
-              { container: container, model: service, app: app,
-                querystring: {}});
-          view.render();
-          var control = container.one('#relation-0');
+          var view = makeServiceRelationsView(),
+              control = container.one('#relation-0');
           control.simulate('click');
           var remove = container.one('#remove-modal-panel .btn-danger');
           remove.simulate('click');
@@ -724,7 +660,7 @@
       }, {
         testKey: 'error2',
         agent_state: 'error'
-      }], filtered = ServiceView.prototype.filterUnits('error', units);
+      }], filtered = views.service.prototype.filterUnits('error', units);
 
       assert.equal(2, filtered.length);
       assert.equal('error1', filtered[0].testKey);

=== modified file 'test/test_unit_view.js'
--- test/test_unit_view.js	2012-10-19 01:44:45 +0000
+++ test/test_unit_view.js	2012-10-25 10:13:22 +0000
@@ -2,8 +2,8 @@
 
 (function() {
   describe('juju unit view', function() {
-    var UnitView, views, machine, models, Y, container, service, unit, db,
-        conn, juju, env, charm, testUtils;
+    var views, machine, models, Y, container, service, unit, db,
+        conn, juju, env, charm, testUtils, makeView;
 
     before(function(done) {
       Y = YUI(GlobalConfig).use(
@@ -12,13 +12,24 @@
           function(Y) {
             views = Y.namespace('juju.views');
             models = Y.namespace('juju.models');
-            UnitView = views.unit;
             testUtils = Y.namespace('juju-tests.utils');
             conn = new testUtils.SocketStub();
             juju = Y.namespace('juju');
             env = new juju.Environment({conn: conn});
             env.connect();
             conn.open();
+            makeView = function(querystring) {
+              if (!Y.Lang.isValue(querystring)) {
+                querystring = {};
+              }
+              return new views.unit(
+                  { container: container,
+                    unit: unit,
+                    db: db,
+                    env: env,
+                    getModelURL: function(u) { return u.id; },
+                    querystring: querystring}).render();
+            };
             done();
           });
     });
@@ -75,30 +86,22 @@
     });
 
     it('should include unit identifier', function() {
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('#unit-id').getHTML().should.contain('mysql/0');
     });
 
     it('should include charm URI', function() {
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('#charm-uri').getHTML().should.contain('cs:precise/mysql');
     });
 
     it('should include unit status', function() {
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('#unit-status').getHTML().should.contain('pending');
     });
 
     it('should include machine info', function() {
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('#machine-name').getHTML().should.contain(
           'Machine machine-0');
       container.one('#machine-agent-state').getHTML().should.contain(
@@ -112,9 +115,7 @@
     });
 
     it('should include relation info', function() {
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('#relations').getHTML().should.contain('Relations');
       container.one('.relation-ident').get('text').trim().should.equal('db:2');
       container.one('.relation-endpoint').get('text').trim().should.equal(
@@ -128,19 +129,15 @@
 
     it('should not display Retry and Resolved buttons when ' +
        'there is no error', function() {
-         var view = new UnitView(
-             { container: container, unit: unit, db: db, env: env,
-               querystring: {}}).render();
-         var _ = expect(container.one('#retry-unit-button')).to.not.exist;
-         _ = expect(container.one('#resolved-unit-button')).to.not.exist;
+          var view = makeView();
+          var _ = expect(container.one('#retry-unit-button')).to.not.exist;
+          _ = expect(container.one('#resolved-unit-button')).to.not.exist;
        });
 
     it('should display Retry and Resolved buttons when ' +
        'there is an error', function() {
           unit.agent_state = 'foo-error';
-          var view = new UnitView(
-              { container: container, unit: unit, db: db, env: env,
-                querystring: {}}).render();
+          var view = makeView();
           container.one('#retry-unit-button').getHTML().should.equal('Retry');
           container.one('#resolved-unit-button').getHTML().should.equal(
               'Resolved');
@@ -148,33 +145,26 @@
        });
 
     it('should always display Remove button', function() {
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render(),
+      var view = makeView(),
           button = container.one('#remove-unit-button');
       button.getHTML().should.equal('Remove');
       // Control is disabled with only one unit for the given service.
       button.get('disabled').should.equal(true);
     });
 
-    it('should send resolved op when confirmation button clicked',
-       function() {
-          unit.agent_state = 'foo-error';
-          var view = new UnitView(
-              { container: container, unit: unit, db: db, env: env,
-                querystring: {}}).render();
-          container.one('#resolved-unit-button').simulate('click');
-          container.one('#resolved-modal-panel .btn-danger').simulate('click');
-          var msg = conn.last_message();
-          msg.op.should.equal('resolved');
-          msg.retry.should.equal(false);
-       });
+    it('should send resolved op when confirmation button clicked', function() {
+      unit.agent_state = 'foo-error';
+      var view = makeView();
+      container.one('#resolved-unit-button').simulate('click');
+      container.one('#resolved-modal-panel .btn-danger').simulate('click');
+      var msg = conn.last_message();
+      msg.op.should.equal('resolved');
+      msg.retry.should.equal(false);
+    });
 
     it('should send resolved op with retry when retry clicked', function() {
       unit.agent_state = 'foo-error';
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('#retry-unit-button').simulate('click');
       var msg = conn.last_message();
       msg.op.should.equal('resolved');
@@ -184,9 +174,7 @@
     it('should send remove_units op when confirmation clicked', function() {
       // Enable removal button by giving the service more than one unit.
       service.set('unit_count', 2);
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('#remove-unit-button').simulate('click');
       container.one('#remove-modal-panel .btn-danger').simulate('click');
       var msg = conn.last_message();
@@ -196,9 +184,7 @@
     it('should clear out the database after a unit is removed', function() {
       // Enable removal button by giving the service more than one unit.
       service.set('unit_count', 2);
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('#remove-unit-button').simulate('click');
       container.one('#remove-modal-panel .btn-danger')
          .simulate('click');
@@ -215,9 +201,7 @@
 
     it('should show unit errors on the page with action buttons', function() {
       unit.relation_errors = {'db': ['mediawiki']};
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('.relation-status').get('text').trim().should.contain(
           'error');
       container.one('.relation-status').all('button').get('text')
@@ -225,18 +209,14 @@
     });
 
     it('should show highlighted relation rows', function() {
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {rel_id: 'relation-0000000002'}}).render();
+      var view = makeView({rel_id: 'relation-0000000002'});
       container.one('#relations tbody tr').hasClass('highlighted')
         .should.equal(true);
     });
 
     it('should be able to send a resolve relation message', function() {
       unit.relation_errors = {'db': ['mediawiki']};
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('.resolved-relation-button').simulate('click');
       container.one('#resolved-relation-modal-panel .btn-danger')
          .simulate('click');
@@ -249,9 +229,7 @@
 
     it('should be able to send a retry relation message', function() {
       unit.relation_errors = {'db': ['mediawiki']};
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {}}).render();
+      var view = makeView();
       container.one('.retry-relation-button').simulate('click');
       var msg = conn.last_message();
       msg.op.should.equal('resolved');
@@ -262,11 +240,7 @@
 
     it('should create an error notification if a resolve fails', function() {
       unit.relation_errors = {'db': ['mediawiki']};
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {},
-            app: {getModelURL: function(u) { return u.id; }}})
-        .render();
+      var view = makeView();
       container.one('.resolved-relation-button').simulate('click');
       container.one('#resolved-relation-modal-panel .btn-danger')
          .simulate('click');
@@ -282,11 +256,7 @@
 
     it('should create an error notification if a retry fails', function() {
       unit.relation_errors = {'db': ['mediawiki']};
-      var view = new UnitView(
-          { container: container, unit: unit, db: db, env: env,
-            querystring: {},
-            app: {getModelURL: function(u) { return u.id; }}})
-        .render();
+      var view = makeView();
       container.one('.retry-relation-button').simulate('click');
       var msg = conn.last_message();
       msg.err = true;


Follow ups