Merge branch 'feature/xosContentProvider' into develop
diff --git a/xos/core/xoslib/dashboards/contentProvider.html b/xos/core/xoslib/dashboards/contentProvider.html
index 7a3eb3c..24cd7f1 100644
--- a/xos/core/xoslib/dashboards/contentProvider.html
+++ b/xos/core/xoslib/dashboards/contentProvider.html
@@ -1,8 +1,18 @@
+<!-- 
+To setup hpc:
+- cd /opt/xos/tosca
+- python ./run.py padmin@vicci.org samples/cdn.yaml
+
+To generate hpcapi:
+- cd /opt/xos
+- python apigen/modelgen apigen/hpc-api.template.py -n User -n Service -a hpc > xos/hpcapi.py
+ -->
+
 <div ng-app="xos.contentProviderApp">
   <ng-view></ng-view>
 </div>
 
-
+<link rel="stylesheet" href="{{ STATIC_URL }}/css/xosLib.css">
 <script src="{{ STATIC_URL }}/js/vendor/angular/angular.min.js"></script>
 <script src="{{ STATIC_URL }}/js/vendor/angular-resource/angular-resource.min.js"></script>
 <script src="{{ STATIC_URL }}/js/vendor/angular-route/angular-route.min.js"></script>
diff --git a/xos/core/xoslib/spec/views/contentprovider.test.js b/xos/core/xoslib/spec/views/contentprovider.test.js
index ca9b727..64b676a 100644
--- a/xos/core/xoslib/spec/views/contentprovider.test.js
+++ b/xos/core/xoslib/spec/views/contentprovider.test.js
@@ -12,6 +12,7 @@
 
   beforeEach(function() {
     module(function($provide) {
+      // mocking routeParams to pass 1 as id
       $provide.provider('$routeParams', function() {
         this.$get = function() {
           return {id: 1};
@@ -71,22 +72,33 @@
   });
 
   describe('the contentProviderDetail directive', () => {
+
+    beforeEach(inject(function($compile, $rootScope) {
+      scope = $rootScope.$new();
+      element = angular.element('<content-provider-detail></content-provider-detail>');
+      $compile(element)(scope);
+      httpBackend.expectGET('/hpcapi/contentproviders/1/').respond(CPmock.CPlist[0]);
+      scope.$digest();
+      httpBackend.flush();
+      isolatedScope = element.isolateScope().vm;
+    }));
+
+    it('should select the active service provider', () => {
+      var res = isolatedScope.activeServiceProvide(1, 'http://0.0.0.0:9000/hpcapi/serviceproviders/1/');
+      expect(res).toBe(true);
+    });
+
+    it('should not select a non active service provider', () => {
+      var res = isolatedScope.activeServiceProvide(1, 'http://0.0.0.0:9000/hpcapi/serviceproviders/3/');
+      expect(res).toBe(false);
+    });
+
     describe('when an id is set in the route', () => {
 
-      beforeEach(inject(function($compile, $rootScope, ContentProvider) {
-        scope = $rootScope.$new();
-
-        httpBackend.expectGET('/hpcapi/contentproviders/1/').respond(CPmock.CPlist[0]);
-        httpBackend.whenPUT('/hpcapi/contentproviders/1/').respond({name: 'done'});
-
-        spyOn(ContentProvider, 'save').and.callThrough();
-
-        element = angular.element('<content-provider-detail></content-provider-detail>');
-        $compile(element)(scope);
-        scope.$digest();
-        httpBackend.flush();
-        isolatedScope = element.isolateScope().vm;
-      }));
+      beforeEach(() => {
+        // spy the instance update method
+        spyOn(isolatedScope.cp, '$update').and.callThrough();
+      });
 
       it('should request the correct contentProvider', () => {
         expect(isolatedScope.cp.name).toEqual(CPmock.CPlist[0].name);
@@ -95,9 +107,72 @@
       it('should update a contentProvider', () => {
         isolatedScope.cp.name = 'new name';
         isolatedScope.saveContentProvider(isolatedScope.cp);
-        httpBackend.flush();
-        expect(isolatedScope.cp.name).toEqual('done');
+        expect(isolatedScope.cp.$update).toHaveBeenCalled();
       });
     });
   });
+
+  describe('the contentProviderCdn directive', () => {
+    beforeEach(inject(($compile, $rootScope) => {
+      scope = $rootScope.$new();
+      element = angular.element('<content-provider-cdn></content-provider-cdn>');
+      $compile(element)(scope);
+      httpBackend.expectGET('/hpcapi/contentproviders/1/').respond(CPmock.CPlist[0]);
+      httpBackend.expectGET('/hpcapi/cdnprefixs/?contentProvider=1').respond([CPmock.CDNlist[0]]);
+      httpBackend.expectGET('/hpcapi/cdnprefixs/').respond(CPmock.CDNlist);
+      httpBackend.whenPOST('/hpcapi/cdnprefixs/').respond(CPmock.CDNlist[0]);
+      httpBackend.whenDELETE('/hpcapi/cdnprefixs/5/').respond();
+      scope.$digest();
+      httpBackend.flush();
+      isolatedScope = element.isolateScope().vm;
+    }));
+
+    it('should load associated CDN prefix', () => {
+      expect(isolatedScope.cp_prf.length).toBe(1);
+      expect(isolatedScope.prf.length).toBe(2);
+    });
+
+    it('should add a CDN Prefix', () => {
+      isolatedScope.addPrefix({prefix: 'test.io', defaultOriginServer: '/hpcapi/originservers/2/'});
+      httpBackend.flush();
+      expect(isolatedScope.cp_prf.length).toBe(2);
+    });
+
+    it('should remove a CDN Prefix', () => {
+      isolatedScope.removePrefix(isolatedScope.cp_prf[0]);
+      httpBackend.flush();
+      expect(isolatedScope.cp_prf.length).toBe(0);
+    });
+  });
+
+  describe('the contentProviderServer directive', () => {
+    beforeEach(inject(($compile, $rootScope) => {
+      scope = $rootScope.$new();
+      element = angular.element('<content-provider-server></content-provider-server>');
+      $compile(element)(scope);
+      httpBackend.expectGET('/hpcapi/contentproviders/1/').respond(CPmock.CPlist[0]);
+      httpBackend.expectGET('/hpcapi/originservers/?contentProvider=1').respond(CPmock.OSlist);
+      httpBackend.whenPOST('/hpcapi/originservers/').respond(CPmock.OSlist[0]);
+      httpBackend.whenDELETE('/hpcapi/originservers/8/').respond();
+      scope.$digest();
+      httpBackend.flush();
+      isolatedScope = element.isolateScope().vm;
+    }));
+
+    it('should load associated OriginServer', () => {
+      expect(isolatedScope.cp_os.length).toBe(4);
+    });
+
+    it('should add a OriginServer', () => {
+      isolatedScope.addOrigin({protocol: 'http', url: 'test.io'});
+      httpBackend.flush();
+      expect(isolatedScope.cp_os.length).toBe(5);
+    });
+
+    it('should remove a OriginServer', () => {
+      isolatedScope.removeOrigin(isolatedScope.cp_os[0]);
+      httpBackend.flush();
+      expect(isolatedScope.cp_os.length).toBe(3);
+    });
+  });
 });
diff --git a/xos/core/xoslib/spec/views/mocks/contentProvider.mock.js b/xos/core/xoslib/spec/views/mocks/contentProvider.mock.js
index 97ee31b..c0d1750 100644
--- a/xos/core/xoslib/spec/views/mocks/contentProvider.mock.js
+++ b/xos/core/xoslib/spec/views/mocks/contentProvider.mock.js
@@ -211,5 +211,479 @@
       'description':null,
       'enabled':true
     }
+  ],
+  CDNlist: [
+    {
+      'humanReadableName':'onlab.vicci.org',
+      'validators':{
+        'updated':[
+
+        ],
+        'contentProvider':[
+          'notBlank'
+        ],
+        'policed':[
+
+        ],
+        'created':[
+
+        ],
+        'deleted':[
+
+        ],
+        'description':[
+
+        ],
+        'enabled':[
+
+        ],
+        'cdn_prefix_id':[
+
+        ],
+        'lazy_blocked':[
+
+        ],
+        'backend_register':[
+          'notBlank'
+        ],
+        'write_protect':[
+
+        ],
+        'prefix':[
+          'notBlank'
+        ],
+        'defaultOriginServer':[
+
+        ],
+        'backend_status':[
+          'notBlank'
+        ],
+        'id':[
+
+        ],
+        'no_sync':[
+
+        ],
+        'enacted':[
+
+        ]
+      },
+      'id':5,
+      'created':'2015-10-26T13:09:44.343Z',
+      'updated':'2015-10-26T13:09:44.343Z',
+      'enacted':null,
+      'policed':null,
+      'backend_register':'{}',
+      'backend_status':'0 - Provisioning in progress',
+      'deleted':false,
+      'write_protect':false,
+      'lazy_blocked':false,
+      'no_sync':false,
+      'cdn_prefix_id':null,
+      'prefix':'onlab.vicci.org',
+      'contentProvider':'http://0.0.0.0:9000/hpcapi/contentproviders/1/',
+      'description':null,
+      'defaultOriginServer':'http://0.0.0.0:9000/hpcapi/originservers/2/',
+      'enabled':true
+    },
+    {
+      'humanReadableName':'downloads.onosproject.org',
+      'validators':{
+        'updated':[
+
+        ],
+        'contentProvider':[
+          'notBlank'
+        ],
+        'policed':[
+
+        ],
+        'created':[
+
+        ],
+        'deleted':[
+
+        ],
+        'description':[
+
+        ],
+        'enabled':[
+
+        ],
+        'cdn_prefix_id':[
+
+        ],
+        'lazy_blocked':[
+
+        ],
+        'backend_register':[
+          'notBlank'
+        ],
+        'write_protect':[
+
+        ],
+        'prefix':[
+          'notBlank'
+        ],
+        'defaultOriginServer':[
+
+        ],
+        'backend_status':[
+          'notBlank'
+        ],
+        'id':[
+
+        ],
+        'no_sync':[
+
+        ],
+        'enacted':[
+
+        ]
+      },
+      'id':1,
+      'created':'2015-10-26T13:09:44.196Z',
+      'updated':'2015-10-26T13:09:44.196Z',
+      'enacted':null,
+      'policed':null,
+      'backend_register':'{}',
+      'backend_status':'0 - Provisioning in progress',
+      'deleted':false,
+      'write_protect':false,
+      'lazy_blocked':false,
+      'no_sync':false,
+      'cdn_prefix_id':null,
+      'prefix':'downloads.onosproject.org',
+      'contentProvider':'http://0.0.0.0:9000/hpcapi/contentproviders/2/',
+      'description':null,
+      'defaultOriginServer':'http://0.0.0.0:9000/hpcapi/originservers/1/',
+      'enabled':true
+    }
+  ],
+  OSlist: [
+    {
+      'humanReadableName':'another.it',
+      'validators':{
+        'updated':[
+
+        ],
+        'contentProvider':[
+          'notBlank'
+        ],
+        'origin_server_id':[
+
+        ],
+        'policed':[
+
+        ],
+        'created':[
+
+        ],
+        'deleted':[
+
+        ],
+        'description':[
+
+        ],
+        'enabled':[
+
+        ],
+        'redirects':[
+
+        ],
+        'protocol':[
+          'notBlank'
+        ],
+        'lazy_blocked':[
+
+        ],
+        'backend_register':[
+          'notBlank'
+        ],
+        'write_protect':[
+
+        ],
+        'url':[
+          'notBlank'
+        ],
+        'authenticated':[
+
+        ],
+        'backend_status':[
+          'notBlank'
+        ],
+        'id':[
+
+        ],
+        'no_sync':[
+
+        ],
+        'enacted':[
+
+        ]
+      },
+      'id':8,
+      'created':'2015-10-26T13:40:36.878Z',
+      'updated':'2015-10-26T13:40:36.878Z',
+      'enacted':null,
+      'policed':null,
+      'backend_register':'{}',
+      'backend_status':'0 - Provisioning in progress',
+      'deleted':false,
+      'write_protect':false,
+      'lazy_blocked':false,
+      'no_sync':false,
+      'origin_server_id':null,
+      'url':'another.it',
+      'contentProvider':'http://0.0.0.0:9000/hpcapi/contentproviders/1/',
+      'authenticated':false,
+      'enabled':true,
+      'protocol':'http',
+      'redirects':true,
+      'description':null
+    },
+    {
+      'humanReadableName':'test.it',
+      'validators':{
+        'updated':[
+
+        ],
+        'contentProvider':[
+          'notBlank'
+        ],
+        'origin_server_id':[
+
+        ],
+        'policed':[
+
+        ],
+        'created':[
+
+        ],
+        'deleted':[
+
+        ],
+        'description':[
+
+        ],
+        'enabled':[
+
+        ],
+        'redirects':[
+
+        ],
+        'protocol':[
+          'notBlank'
+        ],
+        'lazy_blocked':[
+
+        ],
+        'backend_register':[
+          'notBlank'
+        ],
+        'write_protect':[
+
+        ],
+        'url':[
+          'notBlank'
+        ],
+        'authenticated':[
+
+        ],
+        'backend_status':[
+          'notBlank'
+        ],
+        'id':[
+
+        ],
+        'no_sync':[
+
+        ],
+        'enacted':[
+
+        ]
+      },
+      'id':7,
+      'created':'2015-10-26T13:36:42.567Z',
+      'updated':'2015-10-26T13:36:42.567Z',
+      'enacted':null,
+      'policed':null,
+      'backend_register':'{}',
+      'backend_status':'0 - Provisioning in progress',
+      'deleted':false,
+      'write_protect':false,
+      'lazy_blocked':false,
+      'no_sync':false,
+      'origin_server_id':null,
+      'url':'test.it',
+      'contentProvider':'http://0.0.0.0:9000/hpcapi/contentproviders/1/',
+      'authenticated':false,
+      'enabled':true,
+      'protocol':'http',
+      'redirects':true,
+      'description':null
+    },
+    {
+      'humanReadableName':'onlab.vicci.org',
+      'validators':{
+        'updated':[
+
+        ],
+        'contentProvider':[
+          'notBlank'
+        ],
+        'origin_server_id':[
+
+        ],
+        'policed':[
+
+        ],
+        'created':[
+
+        ],
+        'deleted':[
+
+        ],
+        'description':[
+
+        ],
+        'enabled':[
+
+        ],
+        'redirects':[
+
+        ],
+        'protocol':[
+          'notBlank'
+        ],
+        'lazy_blocked':[
+
+        ],
+        'backend_register':[
+          'notBlank'
+        ],
+        'write_protect':[
+
+        ],
+        'url':[
+          'notBlank'
+        ],
+        'authenticated':[
+
+        ],
+        'backend_status':[
+          'notBlank'
+        ],
+        'id':[
+
+        ],
+        'no_sync':[
+
+        ],
+        'enacted':[
+
+        ]
+      },
+      'id':2,
+      'created':'2015-10-26T13:09:44.286Z',
+      'updated':'2015-10-26T13:09:44.286Z',
+      'enacted':null,
+      'policed':null,
+      'backend_register':'{}',
+      'backend_status':'0 - Provisioning in progress',
+      'deleted':false,
+      'write_protect':false,
+      'lazy_blocked':false,
+      'no_sync':false,
+      'origin_server_id':null,
+      'url':'onlab.vicci.org',
+      'contentProvider':'http://0.0.0.0:9000/hpcapi/contentproviders/1/',
+      'authenticated':false,
+      'enabled':true,
+      'protocol':'HTTP',
+      'redirects':true,
+      'description':null
+    },
+    {
+      'humanReadableName':'downloads.onosproject.org',
+      'validators':{
+        'updated':[
+
+        ],
+        'contentProvider':[
+          'notBlank'
+        ],
+        'origin_server_id':[
+
+        ],
+        'policed':[
+
+        ],
+        'created':[
+
+        ],
+        'deleted':[
+
+        ],
+        'description':[
+
+        ],
+        'enabled':[
+
+        ],
+        'redirects':[
+
+        ],
+        'protocol':[
+          'notBlank'
+        ],
+        'lazy_blocked':[
+
+        ],
+        'backend_register':[
+          'notBlank'
+        ],
+        'write_protect':[
+
+        ],
+        'url':[
+          'notBlank'
+        ],
+        'authenticated':[
+
+        ],
+        'backend_status':[
+          'notBlank'
+        ],
+        'id':[
+
+        ],
+        'no_sync':[
+
+        ],
+        'enacted':[
+
+        ]
+      },
+      'id':1,
+      'created':'2015-10-26T13:09:44.182Z',
+      'updated':'2015-10-26T13:09:44.182Z',
+      'enacted':null,
+      'policed':null,
+      'backend_register':'{}',
+      'backend_status':'0 - Provisioning in progress',
+      'deleted':false,
+      'write_protect':false,
+      'lazy_blocked':false,
+      'no_sync':false,
+      'origin_server_id':null,
+      'url':'downloads.onosproject.org',
+      'contentProvider':'http://0.0.0.0:9000/hpcapi/contentproviders/1/',
+      'authenticated':false,
+      'enabled':true,
+      'protocol':'HTTP',
+      'redirects':true,
+      'description':null
+    }
   ]
 };
\ No newline at end of file
diff --git a/xos/core/xoslib/static/css/xosLib.css b/xos/core/xoslib/static/css/xosLib.css
new file mode 100644
index 0000000..d2f9349
--- /dev/null
+++ b/xos/core/xoslib/static/css/xosLib.css
@@ -0,0 +1,13 @@
+/* Style helpers for xoslib*/
+
+
+/* gives an element the same spacing as it is inside a well*/
+.margin-wells {
+  margin: 12px;
+}
+
+/* if inside a form set margin-top to align with inputs instead of labels*/
+/* TODO once scss use a form like form [class^=span] > label + &[class^=span] > .margin-wells to match only if label is set*/
+form button.margin-wells {
+  margin-top: 20px;
+}
\ No newline at end of file
diff --git a/xos/core/xoslib/static/js/xosContentProvider.js b/xos/core/xoslib/static/js/xosContentProvider.js
index dfac97b..0c21fe3 100644
--- a/xos/core/xoslib/static/js/xosContentProvider.js
+++ b/xos/core/xoslib/static/js/xosContentProvider.js
@@ -54,7 +54,7 @@
     }
   };
 })
-.service('ContentProvider', function($resource) {
+.service('ContentProvider', function($resource, $q, User) {
   return $resource('/hpcapi/contentproviders/:id/', {id: '@id'}, {
     'update': {method: 'PUT'}
   });
@@ -137,6 +137,10 @@
           };
         });
       }
+      else {
+        console.log('new');
+        _this.cp = new ContentProvider();
+      }
 
       ServiceProvider.query().$promise
       .then(function(sp) {
@@ -161,11 +165,11 @@
         else {
           isNew = true;
           cp.name = cp.humanReadableName;
-          p = new ContentProvider(cp).$save();
+          console.log('save');
+          p = cp.$save();
         }
 
         p.then(function(res) {
-          console.log('save done', res);
           _this.result = {
             status: 1,
             msg: 'Content Provider Saved'
@@ -188,6 +192,7 @@
   return{
     restrict: 'E',
     controllerAs: 'vm',
+    scope: {},
     templateUrl: '../../static/templates/contentProvider/cp_cdn_prefix.html',
     controller: function() {
       var _this = this;
@@ -263,10 +268,11 @@
   return{
     restrict: 'E',
     controllerAs: 'vm',
+    scope: {},
     templateUrl: '../../static/templates/contentProvider/cp_origin_server.html',
     controller: function() {
       this.pageName = 'server';
-      this.protocols = ['HTTP', 'RTMP', 'RTP', 'SHOUTcast'];
+      this.protocols = {'http': 'HTTP', 'rtmp': 'RTMP', 'rtp': 'RTP','shout': 'SHOUTcast'};
 
       var _this = this;
 
@@ -326,7 +332,7 @@
     }
   };
 })
-.directive('contentProviderUsers', function($routeParams, ContentProvider, User, lodash) {
+.directive('contentProviderUsers', function($routeParams, ContentProvider, User, lodash, $q) {
   return{
     restrict: 'E',
     controllerAs: 'vm',
@@ -339,7 +345,20 @@
       this.cp_users = [];
 
       if($routeParams.id) {
-        ContentProvider.get({id: $routeParams.id}).$promise
+        User.query().$promise
+        .then(function(users) {
+          _this.users = users;
+          return ContentProvider.get({id: $routeParams.id}).$promise;
+        })
+        .then(function(res) {
+          for(var i = 0; i < res.users.length; i++) {
+            var url = res.users[i];
+            var id = parseInt(url.substr(url.length - 2).replace('/',''));
+
+            res.users[i] = lodash.find(_this.users, {id: id});
+          }
+          return res;
+        })
         .then(function(cp) {
           _this.cp = cp;
         }).catch(function(e) {
@@ -350,22 +369,30 @@
         });
       }
 
-      User.query().$promise
-      .then(function(users) {
-        _this.users = users;
-      }).catch(function(e) {
-        _this.result = {
-          status: 0,
-          msg: e.data.detail
-        };
-      });
-
       this.addUserToCp = function(user) {
-        _this.cp_users.push(user);
+        _this.cp.users.push(user);
       };
 
       this.removeUserFromCp = function(user) {
-        lodash.remove(_this.cp_users, user);
+        lodash.remove(_this.cp.users, user);
+      };
+
+      this.saveContentProvider = function(cp) {
+
+        cp.$update()
+        .then(function() {
+          _this.result = {
+            status: 1,
+            msg: 'Content Provider Saved'
+          };
+
+        })
+        .catch(function(e) {
+          _this.result = {
+            status: 0,
+            msg: e.data.detail
+          };
+        });
       };
     }
   };
diff --git a/xos/core/xoslib/static/templates/contentProvider/cp_cdn_prefix.html b/xos/core/xoslib/static/templates/contentProvider/cp_cdn_prefix.html
index d989f90..7367f30 100644
--- a/xos/core/xoslib/static/templates/contentProvider/cp_cdn_prefix.html
+++ b/xos/core/xoslib/static/templates/contentProvider/cp_cdn_prefix.html
@@ -41,8 +41,8 @@
             <option ng-repeat="prf in vm.prf" ng-value="'/hpcapi/originservers/' + prf.id + '/'">{$ prf.humanReadableName $}</option>
           </select>
         </div>
-        <div class="span2">
-          <button class="btn btn-success pull-right">
+        <div class="span2 text-right">
+          <button class="btn btn-success margin-wells">
             <i class="icon icon-plus"></i>
           </button>
         </div>
diff --git a/xos/core/xoslib/static/templates/contentProvider/cp_list.html b/xos/core/xoslib/static/templates/contentProvider/cp_list.html
index ddb0aa7..3303564 100644
--- a/xos/core/xoslib/static/templates/contentProvider/cp_list.html
+++ b/xos/core/xoslib/static/templates/contentProvider/cp_list.html
@@ -19,7 +19,7 @@
     <td>
       {$ item.enabled $}
     </td>
-    <td>
+    <td class="text-right">
       <a ng-click="vm.deleteCp(item.id)" class="btn btn-danger"><i class="icon icon-remove"></i></a></td>
   </tr>
 </table>
diff --git a/xos/core/xoslib/static/templates/contentProvider/cp_origin_server.html b/xos/core/xoslib/static/templates/contentProvider/cp_origin_server.html
index b2e1881..ff77864 100644
--- a/xos/core/xoslib/static/templates/contentProvider/cp_origin_server.html
+++ b/xos/core/xoslib/static/templates/contentProvider/cp_origin_server.html
@@ -33,14 +33,14 @@
       <div class="row-fluid">
         <div class="span4">
           <label>Protocol</label>
-          <select ng-model="vm.new_os.protocol" ng-options="p for p in vm.protocols" style="max-width: 100%;"></select>
+          <select ng-model="vm.new_os.protocol" ng-options="k as v for (k,v) in vm.protocols" style="max-width: 100%;"></select>
         </div>
         <div class="span6">
           <label>Url</label>
           <input type="text" ng-model="vm.new_os.url" required>
         </div>
-        <div class="span2">
-          <button class="btn btn-success pull-right">
+        <div class="span2 text-right">
+          <button class="btn btn-success margin-wells">
             <i class="icon icon-plus"></i>
           </button>
         </div>
diff --git a/xos/core/xoslib/static/templates/contentProvider/cp_user.html b/xos/core/xoslib/static/templates/contentProvider/cp_user.html
index 435e656..a8d12f6 100644
--- a/xos/core/xoslib/static/templates/contentProvider/cp_user.html
+++ b/xos/core/xoslib/static/templates/contentProvider/cp_user.html
@@ -12,7 +12,7 @@
     <div ng-include="'../../static/templates/contentProvider/cp_side_nav.html'"></div>
   </div>
   <div class="span10">
-    <div ng-repeat="item in vm.cp_users" class="well">
+    <div ng-repeat="item in vm.cp.users" class="well">
       <div class="row-fluid">
         <div class="span3">
           {{item.firstname}}
@@ -31,8 +31,18 @@
       </div>
     </div>
     <hr>
-    <form>
-      <select ng-model="vm.user" ng-options="u as u.username for u in vm.users" ng-change="vm.addUserToCp(vm.user)"></select>
+    <form ng-submit="vm.saveContentProvider(vm.cp)">
+      <div class="row-fluid">
+        <div class="span8">
+          <label>Select user:</label>
+          <select ng-model="vm.user" ng-options="u as u.username for u in vm.users" ng-change="vm.addUserToCp(vm.user)"></select>
+        </div>  
+        <div class="span4 text-right">
+          <button class="btn btn-success margin-wells" disabled="disabled">
+            Save
+          </button>
+        </div>
+      </div>
     </form>
   </div>
 </div>
\ No newline at end of file