diff --git a/resources/frontend_client/app/admin/databases/databases.controllers.js b/resources/frontend_client/app/admin/databases/databases.controllers.js index fd9851cec222d1232d29310adb8b5f4b5aaccabc..2de59e00f54e6f092a9859201d4154d6fe7abe2f 100644 --- a/resources/frontend_client/app/admin/databases/databases.controllers.js +++ b/resources/frontend_client/app/admin/databases/databases.controllers.js @@ -5,6 +5,8 @@ var DatabasesControllers = angular.module('corvusadmin.databases.controllers', [ DatabasesControllers.controller('DatabaseList', ['$scope', 'Metabase', function($scope, Metabase) { + $scope.databases = []; + $scope.delete = function(databaseId) { if ($scope.databases) { @@ -20,27 +22,10 @@ DatabasesControllers.controller('DatabaseList', ['$scope', 'Metabase', function( } }; - $scope.$watch('currentOrg', function(org) { - if (org) { - $scope.databases = []; - - Metabase.db_list({ - 'orgId': org.id - }, function(databases) { - // if we are an org that 'inherits' lets only show our our own dbs in this view - if (org.inherits) { - var dm = _.filter(databases, function(database) { - return database.organization.id === org.id; - }); - $scope.databases = dm; - } else { - - $scope.databases = databases; - } - }, function(error) { - console.log('error getting database list', error); - }); - } + Metabase.db_list(function(databases) { + $scope.databases = databases; + }, function(error) { + console.log('error getting database list', error); }); }]); @@ -70,11 +55,10 @@ DatabasesControllers.controller('DatabaseEdit', ['$scope', '$routeParams', '$loc // create a new Database var create = function(database, details, redirectToDetail) { $scope.$broadcast("form:reset"); - database.org = $scope.currentOrg.id; database.details = $scope.ENGINES[database.engine].buildDetails(details); return Metabase.db_create(database).$promise.then(function(new_database) { if (redirectToDetail) { - $location.path('/' + $scope.currentOrg.slug + '/admin/databases/' + new_database.id); + $location.path('/admin/databases/' + new_database.id); } $scope.$broadcast("form:api-success", "Successfully created!"); $scope.$emit("database:created", new_database); diff --git a/resources/frontend_client/app/admin/databases/databases.module.js b/resources/frontend_client/app/admin/databases/databases.module.js index 671c76d7b805f0d798f8c767a4e3df7112239e69..46b0fc9d2235592a3d37145dd465a01d42518a8a 100644 --- a/resources/frontend_client/app/admin/databases/databases.module.js +++ b/resources/frontend_client/app/admin/databases/databases.module.js @@ -5,23 +5,23 @@ var AdminDatabases = angular.module('corvusadmin.databases', [ ]); AdminDatabases.config(['$routeProvider', function ($routeProvider) { - $routeProvider.when('/:orgSlug/admin/databases', { + $routeProvider.when('/admin/databases', { templateUrl: '/app/admin/databases/partials/database_list.html', controller: 'DatabaseList' }); - $routeProvider.when('/:orgSlug/admin/databases/create', { + $routeProvider.when('/admin/databases/create', { templateUrl: '/app/admin/databases/partials/database_edit.html', controller: 'DatabaseEdit' }); - $routeProvider.when('/:orgSlug/admin/databases/:databaseId', { - redirectTo: '/:orgSlug/admin/databases/:databaseId/tables' + $routeProvider.when('/admin/databases/:databaseId', { + redirectTo: '/admin/databases/:databaseId/tables' }); - $routeProvider.when('/:orgSlug/admin/databases/:databaseId/:mode', { + $routeProvider.when('/admin/databases/:databaseId/:mode', { templateUrl: '/app/admin/databases/partials/database_master_detail.html', controller: 'DatabaseMasterDetail' }); - $routeProvider.when('/:orgSlug/admin/databases/:databaseId/:mode/:tableId', { + $routeProvider.when('/admin/databases/:databaseId/:mode/:tableId', { templateUrl: '/app/admin/databases/partials/database_master_detail.html', controller: 'DatabaseMasterDetail' }); diff --git a/resources/frontend_client/app/admin/databases/partials/database_edit.html b/resources/frontend_client/app/admin/databases/partials/database_edit.html index 7d1409b2c7e47dacf3db5d40ea133c1f61a4a80c..c7c5f00f0b6907e9ebb07e18a0dd71f50f4b1f2c 100644 --- a/resources/frontend_client/app/admin/databases/partials/database_edit.html +++ b/resources/frontend_client/app/admin/databases/partials/database_edit.html @@ -1,6 +1,6 @@ <div class="wrapper"> <section class="Breadcrumbs"> - <a class="Breadcrumb Breadcrumb--path" cv-org-href="/admin/databases/">Databases</a> + <a class="Breadcrumb Breadcrumb--path" href="/admin/databases/">Databases</a> <mb-icon name="chevronright" class="Breadcrumb-divider" width="12px" height="12px"></mb-icon> <h2 class="Breadcrumb Breadcrumb--page" ng-if="!database.id">Add Database</h2> <h2 class="Breadcrumb Breadcrumb--page" ng-if="database.id">{{database.name}}</h2> diff --git a/resources/frontend_client/app/admin/databases/partials/database_list.html b/resources/frontend_client/app/admin/databases/partials/database_list.html index 48836b31722407d93e0888ea1e53256d151277cf..224c217c1785806b12d66aefb6a97fbf6459e534 100644 --- a/resources/frontend_client/app/admin/databases/partials/database_list.html +++ b/resources/frontend_client/app/admin/databases/partials/database_list.html @@ -1,6 +1,6 @@ <div class="wrapper"> <section class="PageHeader clearfix"> - <a class="Button Button--primary float-right" cv-org-href="/admin/databases/create">Add database</a> + <a class="Button Button--primary float-right" href="/admin/databases/create">Add database</a> <h2 class="PageTitle">Databases</h2> </section> @@ -22,7 +22,7 @@ </tr> <tr ng-repeat="database in databases"> <td> - <a class="text-bold link" cv-org-href="/admin/databases/{{database.id}}/tables">{{database.name}}</a> + <a class="text-bold link" href="/admin/databases/{{database.id}}/tables">{{database.name}}</a> </td> <td> {{database.engine}} diff --git a/resources/frontend_client/app/admin/databases/partials/database_master_detail.html b/resources/frontend_client/app/admin/databases/partials/database_master_detail.html index 54e17935cff6f7eac9d0b301ef84a6294e377b85..0a690a9ce81a280cec0e7145c195d7c4d34b444b 100644 --- a/resources/frontend_client/app/admin/databases/partials/database_master_detail.html +++ b/resources/frontend_client/app/admin/databases/partials/database_master_detail.html @@ -2,8 +2,8 @@ <section class="my3 clearfix"> <div class="py2 float-right"> <div class="Button-group Button-group--blue text-uppercase text-bold"> - <a class="Button AdminHoverItem" ng-class="{ 'Button--active': pane == 'tables' }" cv-org-href="/admin/databases/{{database.id}}/tables/{{table.id}}">Data</a> - <a class="Button AdminHoverItem" ng-class="{ 'Button--active': pane == 'settings' }" cv-org-href="/admin/databases/{{database.id}}/settings">Connection Details</a> + <a class="Button AdminHoverItem" ng-class="{ 'Button--active': pane == 'tables' }" href="/admin/databases/{{database.id}}/tables/{{table.id}}">Data</a> + <a class="Button AdminHoverItem" ng-class="{ 'Button--active': pane == 'settings' }" href="/admin/databases/{{database.id}}/settings">Connection Details</a> </div> </div> <h3 class="py2">{{database.name}}</h2> diff --git a/resources/frontend_client/app/admin/databases/partials/database_table_list.html b/resources/frontend_client/app/admin/databases/partials/database_table_list.html index cc40d317842fe7c556577be6c01ecc3dda398a0f..c2aa47c4963ca279ab787862003016dbbb8ddda6 100644 --- a/resources/frontend_client/app/admin/databases/partials/database_table_list.html +++ b/resources/frontend_client/app/admin/databases/partials/database_table_list.html @@ -9,7 +9,7 @@ <h3>Loading ...</h3> </li> <li class="DatabaseListItem AdminHoverItem rounded py1 px2 text-grey-4" ng-class="{ 'DatabaseListItem--active': t.id == table.id }" ng-repeat="t in tables | filter:tableSearchText"> - <a class="link link--nohover text-current" cv-org-href="/admin/databases/{{database.id}}/tables/{{t.id}}"> + <a class="link link--nohover text-current" href="/admin/databases/{{database.id}}/tables/{{t.id}}"> <h4>{{t.name}}</h4> </a> </li> diff --git a/resources/frontend_client/app/admin/organization/organization.controllers.js b/resources/frontend_client/app/admin/organization/organization.controllers.js deleted file mode 100644 index 16a722ef167bac9c4aafca825c3bd487125fb34d..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/admin/organization/organization.controllers.js +++ /dev/null @@ -1,39 +0,0 @@ -"use strict"; -/*global btoa*/ -/*global $*/ - -var OrganizationAdminControllers = angular.module('corvusadmin.organization.controllers', [ - 'corvus.services', - 'metabase.forms' -]); - -OrganizationAdminControllers.controller('OrganizationSettings', ['$scope', 'Organization', - function($scope, Organization) { - - $scope.save = function(organization) { - $scope.$broadcast("form:reset"); - - // use NULL for unset timezone - if (!organization.report_timezone) { - organization.report_timezone = null; - } - - Organization.update(organization, function (org) { - $scope.currentOrg = org; - $scope.$broadcast("form:api-success", "Successfully saved!"); - - // we need to trigger a refresh of $scope.user so that these changes propogate the UI - $scope.refreshCurrentUser(); - - }, function (error) { - $scope.$broadcast("form:api-error", error); - }); - }; - - Organization.form_input(function (result) { - $scope.form_input = result; - }, function (error) { - console.log('error getting form input', error); - }); - } -]); \ No newline at end of file diff --git a/resources/frontend_client/app/admin/organization/organization.module.js b/resources/frontend_client/app/admin/organization/organization.module.js deleted file mode 100644 index c7535a0f0b1ade6d14930dbef2f6afe62c2258b9..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/admin/organization/organization.module.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -var OrganizationAdmin = angular.module('corvusadmin.organization', [ - 'corvusadmin.organization.controllers' -]); - -OrganizationAdmin.config(['$routeProvider', function($routeProvider) { - $routeProvider.when('/:orgSlug/admin/org/', { - templateUrl: '/app/admin/organization/partials/org_settings.html', - controller: 'OrganizationSettings' - }); -}]); \ No newline at end of file diff --git a/resources/frontend_client/app/admin/organization/partials/org_settings.html b/resources/frontend_client/app/admin/organization/partials/org_settings.html deleted file mode 100644 index 2dcdc40bc3c394c07fb77bdc6c1f83ae120b487b..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/admin/organization/partials/org_settings.html +++ /dev/null @@ -1,51 +0,0 @@ -<div class="wrapper"> - <section class="PageHeader"> - <h2 class="PageTitle">Organization</h2> - </section> - - <section class="Grid Grid--gutters Grid--full Grid--2of3"> - <!-- form --> - <div class="Grid-cell Cell--2of3"> - <form class="Form-new bordered rounded shadowed" name="form" novalidate> - <div class="Form-field" mb-form-field="slug"> - <mb-form-label display-name="Slug" field-name="slug"></mb-form-label> - <input class="Form-input Form-offset full" name="slug" ng-model="currentOrg.slug" ng-disabled="true" /> - </div> - - <div class="Form-field" mb-form-field="name"> - <mb-form-label display-name="Name" field-name="name"></mb-form-label> - <input class="Form-input Form-offset full" name="name" placeholder="What is the name of your organization?" ng-model="currentOrg.name" required autofocus/> - <span class="Form-charm"></span> - </div> - - <div class="Form-field" mb-form-field="description"> - <mb-form-label display-name="Description" field-name="description"></mb-form-label> - <input class="Form-input Form-offset full" name="description" placeholder="What else should people know about this org?" ng-model="currentOrg.description" /> - <span class="Form-charm"></span> - </div> - - <div class="Form-field" mb-form-field="logo_url"> - <mb-form-label display-name="Logo url" field-name="logo_url"></mb-form-label> - <input class="Form-input Form-offset full" name="logo_url" placeholder="Link to an image of your logo" ng-model="currentOrg.logo_url" /> - <span class="Form-charm"></span> - </div> - - <div class="Form-field" mb-form-field="report_timezone"> - <mb-form-label display-name="Reporting timezone" field-name="report_timezone"></mb-form-label> - <label class="Select Form-offset"> - <select ng-model="currentOrg.report_timezone" ng-options="tz for tz in form_input['timezones']"> - <option value="">--- default timezone ---</option> - </select> - </label> - </div> - - <div class="Form-actions"> - <button class="Button" ng-class="{'Button--primary': form.$valid}" ng-click="save(currentOrg)" ng-disabled="!form.$valid"> - Save - </button> - <mb-form-message form="form"></mb-form-message> - </div> - </form> - </div> - </section> -</div> diff --git a/resources/frontend_client/app/admin/people/partials/people.html b/resources/frontend_client/app/admin/people/partials/people.html index bb67a2ff20167f07894a80885a5168804e66256f..bef4b0ea2da779d1c27efa271107f54946dfc62b 100644 --- a/resources/frontend_client/app/admin/people/partials/people.html +++ b/resources/frontend_client/app/admin/people/partials/people.html @@ -1,6 +1,6 @@ <div class="wrapper"> <section class="PageHeader clearfix"> - <a class="Button Button--primary float-right" cv-org-href="/admin/people/add">Add person</a> + <a class="Button Button--primary float-right" href="/admin/people/add">Add person</a> <h2 class="PageTitle">People</h2> </section> @@ -13,17 +13,17 @@ </tr> </thead> <tbody> - <tr ng-repeat="perm in people"> + <tr ng-repeat="user in people"> <td> - {{perm.user.common_name}} <span ng-show="perm.admin" class="AdminBadge">Admin</span> + {{user.common_name}} <span ng-show="user.is_superuser" class="AdminBadge">Admin</span> </td> <td class="Table-actions"> - <button class="Button" ng-click="toggle(perm.user.id)"> - <span ng-show="perm.admin">Revoke</span> - <span ng-show="!perm.admin">Grant</span> + <button class="Button" ng-click="toggle(user.id)"> + <span ng-show="user.is_superuser">Revoke</span> + <span ng-show="!user.is_superuser">Grant</span> admin </button> - <button class="Button Button--danger" ng-click="delete(perm.user.id)" delete-confirm>Remove</button> + <button class="Button Button--danger" ng-click="delete(user.id)" delete-confirm>Remove</button> </td> </tr> </tbody> diff --git a/resources/frontend_client/app/admin/people/partials/people_add.html b/resources/frontend_client/app/admin/people/partials/people_add.html index 816694aa0d768ef08dbabb86032820acd919c901..d4818c349204144eef78f0bd64f639c003dbdf6a 100644 --- a/resources/frontend_client/app/admin/people/partials/people_add.html +++ b/resources/frontend_client/app/admin/people/partials/people_add.html @@ -1,6 +1,6 @@ <div class="wrapper"> <section class="Breadcrumbs"> - <a class="Breadcrumb Breadcrumb--path" cv-org-href="/admin/people/">People</a> + <a class="Breadcrumb Breadcrumb--path" href="/admin/people/">People</a> <mb-icon name="chevronright" class="Breadcrumb-divider" width="12px" height="12px"></mb-icon> <h2 class="Breadcrumb Breadcrumb--page">Add person</h2> </section> diff --git a/resources/frontend_client/app/admin/people/people.controllers.js b/resources/frontend_client/app/admin/people/people.controllers.js index b3cab57624ae78be91b5f6973c6b6ab232adf4b0..1bbabc17160b53576300b7800cd3beb1eb25a591 100644 --- a/resources/frontend_client/app/admin/people/people.controllers.js +++ b/resources/frontend_client/app/admin/people/people.controllers.js @@ -6,103 +6,93 @@ var PeopleControllers = angular.module('corvusadmin.people.controllers', [ 'metabase.forms' ]); -PeopleControllers.controller('PeopleList', ['$scope', 'Organization', - function($scope, Organization) { +PeopleControllers.controller('PeopleList', ['$scope', 'User', + function($scope, User) { - // grant admin permission for a given user - var grant = function(userId) { - Organization.member_update({ - orgId: $scope.currentOrg.id, - userId: userId, - admin: true - }, function (result) { - $scope.people.forEach(function (perm) { - if (perm.user.id === userId) { - perm.admin = true; + // grant superuser permission for a given user + var grant = function(user) { + user.is_superuser = true; + + User.update(user, function (result) { + $scope.people.forEach(function (u) { + if (u.id === user.id) { + u.is_superuser = true; } }); }, function (error) { console.log('error', error); - $scope.alertError('failed to grant admin to user'); + $scope.alertError('failed to grant superuser to user'); }); }; - // revoke admin permission for a given user - var revoke = function(userId) { - Organization.member_update({ - orgId: $scope.currentOrg.id, - userId: userId, - admin: false - }, function (result) { - $scope.people.forEach(function (perm) { - if (perm.user.id === userId) { - perm.admin = false; + // revoke superuser permission for a given user + var revoke = function(user) { + user.is_superuser = false; + + User.update(user, function (result) { + $scope.people.forEach(function (u) { + if (u.id === user.id) { + u.is_superuser = false; } }); }, function (error) { console.log('error', error); - $scope.alertError('failed to revoke admin from user'); + $scope.alertError('failed to revoke superuser from user'); }); }; - // toggle admin permission for a given user + // toggle superuser permission for a given user $scope.toggle = function(userId) { - $scope.people.forEach(function (perm) { - if (perm.user.id === userId) { - if (perm.admin) { - revoke(userId); + $scope.people.forEach(function (user) { + if (user.id === userId) { + if (user.is_superuser) { + revoke(user); } else { - grant(userId); + grant(user); } } }); }; - // completely remove a given user (from the current org) + // completely remove a given user + // TODO: we need this api function now $scope.delete = function(userId) { - Organization.member_remove({ - orgId: $scope.currentOrg.id, + User.delete({ userId: userId }, function(result) { for (var i = 0; i < $scope.people.length; i++) { - if($scope.people[i].user.id === userId) { + if($scope.people[i].id === userId) { $scope.people.splice(i, 1); break; } } }, function (error) { console.log('error', error); - $scope.alertError('failed to remove user from org'); + $scope.alertError('failed to remove user'); }); }; - $scope.$watch('currentOrg', function (org) { - if (!org) return; - - Organization.members({ - 'orgId': org.id - }, function (result) { - $scope.people = result; - }, function (error) { - console.log('error', error); - }); - + User.list(function (result) { + $scope.people = result; + }, function (error) { + console.log('error', error); }); } ]); -PeopleControllers.controller('PeopleAdd', ['$scope', '$location', 'Organization', - function($scope, $location, Organization) { +PeopleControllers.controller('PeopleAdd', ['$scope', '$location', 'User', + function($scope, $location, User) { $scope.save = function(newUser) { $scope.$broadcast("form:reset"); - newUser.orgId = $scope.currentOrg.id; - newUser.admin = false; - Organization.member_create(newUser, function (result) { + newUser.is_superuser = false; + + // TODO: we need this function!! + User.create(newUser, function (result) { // just go back to people listing page for now - $location.path('/'+$scope.currentOrg.slug+'/admin/people/'); + $location.path('/admin/people/'); }, function (error) { $scope.$broadcast("form:api-error", error); }); diff --git a/resources/frontend_client/app/admin/people/people.module.js b/resources/frontend_client/app/admin/people/people.module.js index 640883b67fdab1a3c5cabed39cc8bde63458fe3c..ab27a28e1674655442120644c5c076579117b21c 100644 --- a/resources/frontend_client/app/admin/people/people.module.js +++ b/resources/frontend_client/app/admin/people/people.module.js @@ -1,16 +1,16 @@ 'use strict'; -var Organization = angular.module('corvusadmin.people', [ +var AdminPeople = angular.module('corvusadmin.people', [ 'corvusadmin.people.controllers' ]); -Organization.config(['$routeProvider', function ($routeProvider) { - $routeProvider.when('/:orgSlug/admin/people/', { +AdminPeople.config(['$routeProvider', function ($routeProvider) { + $routeProvider.when('/admin/people/', { templateUrl: '/app/admin/people/partials/people.html', controller: 'PeopleList' }); - $routeProvider.when('/:orgSlug/admin/people/add', { + $routeProvider.when('/admin/people/add', { templateUrl: '/app/admin/people/partials/people_add.html', controller: 'PeopleAdd' }); diff --git a/resources/frontend_client/app/superadmin/settings/partials/settings.html b/resources/frontend_client/app/admin/settings/partials/settings.html similarity index 100% rename from resources/frontend_client/app/superadmin/settings/partials/settings.html rename to resources/frontend_client/app/admin/settings/partials/settings.html diff --git a/resources/frontend_client/app/superadmin/settings/settings.controllers.js b/resources/frontend_client/app/admin/settings/settings.controllers.js similarity index 91% rename from resources/frontend_client/app/superadmin/settings/settings.controllers.js rename to resources/frontend_client/app/admin/settings/settings.controllers.js index 25f48ac73a43cf5487bdbdad9b4c486dd3fc6087..a5e903302d66fab4a4c98c6773adf8e3d72d7769 100644 --- a/resources/frontend_client/app/superadmin/settings/settings.controllers.js +++ b/resources/frontend_client/app/admin/settings/settings.controllers.js @@ -1,7 +1,7 @@ 'use strict'; /*global _*/ -var SettingsAdminControllers = angular.module('superadmin.settings.controllers', ['superadmin.settings.services']); +var SettingsAdminControllers = angular.module('corvusadmin.settings.controllers', ['corvusadmin.settings.services']); SettingsAdminControllers.controller('SettingsAdminController', ['$scope', 'SettingsAdminServices', function($scope, SettingsAdminServices) { diff --git a/resources/frontend_client/app/admin/settings/settings.module.js b/resources/frontend_client/app/admin/settings/settings.module.js new file mode 100644 index 0000000000000000000000000000000000000000..b07e4eac24b9c6b03b91559bee0e149c28a1b994 --- /dev/null +++ b/resources/frontend_client/app/admin/settings/settings.module.js @@ -0,0 +1,13 @@ +'use strict'; + +var SettingsAdmin = angular.module('corvusadmin.settings', [ + 'corvusadmin.settings.controllers', + 'corvusadmin.settings.services' +]); + +SettingsAdmin.config(['$routeProvider', function($routeProvider) { + $routeProvider.when('/admin/settings/', { + templateUrl: '/app/admin/settings/partials/settings.html', + controller: 'SettingsAdminController' + }); +}]); diff --git a/resources/frontend_client/app/superadmin/settings/settings.services.js b/resources/frontend_client/app/admin/settings/settings.services.js similarity index 62% rename from resources/frontend_client/app/superadmin/settings/settings.services.js rename to resources/frontend_client/app/admin/settings/settings.services.js index cfa200ad2e48001a91b7919a4a9d5b686ea602df..6998ab0781ff955e9bac5e29e81851f9bd3d684d 100644 --- a/resources/frontend_client/app/superadmin/settings/settings.services.js +++ b/resources/frontend_client/app/admin/settings/settings.services.js @@ -1,6 +1,6 @@ 'use strict'; -var SettingsAdminServices = angular.module('superadmin.settings.services', ['ngResource']); +var SettingsAdminServices = angular.module('corvusadmin.settings.services', ['ngResource']); SettingsAdminServices.factory('SettingsAdminServices', ['$resource', function($resource) { return $resource('/api/setting', {}, { @@ -13,12 +13,18 @@ SettingsAdminServices.factory('SettingsAdminServices', ['$resource', function($r // POST endpoint handles create + update in this case put: { url: '/api/setting/:key', - method: 'PUT' + method: 'PUT', + params: { + key: '@key' + } }, delete: { url: '/api/setting/:key', - method: 'DELETE' + method: 'DELETE', + params: { + key: '@key' + } } }); }]); diff --git a/resources/frontend_client/app/app.js b/resources/frontend_client/app/app.js index f9402fa78bcc4e10cc0683a1194b7ed1b8de0524..af4052a2692a5d04c3ccfc698d389334693d4928 100644 --- a/resources/frontend_client/app/app.js +++ b/resources/frontend_client/app/app.js @@ -25,11 +25,9 @@ var Corvus = angular.module('corvus', [ 'corvus.reserve', 'corvus.user', 'corvus.setup', - 'corvusadmin.organization', 'corvusadmin.databases', 'corvusadmin.people', - 'superadmin.settings', - 'superadmin.organization' + 'corvusadmin.settings' ]); Corvus.config(['$routeProvider', '$locationProvider', function($routeProvider, $locationProvider) { $locationProvider.html5Mode({ @@ -58,31 +56,12 @@ Corvus.config(['$routeProvider', '$locationProvider', function($routeProvider, $ } }); - $routeProvider.when('/superadmin/', { + $routeProvider.when('/admin/', { redirectTo: function(routeParams, path, search) { - return '/superadmin/settings/'; + return '/admin/settings'; } }); - // TODO: we need actual homepages for orgs! - $routeProvider.when('/:orgSlug/', { - redirectTo: function(routeParams, path, search) { - return '/' + routeParams.orgSlug + '/dash/'; - } - }); - - $routeProvider.when('/:orgSlug/admin/', { - redirectTo: function(routeParams, path, search) { - return '/' + routeParams.orgSlug + '/admin/org/'; - } - }); - - // admin routes - $routeProvider.when('/:orgSlug/admin/test_login_form', { - templateUrl: '/app/admin/test_login_form.html', - controller: 'TestLoginForm' - }); - // TODO: we need an appropriate homepage or something to show in this situation $routeProvider.otherwise({ redirectTo: '/user/edit_current' diff --git a/resources/frontend_client/app/card/card.controllers.js b/resources/frontend_client/app/card/card.controllers.js index ddb787d92e49bfa336a152bd95f9c3f664f75b48..2d44aff654d1da7288a5eb35ddd30acd24b9d27d 100644 --- a/resources/frontend_client/app/card/card.controllers.js +++ b/resources/frontend_client/app/card/card.controllers.js @@ -31,17 +31,12 @@ CardControllers.controller('CardList', ['$scope', '$location', 'Card', function( $scope.filter = function(filterMode) { $scope.filterMode = filterMode; - $scope.$watch('currentOrg', function(org) { - if (!org) return; - - Card.list({ - 'orgId': org.id, - 'filterMode': filterMode - }, function(cards) { - $scope.cards = cards; - }, function(error) { - console.log('error getting cards list', error); - }); + Card.list({ + 'filterMode': filterMode + }, function(cards) { + $scope.cards = cards; + }, function(error) { + console.log('error getting cards list', error); }); }; @@ -126,7 +121,7 @@ CardControllers.controller('CardDetail', [ cardJson = JSON.stringify(card); // for new cards we redirect the user - $location.path('/' + $scope.currentOrg.slug + '/card/' + newCard.id); + $location.path('/card/' + newCard.id); }, notifyCardUpdatedFn: function(updatedCard) { cardJson = JSON.stringify(card); @@ -334,7 +329,6 @@ CardControllers.controller('CardDetail', [ }, function (result) { if (cloning) { result.id = undefined; // since it's a new card - result.organization = $scope.currentOrg.id; result.carddirty = true; // so it cand be saved right away } @@ -367,7 +361,6 @@ CardControllers.controller('CardDetail', [ } else { // starting a new card - card.organization = $scope.currentOrg.id; // this is just an easy way to ensure defaults are all setup headerModel.setQueryModeFn("query"); @@ -397,32 +390,21 @@ CardControllers.controller('CardDetail', [ React.unmountComponentAtNode(document.getElementById('react_qb_viz')); }); - // TODO: we should get database list first, then do rest of setup - // because without databases this UI is meaningless - $scope.$watch('currentOrg', function (org) { - // we need org always, so we just won't do anything if we don't have one - if (!org) {return;} - - // TODO: while we wait for the databases list we should put something on screen - - // grab our database list, then handle the rest - Metabase.db_list({ - 'orgId': org.id - }, function (dbs) { - databases = dbs; - - if (dbs.length < 1) { - // TODO: some indication that setting up a db is required - return; - } + // TODO: while we wait for the databases list we should put something on screen + // grab our database list, then handle the rest + Metabase.db_list(function (dbs) { + databases = dbs; - // finish initializing our page and render - initAndRender(); + if (dbs.length < 1) { + // TODO: some indication that setting up a db is required + return; + } - }, function (error) { - console.log('error getting database list', error); - }); + // finish initializing our page and render + initAndRender(); + }, function (error) { + console.log('error getting database list', error); }); } ]); diff --git a/resources/frontend_client/app/card/card.module.js b/resources/frontend_client/app/card/card.module.js index 88ec2489b3f114c880c8c9ee87216047d8a4c0a5..d9959e102f0ebe97008520e61a01a5f4726e0278 100644 --- a/resources/frontend_client/app/card/card.module.js +++ b/resources/frontend_client/app/card/card.module.js @@ -14,15 +14,15 @@ var Card = angular.module('corvus.card', [ ]); Card.config(['$routeProvider', function($routeProvider) { - $routeProvider.when('/:orgSlug/card/', { + $routeProvider.when('/card/', { templateUrl: '/app/card/partials/card_list.html', controller: 'CardList' }); - $routeProvider.when('/:orgSlug/card/create/', { + $routeProvider.when('/card/create/', { templateUrl: '/app/card/partials/card_detail.html', controller: 'CardDetail' }); - $routeProvider.when('/:orgSlug/card/:cardId', { + $routeProvider.when('/card/:cardId', { templateUrl: '/app/card/partials/card_detail.html', controller: 'CardDetail' }); diff --git a/resources/frontend_client/app/card/partials/_card.html b/resources/frontend_client/app/card/partials/_card.html index 96a28eb6b85d389e84b9fe6df349878db0b22a35..6db63f12df29648f92672f2c5b4b87157cc696aa 100644 --- a/resources/frontend_client/app/card/partials/_card.html +++ b/resources/frontend_client/app/card/partials/_card.html @@ -8,14 +8,14 @@ <h3>This card contains no data.</h3> <span class="text-grey-3">Your filters may be too narrow.</span> </div> - <a class="link" cv-org-href="/card/{{card.id}}/modify">Try removing some filters</a> + <a class="link" href="/card/{{card.id}}/modify">Try removing some filters</a> </div> <div ng-if="sourceTableEmpty"> - The source dataset <a cv-org-href="/explore/table/{{sourceTable.id}}">{{sourceTable.name}}</a> is empty + The source dataset <a href="/explore/table/{{sourceTable.id}}">{{sourceTable.name}}</a> is empty </div> <div class="py1" ng-if="card.dataset_query.type == 'result'"> The SQL query backing this card returned no records.<br/> - <a cv-org-href="/admin/query/{{card.dataset_query.result.query_id}}/modify">Review your query</a> to ensure that your datasets are not empty and that your WHERE clause is not too narrow. + <a href="/admin/query/{{card.dataset_query.result.query_id}}/modify">Review your query</a> to ensure that your datasets are not empty and that your WHERE clause is not too narrow. </div> </div> </div> @@ -27,13 +27,13 @@ The SQL query backing this card returned the following error: <span class="block text-italic text-grey-4 my1">"{{sqlError}}"</span> <div ng-if="card.dataset_query.type == 'result'"> - <a class="link block my4" cv-org-href="/admin/query/{{card.dataset_query.result.query_id}}/modify">Fix Query</a> + <a class="link block my4" href="/admin/query/{{card.dataset_query.result.query_id}}/modify">Fix Query</a> </div> </div> <div class="py1" ng-if="missingDataset"> The dataset "{{missingDataset}}" backing this card is no longer available <div ng-if="card.dataset_query.database"> - <a cv-org-href="/explore/db/{{card.dataset_query.database}}">Explore</a> + <a href="/explore/db/{{card.dataset_query.database}}">Explore</a> </div> </div> </div> @@ -96,7 +96,7 @@ </div> <div class="Card-footing py1 px2 clearfix" id="{{chartId + '_footing'}}"> - <a class="Card-dataSource float-right" ng-if="dataset_info.title" cv-org-href="{{dataset_info.link}}">{{dataset_info.title}}</a> + <a class="Card-dataSource float-right" ng-if="dataset_info.title" href="{{dataset_info.link}}">{{dataset_info.title}}</a> <div class="py1" ng-if="card.creator && ((card.creator.last_name && card.creator.first_name) || card.creator.email)"> By: <span ng-if="card.creator.first_name && card.creator.last_name">{{card.creator.first_name}} {{card.creator.last_name}}</span> diff --git a/resources/frontend_client/app/card/partials/_card_heading.html b/resources/frontend_client/app/card/partials/_card_heading.html index 4a0355711918fcefcd2f5253024982295ce8cd0b..920fa56782529b833232ae1dcb8ad24445cd4bb0 100644 --- a/resources/frontend_client/app/card/partials/_card_heading.html +++ b/resources/frontend_client/app/card/partials/_card_heading.html @@ -12,7 +12,7 @@ <li> <a class="block link py1 px1" href="#" ng-if="!cardSettings || cardSettings.allowAddToDash" cv-add-to-dashboard-modal card="card">Add to Dashboard</a> <li> - <a class="block link py1 px1" cv-org-href="/card/{{card.id}}" ng-if="card.can_write">Modify Card</a> + <a class="block link py1 px1" href="/card/{{card.id}}" ng-if="card.can_write">Modify Card</a> </li> <li> <a class="block link py1 px1" href="#" ng-if="!cardSettings || cardSettings.allowSend" cv-send-card-modal card="card">Send</a> @@ -26,7 +26,7 @@ </div> <h3 class="text-normal my1"> <a class="Card-title link" href="#" ng-if="(!cardSettings || cardSettings.allowTitleEdits) && card.can_write" editable-text="card.name" onaftersave="inlineSave(card)">{{card.name}}</a> - <a class="Card-title link" cv-org-href="/card/{{card.id}}" ng-if="(!cardSettings || !cardSettings.allowTitleEdits)">{{card.name}}</a> + <a class="Card-title link" href="/card/{{card.id}}" ng-if="(!cardSettings || !cardSettings.allowTitleEdits)">{{card.name}}</a> </h3> </div> </div> diff --git a/resources/frontend_client/app/card/partials/card_list.html b/resources/frontend_client/app/card/partials/card_list.html index 958eaccf8922a630e8f1015ef18765c0774ca5a8..b40c63973c32bedebe64da229ce5c9e53a1e5beb 100644 --- a/resources/frontend_client/app/card/partials/card_list.html +++ b/resources/frontend_client/app/card/partials/card_list.html @@ -2,7 +2,7 @@ <div class="col col-sm-12"> <div class="py2 clearfix"> <div class="float-right"> - <a class="Button Button--primary" cv-org-href="/card/create">Create Card</a> + <a class="Button Button--primary" href="/card/create">Create Card</a> </div> <h2>Cards</h2> </div> @@ -29,11 +29,11 @@ <li class="List-item List-section" ng-repeat="card in cards | filter:searchFilter "> <div class="float-right my2"> <span class="List-timeStamp">{{card.updated_at | date : 'MMM d, y, hh:mm a'}}</span> - <a class="Button mx2" cv-org-href="{{'/card/create?clone=' + card.id}}">Clone</a> + <a class="Button mx2" href="{{'/card/create?clone=' + card.id}}">Clone</a> <a class="Button Button--remove" href="#" ng-click="deleteCard(card.id)" delete-confirm>Delete</a> </div> <h3> - <a class="text-normal link" cv-org-href="/card/{{card.id}}">{{card.name}}</a> + <a class="text-normal link" href="/card/{{card.id}}">{{card.name}}</a> </h3> <h5>Created by: {{card.creator.common_name}}</h5> </li> diff --git a/resources/frontend_client/app/components/core_nav/core_nav.js b/resources/frontend_client/app/components/core_nav/core_nav.js index dcabb811db03c7682dd903329575d2517421d913..058f2e436e63d8d389511124cc705302185463e1 100644 --- a/resources/frontend_client/app/components/core_nav/core_nav.js +++ b/resources/frontend_client/app/components/core_nav/core_nav.js @@ -5,7 +5,7 @@ angular.module('corvus.components').directive('selectableNavItem', ['$location', attrs.$observe('href', function (value) { if (!value) return; - var path = value.substring(0).split('/')[2], + var path = value.substring(0).split('/')[1], activeClass = 'is--selected'; // hijack location into our local scope so it can be watched @@ -13,7 +13,7 @@ angular.module('corvus.components').directive('selectableNavItem', ['$location', scope.$watch('location.path()', function(newPath) { // grab the root name of the path - var root = newPath.substring(0).split('/')[2]; + var root = newPath.substring(0).split('/')[1]; if (path == root) { element.addClass(activeClass); } else { diff --git a/resources/frontend_client/app/controllers.js b/resources/frontend_client/app/controllers.js index 72b94b45f667ca725c59814520e797728ace4115..a018e9e6d649f008bd5563b643c6d5948fdaf08f 100644 --- a/resources/frontend_client/app/controllers.js +++ b/resources/frontend_client/app/controllers.js @@ -10,46 +10,31 @@ var CorvusControllers = angular.module('corvus.controllers', ['corvus.services', CorvusControllers.controller('Corvus', ['$scope', '$location', 'CorvusCore', 'CorvusAlert', 'AppState', function($scope, $location, CorvusCore, CorvusAlert, AppState) { var clearState = function() { + $scope.siteName = undefined; $scope.user = undefined; - $scope.userIsAdmin = false; $scope.userIsSuperuser = false; - $scope.currentOrgSlug = undefined; - $scope.currentOrg = undefined; }; // make our utilities object available throughout the application $scope.utils = CorvusCore; // current User - // TODO: can we directly bind to Appstate.model? $scope.user = undefined; - $scope.userMemberOf = undefined; - $scope.userAdminOf = undefined; - $scope.userIsAdmin = false; $scope.userIsSuperuser = false; - // current Organization - // TODO: can we directly bind to Appstate.model? - $scope.currentOrgSlug = undefined; - $scope.currentOrg = undefined; - $scope.alerts = CorvusAlert.alerts; + $scope.$on("appstate:site-settings", function(event, settings) { + // change in global settings + $scope.siteName = settings['site-name'].value; + }); + $scope.$on("appstate:user", function(event, user) { // change in current user $scope.user = user; - $scope.userMemberOf = user.memberOf(); - $scope.userAdminOf = user.adminOf(); $scope.userIsSuperuser = user.is_superuser; }); - $scope.$on("appstate:organization", function(event, org) { - // change in current organization - $scope.currentOrgSlug = org.slug; - $scope.currentOrg = org; - $scope.userIsAdmin = $scope.user.isAdmin(org.slug); - }); - $scope.$on("appstate:logout", function(event, user) { clearState(); }); @@ -66,14 +51,6 @@ CorvusControllers.controller('Corvus', ['$scope', '$location', 'CorvusCore', 'Co CorvusAlert.alertError(message); }; - $scope.changeCurrOrg = function(orgSlug, admin) { - if (admin) { - $location.path('/' + orgSlug + '/admin/'); - } else { - $location.path('/' + orgSlug + '/'); - } - }; - $scope.refreshCurrentUser = function() { AppState.refreshCurrentUser(); }; @@ -88,25 +65,7 @@ CorvusControllers.controller('Homepage', ['$scope', '$location', 'ipCookie', 'Ap if (AppState.model.currentUser) { var currentUser = AppState.model.currentUser; - // We have a logged-in user, so send them somewhere sensible - var currentOrgFromCookie = ipCookie('metabase.CURRENT_ORG'); - - if (AppState.model.currentOrgSlug) { - // we know their current org - $location.path('/' + AppState.model.currentOrgSlug + '/'); - - } else if (currentOrgFromCookie && currentUser.isMember(currentOrgFromCookie)) { - // cookie is telling us their last current org - $location.path('/' + currentOrgFromCookie + '/'); - - } else if (currentUser.memberOf().length > 0) { - // no other indicator, so simply take the first org they have permissions on - $location.path('/' + currentUser.memberOf()[0].slug + '/'); - - } else { - // user doesn't have perms on any orgs, so they go somewhere neutral - $location.path('/user/edit_current'); - } + $location.path('/dash/'); } else { // User is not logged-in, so always send them to login page $location.path('/auth/login'); @@ -131,15 +90,12 @@ CorvusControllers.controller('Nav', ['$scope', '$routeParams', '$location', 'App var setNavContext = function(context) { switch (context) { - case "site-admin": - $scope.nav = 'superadmin'; + case "admin": + $scope.nav = 'admin'; break; case "setup": $scope.nav = 'setup'; break; - case "org-admin": - $scope.nav = 'admin'; - break; default: $scope.nav = 'main'; } diff --git a/resources/frontend_client/app/dashboard/dashboard.controllers.js b/resources/frontend_client/app/dashboard/dashboard.controllers.js index 17c1548c8ee77233646858bd2f0d1c9929c44612..a912bc8eb0fa208f25f3373c83490b3bb03438f8 100644 --- a/resources/frontend_client/app/dashboard/dashboard.controllers.js +++ b/resources/frontend_client/app/dashboard/dashboard.controllers.js @@ -33,20 +33,15 @@ DashboardControllers.controller('DashList', ['$scope', '$location', 'Dashboard', $scope.filter = function(filterMode) { $scope.filterMode = filterMode; - $scope.$watch('currentOrg', function (org) { - if (!org) return; - - Dashboard.list({ - 'orgId': org.id, - 'filterMode': filterMode - }, function (dashes) { - $scope.dashboards = dashes; - - sort = undefined; - $scope.sort(sort, false); - }, function (error) { - console.log('error getting dahsboards list', error); - }); + Dashboard.list({ + 'filterMode': filterMode + }, function (dashes) { + $scope.dashboards = dashes; + + sort = undefined; + $scope.sort(sort, false); + }, function (error) { + console.log('error getting dahsboards list', error); }); }; @@ -69,10 +64,6 @@ DashboardControllers.controller('DashList', ['$scope', '$location', 'Dashboard', b = new Date(b.updated_at); return a > b ? -1 : a < b ? 1 : 0; }); - } else if ('org' == sortMode) { - $scope.dashboards.sort(function(a, b) { - return a.organization.name.localeCompare(b.organization.name); - }); } else if ('owner' == sortMode) { $scope.dashboards.sort(function(a, b) { return a.creator.email.localeCompare(b.creator.email); @@ -123,7 +114,7 @@ DashboardControllers.controller('DashListForCard', ['$scope', '$routeParams', '$ }); }]); -DashboardControllers.controller('DashDetail', ['$scope', '$routeParams', '$location', 'Organization', 'Dashboard', 'DashCard', function($scope, $routeParams, $location, Organization, Dashboard, DashCard) { +DashboardControllers.controller('DashDetail', ['$scope', '$routeParams', '$location', 'Dashboard', 'DashCard', function($scope, $routeParams, $location, Dashboard, DashCard) { // $scope.dashboard: single Card being displayed/edited // $scope.error: any relevant error message to be displayed @@ -217,11 +208,10 @@ DashboardControllers.controller('DashDetail', ['$scope', '$routeParams', '$locat } $scope.create = function(dashboard) { - dashboard.organization = $scope.currentOrg.id; Dashboard.create(dashboard, function(result) { if (result && !result.error) { // just go to the new dashboard - $location.path('/' + $scope.currentOrg.slug + '/dash/' + result.id); + $location.path('/dash/' + result.id); } else { console.log(result); } @@ -232,7 +222,7 @@ DashboardControllers.controller('DashDetail', ['$scope', '$routeParams', '$locat Dashboard.update(dashboard, function(result) { if (result && !result.error) { // just go back to view page after a save - $location.path('/' + $scope.currentOrg.slug + '/dash/' + result.id); + $location.path('/dash/' + result.id); } }); }; diff --git a/resources/frontend_client/app/dashboard/dashboard.directives.js b/resources/frontend_client/app/dashboard/dashboard.directives.js index ce748a7b4fa5464e5c8053b4475c1f558a3bea4e..bc1cf77f3edc97fa7a5be0f8db97d1447ea83606 100644 --- a/resources/frontend_client/app/dashboard/dashboard.directives.js +++ b/resources/frontend_client/app/dashboard/dashboard.directives.js @@ -42,8 +42,7 @@ DashboardDirectives.directive('cvAddToDashboardModal', ['CorvusCore', 'Dashboard var existingDashboardsById = {}; Dashboard.list({ - 'orgId': $scope.card.organization.id, - 'filterMode': 'all' + 'filterMode': 'mine' }, function(result) { if (result && !result.error) { $scope.dashboards = result; @@ -63,7 +62,6 @@ DashboardDirectives.directive('cvAddToDashboardModal', ['CorvusCore', 'Dashboard } else if ($scope.card) { // populate a new Dash object var newDash = { - 'organization': $scope.card.organization.id, 'name': $scope.addToDashForm.newDashName, 'public_perms': 0 }; diff --git a/resources/frontend_client/app/dashboard/dashboard.module.js b/resources/frontend_client/app/dashboard/dashboard.module.js index ddb2b897f8999abfb9a6d166a6607f8db12f346e..4b87435a2bc0e4978ffd29b9edb8b60f4e6a5ec6 100644 --- a/resources/frontend_client/app/dashboard/dashboard.module.js +++ b/resources/frontend_client/app/dashboard/dashboard.module.js @@ -14,9 +14,9 @@ var Dashboard = angular.module('corvus.dashboard', [ ]); Dashboard.config(['$routeProvider', function ($routeProvider) { - $routeProvider.when('/:orgSlug/dash/', {templateUrl: '/app/dashboard/partials/dash_list.html', controller: 'DashList'}); - $routeProvider.when('/:orgSlug/dash/create', {templateUrl: '/app/dashboard/partials/dash_create.html', controller: 'DashDetail'}); - $routeProvider.when('/:orgSlug/dash/:dashId', {templateUrl: '/app/dashboard/partials/dash_view.html', controller: 'DashDetail'}); - $routeProvider.when('/:orgSlug/dash/:dashId/modify', {templateUrl: '/app/dashboard/partials/dash_modify.html', controller: 'DashDetail'}); - $routeProvider.when('/:orgSlug/dash/for_card/:cardId', {templateUrl: '/app/dashboard/partials/dash_list_for_card.html', controller: 'DashListForCard'}); + $routeProvider.when('/dash/', {templateUrl: '/app/dashboard/partials/dash_list.html', controller: 'DashList'}); + $routeProvider.when('/dash/create', {templateUrl: '/app/dashboard/partials/dash_create.html', controller: 'DashDetail'}); + $routeProvider.when('/dash/:dashId', {templateUrl: '/app/dashboard/partials/dash_view.html', controller: 'DashDetail'}); + $routeProvider.when('/dash/:dashId/modify', {templateUrl: '/app/dashboard/partials/dash_modify.html', controller: 'DashDetail'}); + $routeProvider.when('/dash/for_card/:cardId', {templateUrl: '/app/dashboard/partials/dash_list_for_card.html', controller: 'DashListForCard'}); }]); diff --git a/resources/frontend_client/app/dashboard/partials/dash_list.html b/resources/frontend_client/app/dashboard/partials/dash_list.html index a61e0ee3f83e57c430c6f99ed0264098f8782c7f..a6adf22344a272a2ba1117272315001a7bf676d0 100644 --- a/resources/frontend_client/app/dashboard/partials/dash_list.html +++ b/resources/frontend_client/app/dashboard/partials/dash_list.html @@ -1,7 +1,7 @@ <div class="wrapper"> <div class="col col-sm-12"> <div class="py2"> - <a class="Button Button--primary float-right my1" cv-org-href="/dash/create">Create Dashboard</a> + <a class="Button Button--primary float-right my1" href="/dash/create">Create Dashboard</a> <h2>Dashboards</h2> </div> <div class="List"> @@ -24,7 +24,7 @@ <a class="Button Button--remove" href="#" ng-click="deleteDash(dash.id)" delete-confirm>Delete</a> </div> <h3> - <a class="link text-normal" cv-org-href="/dash/{{dash.id}}">{{dash.name}}</a> + <a class="link text-normal" href="/dash/{{dash.id}}">{{dash.name}}</a> </h3> <p>{{dash.description}}</p> <span class="text-grey-3">Created by: {{dash.creator.common_name}}</span> diff --git a/resources/frontend_client/app/dashboard/partials/dash_list_for_card.html b/resources/frontend_client/app/dashboard/partials/dash_list_for_card.html index 0518499419f0ad486e34afc5cbad776de264ce31..b2295a6ff11d8895565205973e1034a1dbff3284 100644 --- a/resources/frontend_client/app/dashboard/partials/dash_list_for_card.html +++ b/resources/frontend_client/app/dashboard/partials/dash_list_for_card.html @@ -19,7 +19,7 @@ <li class="List-item List-section" ng-repeat="dash in dashboards | filter:searchFilter"> <span class="List-timeStamp float-right">{{dash.updated_at | date : 'MMM d, y, hh:mm a'}}</span> <h3> - <a class="link text-normal" cv-org-href="/dash/{{dash.id}}">{{dash.name}}</a> + <a class="link text-normal" href="/dash/{{dash.id}}">{{dash.name}}</a> </h3> <p>{{dash.description}}</p> {{dash.creator.common_name}} diff --git a/resources/frontend_client/app/dashboard/partials/dash_view.html b/resources/frontend_client/app/dashboard/partials/dash_view.html index a1d5e6185bd71ac9976fbce83285ed698f131af6..81996a3bfe37f9e0a1269bc14c0146e4d4f87444 100644 --- a/resources/frontend_client/app/dashboard/partials/dash_view.html +++ b/resources/frontend_client/app/dashboard/partials/dash_view.html @@ -31,7 +31,7 @@ <div class="text-centered my4 py4" ng-if="dashboardLoaded && !dashcards.length > 0"> <h1 class="text-normal text-grey-2 mt4 pt4">No cards have been added to this dashboard.</h1> - <a class="Button Button--primary" cv-org-href="/card/create">Create Card</a> + <a class="Button Button--primary" href="/card/create">Create Card</a> </div> <div gridster="gridsterOptions"> diff --git a/resources/frontend_client/app/directives.js b/resources/frontend_client/app/directives.js index 1543bb1379ebcf48ebb3517d8d6573c7ecf0fdbf..6cd4f2971e846a3a01f19e485236deb955a75901 100644 --- a/resources/frontend_client/app/directives.js +++ b/resources/frontend_client/app/directives.js @@ -11,54 +11,6 @@ var CorvusDirectives = angular.module('corvus.directives', []); -CorvusDirectives.directive('cvOrgHref', ['AppState', - function(AppState) { - return { - restrict: 'A', - priority: 99, - link: function(scope, element, attr) { - - var buildPath = function(org, path) { - var href; - if (path.slice(0, 1) != '/') { - // path doesn't begin with a slash, so put it in ourselves - href = '/' + org.slug + '/' + path; - } else { - href = '/' + org.slug + path; - } - - // TODO: the line below was part of the angular ng-href definition, - // but caused an error for me so i commented it out :( - - // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist - // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need - // to set the property as well to achieve the desired effect. - // we use attr[attrName] value since $set can sanitize the url. - //if (msie && 'href') element.prop('href', href); - - return href; - }; - - // we are dependent on the value of our attribute, so begin by observing it - attr.$observe('cvOrgHref', function(path) { - if (!path) { - attr.$set('href', null); - return; - } - - if (AppState.model.currentOrg) { - attr.$set('href', buildPath(AppState.model.currentOrg, path)); - } - - scope.$on('appstate:organization', function (event, org) { - attr.$set('href', buildPath(org, path)); - }); - }); - } - }; - } -]); - CorvusDirectives.directive('deleteConfirm', [function() { return { priority: 1, @@ -192,14 +144,12 @@ NavbarDirectives.directive('mbProfileLink', [function () { function link($scope, element, attr) { - $scope.userIsAdmin = false; $scope.userIsSuperuser = false; $scope.$watch('user', function (user) { if (!user) return; // extract a couple informational pieces about user - $scope.userIsAdmin = user.adminOf(); $scope.userIsSuperuser = user.is_superuser; // determine initials for profile logo @@ -229,10 +179,8 @@ NavbarDirectives.directive('mbProfileLink', [function () { '</a>' + '<ul class="Dropdown-content right">' + '<li><a class="link" href="/user/edit_current">Account Settings</a></li>' + - '<li><a class="link" ng-if="userIsAdmin && context == \'main\'" cv-org-href="/admin/">Admin</a></li>' + - '<li><a class="link" ng-if="userIsAdmin && context == \'admin\'" cv-org-href="/">Exit Admin</a></li>' + - '<li><a class="link" ng-if="userIsSuperuser && context != \'superadmin\'" href="/superadmin/">Site Administration</a></li>' + - '<li><a class="link" ng-if="userIsSuperuser && context == \'superadmin\'" href="/">Exit Site Administration</a></li>' + + '<li><a class="link" ng-if="userIsSuperuser && context != \'admin\'" href="/admin/">Administration</a></li>' + + '<li><a class="link" ng-if="userIsSuperuser && context == \'admin\'" href="/">Exit Administration</a></li>' + '<li><a class="link" href="/auth/logout">Logout</a></li>' + '</ul>' + '</li>' + diff --git a/resources/frontend_client/app/explore/explore.controllers.js b/resources/frontend_client/app/explore/explore.controllers.js index 8a386440af4756d8e4a757b2970ac9a0e4a5fad5..c4f3ced827879b09644bc5c765da77b0ecf4cf0c 100644 --- a/resources/frontend_client/app/explore/explore.controllers.js +++ b/resources/frontend_client/app/explore/explore.controllers.js @@ -8,46 +8,41 @@ var ExploreControllers = angular.module('corvus.explore.controllers', ['corvus.m ExploreControllers.controller('ExploreDatabaseList', ['$scope', 'Metabase', function($scope, Metabase) { $scope.show_non_entities = {}; - $scope.$watch('currentOrg', function(org) { - if (!org) return; + Metabase.table_list(function(tables) { + var databases = {}; - Metabase.table_list({ - 'org': org.id - }, function(tables) { - var databases = {}; - - tables.forEach(function(table) { - var database; - if (databases[table.db.id]) { - database = databases[table.db.id]; - } else { - database = table.db; - database.entities = []; - database.non_entities = []; + tables.forEach(function(table) { + var database; + if (databases[table.db.id]) { + database = databases[table.db.id]; + } else { + database = table.db; + database.entities = []; + database.non_entities = []; - databases[table.db.id] = database; - } + databases[table.db.id] = database; + } - if (table.entity_name) { - database.entities.push(table); - } else { - database.non_entities.push(table); - } - if (database.entities.length > 0) { - $scope.show_non_entities[database.id] = false; - } else { - $scope.show_non_entities[database.id] = true; - } - }); + if (table.entity_name) { + database.entities.push(table); + } else { + database.non_entities.push(table); + } + if (database.entities.length > 0) { + $scope.show_non_entities[database.id] = false; + } else { + $scope.show_non_entities[database.id] = true; + } + }); - $scope.databases = databases; + $scope.databases = databases; - }, function(error) { - console.log('error getting table list', error); - }); + }, function(error) { + console.log('error getting table list', error); }); }]); + ExploreControllers.controller('ExploreTableDetail', ['$scope', '$routeParams', '$location', 'Metabase', function($scope, $routeParams, $location, Metabase) { // $scope.table @@ -87,40 +82,6 @@ ExploreControllers.controller('ExploreTableDetail', ['$scope', '$routeParams', ' }]); -ExploreControllers.controller('ExploreTableMetadata', ['$scope', '$routeParams', 'Metabase', function($scope, $routeParams, Metabase) { - Metabase.table_get({ - 'tableId': $routeParams.tableId - }, function(result, error) { - $scope.table = result; - - // get the fields for this table - Metabase.table_fields({ - 'tableId': $routeParams.tableId - }, function(result) { - $scope.fields = result; - }); - - // get the views for this table - Metabase.table_views({ - 'tableId': $routeParams.tableId - }, function(result) { - $scope.views = result; - }); - }); - - //not used yet - $scope.getFieldSpecialTypeNameById = function(id) { - for (var type in $scope.util.field_special_types) { - console.log(type); - if (type.id == id) { - return type.name; - } - } - //not mapped - return type.id; - }; -}]); - ExploreControllers.controller('ExploreEntityDetail', ['$scope', '$routeParams', '$location', 'Metabase', function($scope, $routeParams, $location, Metabase) { $scope.NUMERIC_FIELD_TYPES = [ @@ -224,176 +185,3 @@ ExploreControllers.controller('ExploreEntityDetail', ['$scope', '$routeParams', } }; }]); - - -ExploreControllers.controller('ExploreTableCohorts', ['$scope', '$routeParams', '$location', 'Metabase', 'CorvusFormGenerator', function($scope, $routeParams, $location, Metabase, CorvusFormGenerator) { - - // $scope.table - // $scope.cohortsData - - $scope.cohortsInput = {}; - $scope.isTableCohortsCompatible = true; - $scope.validActivityValues = []; - - var getValidValuesForField = function(fieldId) { - if (!$scope.fields) { - return []; - } - for (var i = 0; i < $scope.fields.length; i++) { - var currentField = $scope.fields[i]; - if (currentField.id == fieldId) { - for (var p = 0; p < currentField.valid_operators.length; p++) { - var currentOperator = currentField.valid_operators[p]; - if (currentOperator.name == "=") { - for (var q = 0; q < currentOperator.fields.length; q++) { - if (currentOperator.fields[q].type == "select") { - return currentOperator.fields[q].values; - } - } - } - } - } - } - return []; - }; - - if ($routeParams.tableId) { - Metabase.table_query_metadata({ - 'tableId': $routeParams.tableId - }, function(result) { - // Decorate with valid operators - $scope.table = CorvusFormGenerator.addValidOperatorsToFields(result); - - // get the fields for this table - $scope.fields = result.fields; - - // separate out foreign key fields as user ids - $scope.userIdFields = []; - for (var i = 0; i < result.fields.length; i++) { - if (result.fields[i].special_type == 'fk') { - $scope.userIdFields.push(result.fields[i]); - } - } - //choose sensible default or disallow cohorts - if ($scope.userIdFields.length > 0) { - $scope.cohortsInput.field2 = $scope.userIdFields[0].id; - } else { - console.log('no user id field'); - $scope.isTableCohortsCompatible = false; - } - - // separate out date fields - $scope.dateFields = []; - for (i = 0; i < result.fields.length; i++) { - if (result.fields[i].base_type == 'DateTimeField' || result.fields[i].base_type == 'DateField' || result.fields[i].base_type == 'BigIntegerField') { - $scope.dateFields.push(result.fields[i]); - } - } - //choose sensible default or disallow cohorts - if ($scope.dateFields.length > 0) { - $scope.cohortsInput.field = $scope.dateFields[0].id; - } else { - console.log('no date field'); - $scope.isTableCohortsCompatible = false; - } - - // separate out activity fields - $scope.activityFields = []; - for (i = 0; i < result.fields.length; i++) { - if (result.fields[i].special_type == 'category') { - $scope.activityFields.push(result.fields[i]); - } - } - //choose sensible default or disallow cohorts - if ($scope.activityFields.length > 0) { - $scope.cohortsInput.field3 = $scope.activityFields[0].id; - } else { - console.log('no activity field'); - $scope.isTableCohortsCompatible = false; - } - - //when we have a value for activity field (field3), - //choose defaults for start and end activity - $scope.$watch("cohortsInput.field3", function(value) { - if (value) { - $scope.validActivityValues = getValidValuesForField(value); - if ($scope.validActivityValues.length > 1) { - var startIndex = 0; - while (!$scope.validActivityValues[startIndex]) { - startIndex++; - } - $scope.cohortsInput.field4 = $scope.validActivityValues[startIndex]; - $scope.cohortsInput.field5 = $scope.validActivityValues[startIndex + 1]; - } else { - console.log('no start and end activity values for field ' + value + ":"); - console.log($scope.validActivityValues); - $scope.isTableCohortsCompatible = false; - } - } - }); - - //set "time window" default to "day" - $scope.cohortsInput.field6 = "day"; - - //set "survival or abandonment" default to survival - $scope.cohortsInput.field7 = "survival"; - - $scope.$watch("cohortsInput", function(value) { - if (value) { - if (value.field && value.field2 && value.field3 && value.field4 && value.field5 && value.field6 && value.field7) { - $scope.executeCohorts($scope.cohortsInput); - console.log("ready"); - } - } - }); - - }); - } - - $scope.executeCohorts = function(input) { - $scope.running = true; - - // execute the search - //input.type = 'cohorts'; - //input.database = $scope.table.db.id; - var query = { - 'type': 'cohorts', - 'database': $scope.table.db.id, - 'cohorts': input - }; - Metabase.dataset(query, function(result) { - $scope.cohortsData = result; - $scope.running = false; - }); - }; -}]); - -ExploreControllers.controller('ExploreTableSegment', ['$scope', '$routeParams', '$location', 'Metabase', function($scope, $routeParams, $location, Metabase) { - - // $scope.table - // $scope.cohortsData - - if ($routeParams.tableId) { - Metabase.table_get({ - 'tableId': $routeParams.tableId - }, function(result) { - $scope.table = result; - }, function(error) { - console.log(error); - if (error.status == 404) { - $location.path('/'); - } - }); - } - - $scope.createSegment = function(segment) { - segment.tableId = $routeParams.tableId; - Metabase.table_createsegment(segment, function(result) { - if (result && !result.error) { - $location.path('/' + $scope.currentOrg.slug + '/explore/table/' + $routeParams.tableId); - } else { - console.log(result); - } - }); - }; -}]); \ No newline at end of file diff --git a/resources/frontend_client/app/explore/explore.directives.js b/resources/frontend_client/app/explore/explore.directives.js index a0e2ae024c940e8a71d7458bb269868f622b6ff9..f0f649920decdcfe7b51b9060f356e6e680b2273 100644 --- a/resources/frontend_client/app/explore/explore.directives.js +++ b/resources/frontend_client/app/explore/explore.directives.js @@ -431,235 +431,3 @@ ExploreDirectives.directive('cvDataGrid', ['Metabase', 'TableSegment', 'CorvusCo link: link }; }]); - -ExploreDirectives.directive('limitWidget', [function(Metabase) { - - function link(scope, element, attr) { - scope.limitOptions = [{ - label: "1", - value: 1 - }, { - label: "10", - value: 10 - }, { - label: "25", - value: 25 - }, { - label: "50", - value: 50 - }, { - label: "100", - value: 100 - }, { - label: "1000", - value: 1000 - }]; - - } - - return { - restrict: 'E', - replace: true, - templateUrl: '/app/explore/partials/widget_limit.html', - scope: { - query: '=', - readonly: '=' - }, - link: link - }; -}]); - -ExploreDirectives.directive('aggregationWidget', [function(Metabase) { - - function link(scope, element, attr) { - - scope.aggregation_selected = function() { - scope.selected_aggregation = scope.table.aggregation_lookup[scope.query.aggregation[0]]; - - scope.query.aggregation.length = scope.selected_aggregation.fields.length + 1; - }; - - scope.addDimension = function() { - if (scope.query.breakout.length < 2) { - scope.query.breakout.push(null); - } - }; - - scope.removeDimension = function(table_breakout_index) { - - scope.query.breakout.splice(table_breakout_index, 1); - }; - } - - return { - restrict: 'E', - replace: true, - templateUrl: '/app/explore/partials/widget_advanced_aggregation.html', - scope: { - query: '=', - table: '=', - readonly: '=' - }, - link: link - }; -}]); - - -ExploreDirectives.directive('filterWidget', [function(Metabase) { - - function link(scope, element, attr) { - - scope.addFilter = function() { - if (scope.query.filter[0] != "AND") { - // prepend with an AND - scope.query.filter = ["AND", scope.query.filter]; - } - scope.query.filter.push([null, null]); - }; - - scope.removeFilter = function(table_filter_index) { - if (scope.query.filter[0] != "AND") { - scope.query.filter = [null, null]; - } else { - scope.query.filter.splice(table_filter_index + 1, 1); - } - }; - scope.wrapped_filters = function() { - var result; - if (typeof scope.query == "undefined") { - return result; - } - if (scope.query.filter[0] != "AND") { - result = [scope.query.filter]; - } else { - result = scope.query.filter.slice(1); - } - //console.log(result); - return result; - }; - - scope.field_for = function(table_filter, field_index) { - var selected_field = scope.table.fields_lookup[table_filter[1]]; - //console.log("selected_field", selected_field); - var return_val = selected_field.operators_lookup[table_filter[0]].fields[field_index]; - //console.log("selected_input_field", return_val.values); - return return_val; - }; - - scope.field_type_for = function(table_filter, field_index) { - return scope.field_for(table_filter, field_index).type; - }; - - scope.operator_selected = function(table_filter) { - console.log('filter - ', table_filter); - var selected_field = scope.table.fields_lookup[table_filter[1]]; - console.log('selected field', selected_field); - table_filter.length = selected_field.operators_lookup[table_filter[0]].fields.length + 2; - for (var i = 2; i < table_filter.length; i++) { - table_filter[i] = null; - } - console.log('filter -> ', table_filter); - }; - } - - return { - restrict: 'E', - replace: true, - templateUrl: '/app/explore/partials/widget_filter.html', - scope: { - query: '=', - table: '=', - readonly: '=' - }, - link: link - }; -}]); - -ExploreDirectives.directive('sortByWidget', [function(Metabase) { - - // scope.order_by is an array of [col_name, sort_direction] tuples - // - // we'll keep the model that backs the actual UI in scope.order_by. Then when that - // changes we'll filter out tuples where col_name is null and propogate that to - // scope.query.order_by, which is what actually gets passed to the backend - // - // (otherwise, backend will barf if we pass a tuple with a null col_name) - function link(scope, element, attr) { - - // if scope.query.order_by is already defined then use that as a starting point - if (scope.query && scope.query.order_by) scope.order_by = scope.query.order_by; - - scope.addSortBy = function() { - if (!scope.order_by) { - scope.order_by = []; - } - scope.order_by.push([null, "ascending"]); - scope.updateQueryOrderBy(); - }; - - scope.removeSortBy = function(sortByIndex) { - scope.order_by.splice(sortByIndex, 1); - if (scope.order_by.length === 0) { - scope.order_by = undefined; - } - scope.updateQueryOrderBy(); - }; - - scope.updateQueryOrderBy = function() { - scope.query.order_by = []; - scope.order_by.forEach(function(columnTuple) { - if (columnTuple[0]) scope.query.order_by.push(columnTuple); - }); - - console.log(scope.query.order_by); - }; - } - - return { - restrict: 'E', - replace: true, - templateUrl: '/app/explore/partials/widget_sort_by.html', - scope: { - query: '=', - table: '=', - readonly: '=' - }, - link: link - }; -}]); - -ExploreDirectives.directive('fieldsWidget', [function(Metabase) { - - function link(scope, element, attr) { - - scope.addField = function() { - if (!scope.query.fields) { - scope.query.fields = []; - } - scope.query.fields.push(null); - }; - - scope.removeField = function(index) { - scope.query.fields.splice(index, 1); - if (scope.query.fields.length === 0) { - scope.query.fields = undefined; - } - }; - - scope.fieldChanged = function(column, index) { - scope.query.fields[index] = column; - }; - } - - return { - restrict: 'E', - replace: true, - templateUrl: '/app/explore/partials/widget_fields.html', - scope: { - query: '=', - table: '=', - readonly: '=' - }, - link: link - }; -}]); diff --git a/resources/frontend_client/app/explore/explore.module.js b/resources/frontend_client/app/explore/explore.module.js index ed47fdf7114762ce84772d57053f3b6079e32e29..f14c25c1d6d9a9f7b4e2884db82426eb5dc6e7bb 100644 --- a/resources/frontend_client/app/explore/explore.module.js +++ b/resources/frontend_client/app/explore/explore.module.js @@ -8,10 +8,7 @@ var Explore = angular.module('corvus.explore', [ ]); Explore.config(['$routeProvider', function ($routeProvider) { - $routeProvider.when('/:orgSlug/explore/', {templateUrl: '/app/explore/partials/database_list.html', controller: 'ExploreDatabaseList'}); - $routeProvider.when('/:orgSlug/explore/table/:tableId', {templateUrl: '/app/explore/partials/table_detail.html', controller: 'ExploreTableDetail'}); - $routeProvider.when('/:orgSlug/explore/table/:tableId/cohorts', {templateUrl: '/app/explore/partials/table_cohorts.html', controller: 'ExploreTableCohorts'}); - $routeProvider.when('/:orgSlug/explore/table/:tableId/segments', {templateUrl: '/app/explore/partials/table_segment.html', controller: 'ExploreTableSegment'}); - $routeProvider.when('/:orgSlug/explore/table/:tableId/metadata', {templateUrl: '/app/explore/partials/table_metadata.html', controller: 'ExploreTableMetadata'}); - $routeProvider.when('/:orgSlug/explore/table/:tableId/:entityKey*', {templateUrl: '/app/explore/partials/entity_detail.html', controller: 'ExploreEntityDetail'}); + $routeProvider.when('/explore/', {templateUrl: '/app/explore/partials/database_list.html', controller: 'ExploreDatabaseList'}); + $routeProvider.when('/explore/table/:tableId', {templateUrl: '/app/explore/partials/table_detail.html', controller: 'ExploreTableDetail'}); + $routeProvider.when('/explore/table/:tableId/:entityKey*', {templateUrl: '/app/explore/partials/entity_detail.html', controller: 'ExploreEntityDetail'}); }]); diff --git a/resources/frontend_client/app/explore/partials/data_grid.html b/resources/frontend_client/app/explore/partials/data_grid.html index 662138248d1d0eb1caf06dac2adbddce3cca5dab..d11a5ac3340968821f07023ec556e0c147d91c59 100644 --- a/resources/frontend_client/app/explore/partials/data_grid.html +++ b/resources/frontend_client/app/explore/partials/data_grid.html @@ -138,7 +138,7 @@ <tbody> <tr ng-repeat="row in data.rows track by $index"> <td ng-repeat="coldef in table_metadata.fields track by $index" ng-if="coldef.preview_display"> - <a ng-if="isLinkable(coldef)" cv-org-href="{{buildEntityLink(coldef, row[coldef.colindex])}}">{{row[coldef.colindex]}}</a> + <a ng-if="isLinkable(coldef)" href="{{buildEntityLink(coldef, row[coldef.colindex])}}">{{row[coldef.colindex]}}</a> <span ng-if="!isLinkable(coldef)"> <span ng-if="isNumber(coldef)">{{row[coldef.colindex] | number : 2}}</span> <span ng-if="!isNumber(coldef)">{{row[coldef.colindex] | limitTo : 24}}</span> diff --git a/resources/frontend_client/app/explore/partials/database_list.html b/resources/frontend_client/app/explore/partials/database_list.html index 64364e397edd62dde50ecb8e6f9807ffab370281..8c038ec397386f26a5986d35aed7a5384c0a8517 100644 --- a/resources/frontend_client/app/explore/partials/database_list.html +++ b/resources/frontend_client/app/explore/partials/database_list.html @@ -13,7 +13,7 @@ <li class="border-bottom py2" ng-repeat="table in database.entities"> <span class="float-right text-grey-3 mt2">{{table.rows}} rows</span> <h3 class="text-normal text-brand mt2 mb0"> - <a class="link" cv-org-href="/explore/table/{{table.id}}">{{table.entity_name}}</a> + <a class="link" href="/explore/table/{{table.id}}">{{table.entity_name}}</a> </h3> <p class="text-grey-3 mt1">{{table.description}}</p> </li> @@ -25,7 +25,7 @@ <div class="clearfix"> <span class="float-right text-grey-3 mt1">{{table.rows}} rows</span> <h4 class="text-normal inline-block"> - <a ng-if="table.rows > 0" class="link" cv-org-href="/explore/table/{{table.id}}"> + <a ng-if="table.rows > 0" class="link" href="/explore/table/{{table.id}}"> {{table.name}} </a> <span ng-if="table.rows === 0"> diff --git a/resources/frontend_client/app/explore/partials/entity_detail.html b/resources/frontend_client/app/explore/partials/entity_detail.html index 14ce28b7933f97c0318cb00065aa1f865a5989a6..176a9d27055f0191bfc1249774be0a3c4206f81e 100644 --- a/resources/frontend_client/app/explore/partials/entity_detail.html +++ b/resources/frontend_client/app/explore/partials/entity_detail.html @@ -1,11 +1,8 @@ <div> <div class="col col-md-12"> <div class="row p2 border-bottom clearfix"> - <div class="float-right mt1"> - <a class="Button" cv-org-href="/explore/table/{{table.id}}/metadata">Metadata</a> - </div> <h2> - <a cv-org-href="/explore/table/{{table.id}}"> + <a href="/explore/table/{{table.id}}"> <span class="text-brand inline-block" ng-if="table.entity_name">{{table.entity_name}}</span> <span class="text-brand inline-block" ng-if="!table.entity_name">{{table.name}}</span> </a> @@ -48,7 +45,7 @@ </a> </td> <td ng-switch-when="fk"> - <a cv-org-href="{{'/explore/table/'+col.extra_info.target_table_id+'/'+entity.data.rows[0][$index]}}"> + <a href="{{'/explore/table/'+col.extra_info.target_table_id+'/'+entity.data.rows[0][$index]}}"> {{entity.data.rows[0][$index]}} </a> </td> diff --git a/resources/frontend_client/app/explore/partials/table_cohorts.html b/resources/frontend_client/app/explore/partials/table_cohorts.html deleted file mode 100644 index b38473e86b48adf6e3f8edf7f98719a5edcfa061..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/table_cohorts.html +++ /dev/null @@ -1,94 +0,0 @@ -<div class="page-content row" ng-if="isTableCohortsCompatible"> - <form novalidate> - <p> - <label>Date field:</label> - <label class="Select"> - <select ng-model="cohortsInput.field" ng-options="df.id as df.name for df in dateFields" ng-disabled="dateFields.length < 2"> - <option value="">---------</option> - </select> - </p> - <p> - <label>User id field:</label> - <label class="Select"> - <select ng-model="cohortsInput.field2" ng-options="f.id as f.name for f in userIdFields" ng-disabled="userIdFields.length < 2"> - <option value="">---------</option> - </select> - </p> - <p> - <label>Activity type field:</label> - <label class="Select"> - <select ng-model="cohortsInput.field3" ng-options="af.id as af.name for af in activityFields" ng-disabled="activityFields.length < 2"> - <option value="">---------</option> - </select> - </label> - </p> - <p> - <label>Start activity value:</label> - <label class="Select"> - <select ng-model="cohortsInput.field4" ng-options="f as f for f in validActivityValues" ng-disabled="readonly"> - </select> - </label> - </p> - <p> - <label>End activity value:</label> - <label class="Select"> - <select ng-model="cohortsInput.field5" ng-options="f as f for f in validActivityValues" ng-disabled="readonly"> - </select> - </label> - </p> - <a href="#" ng-click="showAdvancedOptions = ! showAdvancedOptions">{{showAdvancedOptions ? 'hide advanced options' : 'show advanced options'}}</a> - <div class="advanced-options" ng-show="showAdvancedOptions"> - <p> - <label>Survival or Abandonment</label> - <label class="Select"> - <select ng-model="cohortsInput.field7"> - <option value="survival">Survival</option> - <option value="abandonment">Abandonment</option> - </select> - </label> - </p> - <p> - <label>Time window:</label> - <label class="Select"> - <select ng-model="cohortsInput.field6"> - <option value="day">Day</option> - <option value="week">Week</option> - <option value="month">Month</option> - <option value="year">Year</option> - </select> - </label> - </p> - </div> - <p> - <input type="button" value="Search" class="btn btn-primary" ng-click="executeCohorts(cohortsInput)" /> - </p> - </form> - - - <p ng-if="running">Running Analysis ...</p> - - - <div ng-if="cohortsData"> - <h3>Results</h3> - - <div ng-if="cohortsData.error"> - <pre>{{cohortsData.error}}</pre> - </div> - - <div> - <table class="table table-striped table-condensed"> - <tr> - <th ng-repeat="column_name in cohortsData.data.columns">{{column_name}}</th> - </tr> - - <tr ng-repeat="row in cohortsData.data.rows"> - <td ng-repeat="cell in row track by $index">{{cell}}</td> - </tr> - </table> - </div> - </div> -</div> - -<div class="page-content row card-data-empty" ng-if="!isTableCohortsCompatible"> -Sorry, cohort analysis cannot be performed on this table. -</div> diff --git a/resources/frontend_client/app/explore/partials/table_heatmap.html b/resources/frontend_client/app/explore/partials/table_heatmap.html deleted file mode 100644 index 0afce0c3cf37012779192ee2edf885d17194efe5..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/table_heatmap.html +++ /dev/null @@ -1,12 +0,0 @@ - -<ol class="breadcrumb"> - <li><a cv-org-href="/explore/org/{{table.db.organization.id}}">{{table.db.organization.name}}</a></li> - <li><a cv-org-href="/explore/db/{{table.db.id}}">{{table.db.name}}</a></li> - <li><a cv-org-href="/explore/table/{{table.id}}">{{table.name}}</a></li> - <li class="active">Heatmap</li> -</ol> - - -<div id="map-canvas" style="height: 600px; width: 800px;" cv-latlong-heatmap="heatmapData"></div> - - diff --git a/resources/frontend_client/app/explore/partials/table_metadata.html b/resources/frontend_client/app/explore/partials/table_metadata.html deleted file mode 100644 index 0a6a909de3fbe501e177347a88423293f60fa0fb..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/table_metadata.html +++ /dev/null @@ -1,54 +0,0 @@ -<div class="px2"> - <div class="px2 py3 clearfix"> - <div class="float-right"> - <a class="Button" cv-org-href="/explore/table/{{table.id}}/">Details</a> - </div> - <h3>Metadata</h3> - </div> - <div class="EntityGroup bordered clearfix"> - <div class="border-bottom"> - <div class="EntityInfo py2 clearfix"> - <label class="Select EntityCustomType float-right"> - <select ng-class="{CustomTypeApplied: table.entity_type }" ng-model="table.entity_type" ng-change="inlineSave()" ng-options="ent_type.id as ent_type.name for ent_type in utils.table_entity_types" disabled="disabled"> - </select> - - </label> - <h2> - <div class="EntityFieldName"> - <span ng-if="table.entity_name">{{table.entity_name}}</span> - <span ng-if="!table.entity_name">{{table.name}}</span> - </div> - </h2> - <div class="text-grey-3" ng-if="table.entity_name">Underlying Table Name: {{table.name}}</div> - <div> - <div class="EntityDescription" ng-if="table.description"> - {{table.description}} - </div> - </div> - </div> - </div> - <ul> - <li class="EntityField" ng-repeat="field in fields" ng-class="{EntityFieldHidden: field.preview_display == false }"> - <div class="EntityInfo"> - <div class="float-right"> - <label class="Select EntityCustomType"> - <select ng-class="{CustomTypeApplied: field.field_type }" ng-model="field.field_type" ng-change="inlineSaveField($index)" ng-options="spec_type.id as spec_type.name for spec_type in utils.field_field_types" disabled="disabled"> - </select> - </label> - <label class="Select EntityCustomType"> - <select ng-class="{CustomTypeApplied: field.special_type }" ng-model="field.special_type" ng-change="inlineSaveField($index)" ng-options="spec_type.id as spec_type.name for spec_type in utils.field_special_types" disabled="disabled"> - </select> - </label> - </div> - <div class="clearfix"> - <h3 class="EntityFieldName float-left">{{field.name}}</h3> - <span class="EntityOriginalType float-left">{{field.base_type}}</span> - </div> - <div class="EntityDescription"> - {{field.description}} - </div> - </div> - </li> - </ul> - </div> -</div> diff --git a/resources/frontend_client/app/explore/partials/table_segment.html b/resources/frontend_client/app/explore/partials/table_segment.html deleted file mode 100644 index 66500a590ac3e76ade7755d58780c9c35e25079c..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/table_segment.html +++ /dev/null @@ -1,19 +0,0 @@ - -<ol class="breadcrumb"> - <li><a cv-org-href="/explore/org/{{table.db.organization.id}}">{{table.db.organization.name}}</a></li> - <li><a cv-org-href="/explore/db/{{table.db.id}}">{{table.db.name}}</a></li> - <li><a cv-org-href="/explore/table/{{table.id}}">{{table.name}}</a></li> - <li class="active">Create Segment</li> -</ol> - -<form id="query_form" novalidate> - <p> - <label for="id_sql">SELECT * from {{table.name}} WHERE:</label> - <textarea cols="40" id="id_sql" name="sql" rows="10" style="width: 100%; white-space:pre;" ng-model="segment.query"></textarea> - </p> - <p> - <label for="id_database">Name:</label> - <input type="text" ng-model="segment.name"> - </p> - <input class="btn btn-default btn-primary" type="button" value="Create Segment" ng-click="createSegment(segment)"/> -</form> diff --git a/resources/frontend_client/app/explore/partials/widget_advanced_aggregation.html b/resources/frontend_client/app/explore/partials/widget_advanced_aggregation.html deleted file mode 100644 index 8bb9f676b74fc840a2315a9912421d02651ac911..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/widget_advanced_aggregation.html +++ /dev/null @@ -1,36 +0,0 @@ -<div> - <div class="px2 py1 border-bottom"> - <h5>Give me the</h5> - <label class="Select py1"> - <select id="id_aggregation" ng-model="query.aggregation[0]" ng-options="agg.short as agg.name for agg in table.aggregation_options" ng-change="aggregation_selected()" ng-disabled="readonly"> readonly: {{readonly}} - </select> - </label> - <span class="py1" ng-repeat="aggregation_field in selected_aggregation.fields track by $index"> - <h5>Column:</h5> - <label class="Select py1"> - <select id="id_filtered_field" ng-model="query.aggregation[$index+1]" ng-options="field[0] as field[1] for field in aggregation_field" ng-disabled="readonly"> - <option value="">Pick a Field</option> - </select> - </label> - </span> - </div> - - <div class="px2 py1 border-bottom"> - <h5>Broken out by</h5> - - <ul id="breakout_list"> - <li class="py1 block clearfix" ng-repeat="breakout_field in query.breakout track by $index"> - <a class="text-grey-2 float-right" href="#" ng-if="!readonly" ng-click="removeDimension($index)"> - <mb-icon name="close" class="mt1" width="16px" height="16px"></mb-icon> - </a> - <label class="Select"> - <select ng-model="query.breakout[$index]" ng-options="br[0] as br[1] for br in table.breakout_options.fields" ng-disabled="readonly"> - </select> - </label> - </li> - </ul> - <div class="py1"> - <a class="link" href="#" ng-if="!readonly" ng-click="addDimension()">Add dimension</a> - </div> - </div> -</div> diff --git a/resources/frontend_client/app/explore/partials/widget_fields.html b/resources/frontend_client/app/explore/partials/widget_fields.html deleted file mode 100644 index a0a6060cae42b8d07ad61cf5d8cc9b056d44e913..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/widget_fields.html +++ /dev/null @@ -1,21 +0,0 @@ -<div class="px2 py1 border-bottom"> - <h5>Only show columns</h5> - <div> - <ul> - <li class="py1 clearfix" ng-repeat="field in query.fields track by $index"> - <a class="text-grey-2 float-right" ng-if="!readonly" href="#" ng-click="removeField($index)"> - <mb-icon name="close" width="16px" height="16px"></mb-icon> - </a> - <label class="Select" for="id_sort_by_field"> - <select id="id_sort_by_field" ng-model="field" ng-options="field.id as field.name for field in table.fields" - ng-disabled="readonly" ng-change="fieldChanged(field, $index)"> - <option value="">Pick a Column</option> - </select> - </label> - </li> - </ul> - <div class="py1"> - <a class="link" href="#" ng-if="!readonly" ng-click="addField()">Add a column</a> - </div> - </div> -</div> diff --git a/resources/frontend_client/app/explore/partials/widget_filter.html b/resources/frontend_client/app/explore/partials/widget_filter.html deleted file mode 100644 index 8b6963a965a9d9b8b5ca958c46bfb5291de9b549..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/widget_filter.html +++ /dev/null @@ -1,39 +0,0 @@ -<div class="px2 py1 border-bottom"> - <h5>Filtered by</h5> - <div> - <ul class="FilterClauseList"> - <li class="FilterClause py1 clearfix" ng-repeat="table_filter in wrapped_filters()"> - <div ng-if="table_filter != 'AND'"> - <a class="text-grey-2 float-right" ng-if="!readonly" href="#" ng-click="removeFilter($index)"> - <mb-icon name="close" width="16px" height="16px"></mb-icon> - </a> - <label class="Select" for="id_filtered_field"> - <select id="id_filtered_field" ng-model="table_filter[1]" ng-options="field.id as field.name for field in table.fields" ng-disabled="readonly"> - <option value="">Pick a Field</option> - </select> - </label> - - <label class="Select"> - <select id="id_filtered_field" ng-model="table_filter[0]" ng-options="operator.name as operator.verbose_name for operator in table.fields_lookup[table_filter[1]].valid_operators" ng-change="operator_selected(table_filter)" ng-disabled="readonly"> - <option value="">---------</option> - </select> - </label> - <span ng-repeat="filter_value in table_filter|slice:2:1000 track by $index"> - <span ng-if="field_type_for(table_filter, $index)=='select'"> - <label class="Select"> - <select ng-model="table_filter[$index+2]" ng-options="f.key as f.name for f in field_for(table_filter, $index).values" ng-disabled="readonly"> - </select> - </label> - </span> - <span ng-if="field_type_for(table_filter, $index)!='select'"> - <input class="input" size="30" type="{{field_type_for(table_filter, $index)}}" ng-model="table_filter[$index+2]" ng-readonly="readonly"/ > - </span> - </span> - </div> - </li> - </ul> - <div class="AddFilterButton py1"> - <a class="link" href="#" ng-if="!readonly" ng-click="addFilter()">Add filter</a> - </div> - </div> -</div> diff --git a/resources/frontend_client/app/explore/partials/widget_limit.html b/resources/frontend_client/app/explore/partials/widget_limit.html deleted file mode 100644 index 6fb9e49186725f36c93e1ee84213bcb086a4e260..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/widget_limit.html +++ /dev/null @@ -1,8 +0,0 @@ -<div class="px2 py1 border-bottom"> - <label class="Select float-right"> - <select id="id_limit" ng-model="query.limit" ng-disabled="readonly" ng-options = "limitOption.value as limitOption.label for limitOption in limitOptions"> - <option value="">--max (65k)--</option> - </select> - </label> - <h5>Only Display:</h5> -</div> diff --git a/resources/frontend_client/app/explore/partials/widget_sort_by.html b/resources/frontend_client/app/explore/partials/widget_sort_by.html deleted file mode 100644 index d5523a70e5955987167853edb95b8043dd2b1d88..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/explore/partials/widget_sort_by.html +++ /dev/null @@ -1,27 +0,0 @@ -<div class="px2 py1 border-bottom"> - <h5>Sorted by</h5> - <div> - <ul> - <li class="py1 clearfix" ng-repeat="sortByColumn in order_by"> - <a class="text-grey-2 float-right" ng-if="!readonly" href="#" ng-click="removeSortBy($index)"> - <mb-icon name="close" width="16px" height="16px"></mb-icon> - </a> - <label class="Select" for="id_sort_by_field"> - <select id="id_sort_by_field" ng-model="sortByColumn[0]" ng-options="field.id as field.name for field in table.fields" - ng-disabled="readonly" ng-change="updateQueryOrderBy()"> - <option value="">Pick a Field</option> - </select> - </label> - <label class="Select"> - <select id="id_sort_by_field_direction" ng-model="sortByColumn[1]" ng-disabled="readonly" ng-change="updateQueryOrderBy()"> - <option value="ascending">Ascending</option> - <option value="descending">Descending</option> - </select> - </label> - </li> - </ul> - <div class="py1"> - <a class="link" href="#" ng-if="!readonly" ng-click="addSortBy()">Add sort by column</a> - </div> - </div> -</div> diff --git a/resources/frontend_client/app/operator/operator.controllers.js b/resources/frontend_client/app/operator/operator.controllers.js index 20da03dc18a82d3a98e35120b4389b35e7dde86b..c6626c1a546543c12a34f48e56d748f1b559ed5c 100644 --- a/resources/frontend_client/app/operator/operator.controllers.js +++ b/resources/frontend_client/app/operator/operator.controllers.js @@ -8,68 +8,63 @@ OperatorControllers.controller('SpecialistList', ['$scope', 'Metabase', 'Operato $scope.orderByField = "nick"; $scope.reverseSort = false; - $scope.$watch('currentOrg', function (org) { - if (!org) return; + Operator.queryInfo().then(function(queryInfo){ - Operator.queryInfo(org.id).then(function(queryInfo){ + // TODO: we need offset support in dataset_query if we want to do paging - // TODO: we need offset support in dataset_query if we want to do paging - - // TODO: ideally we can search by (name, id, store) - $scope.search = function () { - Metabase.dataset({ - 'database': queryInfo.database, - 'type': 'query', - 'query': { - 'source_table': queryInfo.specialist_table, - 'aggregation': ['rows'], - 'breakout': [null], - 'filter':[null, null], - 'limit': null - } - }, function (queryResponse) { - // TODO: we should check that the query succeeded - $scope.specialists = Operator.convertToObjects(queryResponse.data); - - }, function (error) { - console.log(error); - }); - }; - - $scope.search(); - - //run saved SQL queries for overview metrics in right pane + // TODO: ideally we can search by (name, id, store) + $scope.search = function () { Metabase.dataset({ 'database': queryInfo.database, - 'type': 'result', - 'result':{ - query_id: queryInfo.specialist_overview_avg_rating_query + 'type': 'query', + 'query': { + 'source_table': queryInfo.specialist_table, + 'aggregation': ['rows'], + 'breakout': [null], + 'filter':[null, null], + 'limit': null } - }, function(queryResponse){ - $scope.overviewAvgRating = queryResponse.data.rows[0][0]; - }, function(error){ - console.log("error:"); - console.log(error); - }); + }, function (queryResponse) { + // TODO: we should check that the query succeeded + $scope.specialists = Operator.convertToObjects(queryResponse.data); - Metabase.dataset({ - 'database': queryInfo.database, - 'type': 'result', - 'result': { - query_id: queryInfo.specialist_overview_avg_response_time_query - } - }, function(queryResponse){ - $scope.overviewAvgResponseTimeSecs = queryResponse.data.rows[0][0]; - }, function(error){ - console.log("error:"); + }, function (error) { console.log(error); }); + }; + + $scope.search(); + + //run saved SQL queries for overview metrics in right pane + Metabase.dataset({ + 'database': queryInfo.database, + 'type': 'result', + 'result':{ + query_id: queryInfo.specialist_overview_avg_rating_query + } + }, function(queryResponse){ + $scope.overviewAvgRating = queryResponse.data.rows[0][0]; + }, function(error){ + console.log("error:"); + console.log(error); + }); - }, function(reason){ - console.log("failed to get queryInfo:"); - console.log(reason); + Metabase.dataset({ + 'database': queryInfo.database, + 'type': 'result', + 'result': { + query_id: queryInfo.specialist_overview_avg_response_time_query + } + }, function(queryResponse){ + $scope.overviewAvgResponseTimeSecs = queryResponse.data.rows[0][0]; + }, function(error){ + console.log("error:"); + console.log(error); }); + }, function(reason){ + console.log("failed to get queryInfo:"); + console.log(reason); }); } @@ -84,50 +79,46 @@ OperatorControllers.controller('SpecialistDetail', ['$scope', '$routeParams', 'M // set reverse to true so we see the most recent messages first $scope.reverseSort = true; - $scope.$watch('currentOrg', function (org) { - if (!org) return; + Operator.queryInfo().then(function(queryInfo){ + if ($routeParams.specialistId) { + Metabase.dataset({ + 'database': queryInfo.database, + 'type': 'query', + 'query': { + 'source_table': queryInfo.specialist_table, + 'aggregation': ['rows'], + 'breakout': [null], + 'filter':['=', queryInfo.specialist_id_field, parseInt($routeParams.specialistId, 10)], + 'limit': null + } + }, function (queryResponse) { + $scope.specialist = Operator.convertToObjects(queryResponse.data)[0]; - Operator.queryInfo(org.id).then(function(queryInfo){ - if ($routeParams.specialistId) { + // grab conversations Metabase.dataset({ 'database': queryInfo.database, 'type': 'query', 'query': { - 'source_table': queryInfo.specialist_table, + 'source_table': queryInfo.conversations_table, 'aggregation': ['rows'], 'breakout': [null], - 'filter':['=', queryInfo.specialist_id_field, parseInt($routeParams.specialistId, 10)], + 'filter':['=', queryInfo.conversations_specialist_fk, parseInt($routeParams.specialistId, 10)], 'limit': null } - }, function (queryResponse) { - $scope.specialist = Operator.convertToObjects(queryResponse.data)[0]; - - // grab conversations - Metabase.dataset({ - 'database': queryInfo.database, - 'type': 'query', - 'query': { - 'source_table': queryInfo.conversations_table, - 'aggregation': ['rows'], - 'breakout': [null], - 'filter':['=', queryInfo.conversations_specialist_fk, parseInt($routeParams.specialistId, 10)], - 'limit': null - } - }, function (response) { - $scope.conversations = Operator.convertToObjects(response.data); - - }, function (error) { - console.log(error); - }); + }, function (response) { + $scope.conversations = Operator.convertToObjects(response.data); }, function (error) { console.log(error); }); - } - }, function(reason){ - console.log("failed to get queryInfo:"); - console.log(reason); - }); + + }, function (error) { + console.log(error); + }); + } + }, function(reason){ + console.log("failed to get queryInfo:"); + console.log(reason); }); } ]); @@ -141,60 +132,56 @@ OperatorControllers.controller('ConversationDetail', ['$scope', '$routeParams', return angular.fromJson(str); }; - $scope.$watch('currentOrg', function (org) { - if (!org) return; + Operator.queryInfo().then(function(queryInfo){ + if ($routeParams.conversationId) { + Metabase.dataset({ + 'database': queryInfo.database, + 'type': 'query', + 'query': { + 'source_table': queryInfo.conversations_table, + 'aggregation': ['rows'], + 'breakout': [null], + 'filter':['=', queryInfo.conversations_id_field, $routeParams.conversationId], + 'limit': null + } + }, function (queryResponse) { + $scope.conversation = Operator.convertToObjects(queryResponse.data)[0]; - Operator.queryInfo(org.id).then(function(queryInfo){ - if ($routeParams.conversationId) { + // grab messages + // TODO: ensure ordering by message timestamp Metabase.dataset({ 'database': queryInfo.database, 'type': 'query', 'query': { - 'source_table': queryInfo.conversations_table, + 'source_table': queryInfo.messages_table, 'aggregation': ['rows'], 'breakout': [null], - 'filter':['=', queryInfo.conversations_id_field, $routeParams.conversationId], + 'filter':['=', queryInfo.messages_table_conversation_fk, $routeParams.conversationId], 'limit': null } - }, function (queryResponse) { - $scope.conversation = Operator.convertToObjects(queryResponse.data)[0]; - - // grab messages - // TODO: ensure ordering by message timestamp - Metabase.dataset({ - 'database': queryInfo.database, - 'type': 'query', - 'query': { - 'source_table': queryInfo.messages_table, - 'aggregation': ['rows'], - 'breakout': [null], - 'filter':['=', queryInfo.messages_table_conversation_fk, $routeParams.conversationId], - 'limit': null - } - }, function (response) { - $scope.messages = Operator.convertToObjects(response.data); - - // sort them by timestamp - $scope.messages.sort(function compare (a, b) { - if (a.time_updated_server < b.time_updated_server) - return -1; - if (a.time_updated_server > b.time_updated_server) - return 1; - return 0; - }); - - }, function (error) { - console.log(error); + }, function (response) { + $scope.messages = Operator.convertToObjects(response.data); + + // sort them by timestamp + $scope.messages.sort(function compare (a, b) { + if (a.time_updated_server < b.time_updated_server) + return -1; + if (a.time_updated_server > b.time_updated_server) + return 1; + return 0; }); }, function (error) { console.log(error); }); - } - }, function(reason){ - console.log("failed to get queryInfo:"); - console.log(reason); - }); + + }, function (error) { + console.log(error); + }); + } + }, function(reason){ + console.log("failed to get queryInfo:"); + console.log(reason); }); } ]); diff --git a/resources/frontend_client/app/operator/operator.module.js b/resources/frontend_client/app/operator/operator.module.js index db55c7d1e5492c127b8de5b40cfaf92ed1d22e56..6dadd90e2010fd34eb695fa06bee8409c4c80723 100644 --- a/resources/frontend_client/app/operator/operator.module.js +++ b/resources/frontend_client/app/operator/operator.module.js @@ -12,7 +12,7 @@ var Operator = angular.module('corvus.operator', [ ]); Operator.config(['$routeProvider', function ($routeProvider) { - $routeProvider.when('/:orgSlug/specialist/:specialistId', {templateUrl: '/app/operator/partials/specialist_detail.html', controller: 'SpecialistDetail'}); - $routeProvider.when('/:orgSlug/specialist/', {templateUrl: '/app/operator/partials/specialist_list.html', controller: 'SpecialistList'}); - $routeProvider.when('/:orgSlug/conversation/:conversationId', {templateUrl: '/app/operator/partials/conversation_detail.html', controller: 'ConversationDetail'}); + $routeProvider.when('/operator/specialist/:specialistId', {templateUrl: '/app/operator/partials/specialist_detail.html', controller: 'SpecialistDetail'}); + $routeProvider.when('/operator/specialist/', {templateUrl: '/app/operator/partials/specialist_list.html', controller: 'SpecialistList'}); + $routeProvider.when('/operator/conversation/:conversationId', {templateUrl: '/app/operator/partials/conversation_detail.html', controller: 'ConversationDetail'}); }]); diff --git a/resources/frontend_client/app/operator/operator.services.js b/resources/frontend_client/app/operator/operator.services.js index 63def6e3402cda4397d5b19fa46ac61c3b305e1e..83d7a9a779d59a7d6bee45568f2a2ffabc31006f 100644 --- a/resources/frontend_client/app/operator/operator.services.js +++ b/resources/frontend_client/app/operator/operator.services.js @@ -24,12 +24,10 @@ OperatorServices.service('Operator', ['$resource', '$q', 'Metabase', 'Query', var SPECIALIST_OVERVIEW_AVG_RESPONSE_TIME_QUERY = "Specialist Entity Avg Response Time Secs"; - this.queryInfo = function(orgId) { + this.queryInfo = function() { var deferred = $q.defer(); var queryInfo = {}; - Metabase.db_list({ - 'orgId': orgId - }, function (dbs){ + Metabase.db_list(function (dbs){ dbs.forEach(function(db){ if(db.name == OPERATOR_DB_NAME){ queryInfo.database = db.id; @@ -63,7 +61,6 @@ OperatorServices.service('Operator', ['$resource', '$q', 'Metabase', 'Query', if(field.name == MESSAGES_CONVERSATIONS_FK_NAME){ queryInfo.messages_table_conversation_fk = field.id; Query.list({ - orgId: orgId, filterMode: 'all' }, function(queries){ queries.forEach(function(query){ diff --git a/resources/frontend_client/app/query_builder/add_to_dashboard_popover.react.js b/resources/frontend_client/app/query_builder/add_to_dashboard_popover.react.js index 2056068a74c53293045ad124897cc4268f981fe2..928a25a130cea7d09f90f8edad5959be4753f05a 100644 --- a/resources/frontend_client/app/query_builder/add_to_dashboard_popover.react.js +++ b/resources/frontend_client/app/query_builder/add_to_dashboard_popover.react.js @@ -25,8 +25,7 @@ var AddToDashboardPopover = React.createClass({ loadDashboardList: function() { var component = this; this.props.dashboardApi.list({ - 'orgId': this.props.card.organization.id, - 'filterMode': 'all' + 'filterMode': 'mine' }, function(result) { component.setState({ dashboards: result @@ -86,7 +85,6 @@ var AddToDashboardPopover = React.createClass({ // populate a new Dash object var newDash = { - 'organization': this.props.card.organization.id, 'name': (name && name.length > 0) ? name : null, 'description': (description && description.length > 0) ? name : null, 'public_perms': 0 @@ -245,13 +243,13 @@ var AddToDashboardPopover = React.createClass({ content = this.renderCreateDashboardForm(); } else if (this.state.newDashSuccess) { dashDetails = this.state.newDashSuccess; - dashLink = "/"+this.props.card.organization.slug+"/dash/"+dashDetails.id; + dashLink = "/dash/"+dashDetails.id; content = this.renderSuccess("Your dashboard, " + dashDetails.name + " was created and " + this.props.card.name + " was added.", dashLink); } else if (this.state.existingDashSuccess) { dashDetails = this.state.existingDashSuccess; - dashLink = "/"+this.props.card.organization.slug+"/dash/"+dashDetails.id; + dashLink = "/dash/"+dashDetails.id; content = this.renderSuccess(this.props.card.name + " was added to " + dashDetails.name, dashLink); } else { diff --git a/resources/frontend_client/app/query_builder/saver.react.js b/resources/frontend_client/app/query_builder/saver.react.js index ef158f69b475f98eb43ccb0b5cd3ade210b42de1..e88d5890feaa5366f934e16e204a9fa87756fc71 100644 --- a/resources/frontend_client/app/query_builder/saver.react.js +++ b/resources/frontend_client/app/query_builder/saver.react.js @@ -87,8 +87,7 @@ var Saver = React.createClass({ // TODO: hard coding values :( var privacyOptions = [ (<option key="0" value={0}>Private</option>), - (<option key="1" value={1}>Others can read</option>), - (<option key="2" value={2}>Others can modify</option>) + (<option key="1" value={1}>Public (others can read)</option>) ]; var formError; diff --git a/resources/frontend_client/app/reserve/reserve.controllers.js b/resources/frontend_client/app/reserve/reserve.controllers.js index 29660342f657f5451ee751bea1add2babaa1c9bd..e967eff71b71276e7d431a2ed62ddd7c66be9088 100644 --- a/resources/frontend_client/app/reserve/reserve.controllers.js +++ b/resources/frontend_client/app/reserve/reserve.controllers.js @@ -7,30 +7,26 @@ ReserveControllers.controller('VenueList', ['$scope', 'Metabase', 'Reserve', $scope.orderByField = "name"; $scope.reverseSort = false; - $scope.$watch('currentOrg', function(org){ - if(!org) return; - - Reserve.queryInfo(org.id).then(function(queryInfo){ - $scope.search = function(){ - Metabase.dataset({ - 'database': queryInfo.database, - 'type': 'query', - 'query': { - 'source_table': queryInfo.venue_table, - 'aggregation': ['rows'], - 'breakout': [null], - 'filter': [null, null], - 'limit': null - } - }, function(queryResponse){ - $scope.venues = Reserve.convertToObjects(queryResponse.data); - }, function(error){ - console.log(error); - }); - }; + Reserve.queryInfo().then(function(queryInfo){ + $scope.search = function(){ + Metabase.dataset({ + 'database': queryInfo.database, + 'type': 'query', + 'query': { + 'source_table': queryInfo.venue_table, + 'aggregation': ['rows'], + 'breakout': [null], + 'filter': [null, null], + 'limit': null + } + }, function(queryResponse){ + $scope.venues = Reserve.convertToObjects(queryResponse.data); + }, function(error){ + console.log(error); + }); + }; - $scope.search(); - }); + $scope.search(); }); } ]); @@ -40,62 +36,59 @@ ReserveControllers.controller('VenueDetail', ['$scope', '$routeParams', 'Metabas $scope.orderByField = "user"; $scope.reverseSort = false; - $scope.$watch('currentOrg', function(org){ - if(!org) return; - Reserve.queryInfo(org.id).then(function(queryInfo){ - Metabase.dataset({ + Reserve.queryInfo().then(function(queryInfo){ + Metabase.dataset({ + 'database': queryInfo.database, + 'type': 'query', + 'query': { + 'source_table': queryInfo.venue_table, + 'aggregation': ['rows'], + 'breakout': [null], + 'filter': ['=', queryInfo.venue_id_field, $routeParams.venueId] + } + }, function(venueResponse){ + $scope.venue = Reserve.convertToObjects(venueResponse.data)[0]; + var dataset_query = { 'database': queryInfo.database, 'type': 'query', 'query': { - 'source_table': queryInfo.venue_table, + 'fields':[ + queryInfo.user_firstName_field, + queryInfo.user_lastName_field, + queryInfo.user_id_field, + queryInfo.booking_guests_field, + queryInfo.booking_overage_field, + queryInfo.booking_rating_field, + queryInfo.booking_createdAt_field, + queryInfo.booking_updatedAt_field + ], + 'from':[{ + 'table': queryInfo.user_table + }, { + 'table': queryInfo.booking_table, + 'join_type': 'inner', + 'conditions':[ + { + 'src_field': queryInfo.user_id_field, + 'dest_field': queryInfo.booking_user_fk + } + ] + }], 'aggregation': ['rows'], 'breakout': [null], - 'filter': ['=', queryInfo.venue_id_field, $routeParams.venueId] + 'filter': ['=', queryInfo.booking_venue_fk, $routeParams.venueId] } - }, function(venueResponse){ - $scope.venue = Reserve.convertToObjects(venueResponse.data)[0]; - var dataset_query = { - 'database': queryInfo.database, - 'type': 'query', - 'query': { - 'fields':[ - queryInfo.user_firstName_field, - queryInfo.user_lastName_field, - queryInfo.user_id_field, - queryInfo.booking_guests_field, - queryInfo.booking_overage_field, - queryInfo.booking_rating_field, - queryInfo.booking_createdAt_field, - queryInfo.booking_updatedAt_field - ], - 'from':[{ - 'table': queryInfo.user_table - }, { - 'table': queryInfo.booking_table, - 'join_type': 'inner', - 'conditions':[ - { - 'src_field': queryInfo.user_id_field, - 'dest_field': queryInfo.booking_user_fk - } - ] - }], - 'aggregation': ['rows'], - 'breakout': [null], - 'filter': ['=', queryInfo.booking_venue_fk, $routeParams.venueId] - } - }; - - Metabase.dataset(dataset_query, - function(bookingsResponse){ - $scope.bookings = Reserve.convertToObjects(bookingsResponse.data); - }, function(error){ - console.log(error); - }); + }; + Metabase.dataset(dataset_query, + function(bookingsResponse){ + $scope.bookings = Reserve.convertToObjects(bookingsResponse.data); }, function(error){ console.log(error); }); + + }, function(error){ + console.log(error); }); }); } @@ -107,30 +100,26 @@ ReserveControllers.controller('UserList', ['$scope', 'Metabase', 'Reserve', $scope.orderByField = "firstName"; $scope.reverseSort = false; - $scope.$watch('currentOrg', function(org){ - if(!org) return; - - Reserve.queryInfo(org.id).then(function(queryInfo){ - $scope.search = function(){ - Metabase.dataset({ - 'database': queryInfo.database, - 'type': 'query', - 'query': { - 'source_table': queryInfo.user_table, - 'aggregation': ['rows'], - 'breakout': [null], - 'filter': [null, null], - 'limit': null - } - }, function(queryResponse){ - $scope.users = Reserve.convertToObjects(queryResponse.data); - }, function(error){ - console.log(error); - }); - }; + Reserve.queryInfo().then(function(queryInfo){ + $scope.search = function(){ + Metabase.dataset({ + 'database': queryInfo.database, + 'type': 'query', + 'query': { + 'source_table': queryInfo.user_table, + 'aggregation': ['rows'], + 'breakout': [null], + 'filter': [null, null], + 'limit': null + } + }, function(queryResponse){ + $scope.users = Reserve.convertToObjects(queryResponse.data); + }, function(error){ + console.log(error); + }); + }; - $scope.search(); - }); + $scope.search(); }); } @@ -142,59 +131,56 @@ ReserveControllers.controller('UserDetail', ['$scope', '$routeParams', 'Metabase $scope.orderByField = "user"; $scope.reverseSort = false; - $scope.$watch('currentOrg', function(org){ - if(!org) return; - Reserve.queryInfo(org.id).then(function(queryInfo){ + Reserve.queryInfo().then(function(queryInfo){ + Metabase.dataset({ + 'database': queryInfo.database, + 'type': 'query', + 'query': { + 'source_table': queryInfo.user_table, + 'aggregation': ['rows'], + 'breakout': [null], + 'filter': ['=', queryInfo.user_id_field, $routeParams.userId] + } + }, function(userResponse){ + $scope.user = Reserve.convertToObjects(userResponse.data)[0]; + Metabase.dataset({ 'database': queryInfo.database, 'type': 'query', 'query': { - 'source_table': queryInfo.user_table, + 'fields':[ + queryInfo.venue_name_field, + queryInfo.venue_id_field, + queryInfo.booking_guests_field, + queryInfo.booking_overage_field, + queryInfo.booking_rating_field, + queryInfo.booking_createdAt_field, + queryInfo.booking_updatedAt_field + ], + 'from':[{ + 'table': queryInfo.venue_table + }, { + 'table': queryInfo.booking_table, + 'join_type': 'inner', + 'conditions':[ + { + 'src_field': queryInfo.venue_id_field, + 'dest_field': queryInfo.booking_venue_fk + } + ] + }], 'aggregation': ['rows'], 'breakout': [null], - 'filter': ['=', queryInfo.user_id_field, $routeParams.userId] + 'filter': ['=', queryInfo.booking_user_fk, $routeParams.userId] } - }, function(userResponse){ - $scope.user = Reserve.convertToObjects(userResponse.data)[0]; - - Metabase.dataset({ - 'database': queryInfo.database, - 'type': 'query', - 'query': { - 'fields':[ - queryInfo.venue_name_field, - queryInfo.venue_id_field, - queryInfo.booking_guests_field, - queryInfo.booking_overage_field, - queryInfo.booking_rating_field, - queryInfo.booking_createdAt_field, - queryInfo.booking_updatedAt_field - ], - 'from':[{ - 'table': queryInfo.venue_table - }, { - 'table': queryInfo.booking_table, - 'join_type': 'inner', - 'conditions':[ - { - 'src_field': queryInfo.venue_id_field, - 'dest_field': queryInfo.booking_venue_fk - } - ] - }], - 'aggregation': ['rows'], - 'breakout': [null], - 'filter': ['=', queryInfo.booking_user_fk, $routeParams.userId] - } - }, function(bookingsResponse){ - $scope.bookings = Reserve.convertToObjects(bookingsResponse.data); - }, function(error){ - console.log(error); - }); - + }, function(bookingsResponse){ + $scope.bookings = Reserve.convertToObjects(bookingsResponse.data); }, function(error){ console.log(error); }); + + }, function(error){ + console.log(error); }); }); } diff --git a/resources/frontend_client/app/reserve/reserve.module.js b/resources/frontend_client/app/reserve/reserve.module.js index a5998d4cd0813085a19a2d9c798ff4ff65de6e87..3a768dc5e18aada1d17fc378aa59eb3f888aa1c1 100644 --- a/resources/frontend_client/app/reserve/reserve.module.js +++ b/resources/frontend_client/app/reserve/reserve.module.js @@ -12,9 +12,9 @@ var Reserve = angular.module('corvus.reserve', [ ]); Reserve.config(['$routeProvider', function ($routeProvider) { - $routeProvider.when('/:orgSlug/venue/', {templateUrl: '/app/reserve/partials/venue_list.html', controller: 'VenueList'}); - $routeProvider.when('/:orgSlug/venue/:venueId', {templateUrl: '/app/reserve/partials/venue_detail.html', controller: 'VenueDetail'}); + $routeProvider.when('/reserve/venue/', {templateUrl: '/app/reserve/partials/venue_list.html', controller: 'VenueList'}); + $routeProvider.when('/reserve/venue/:venueId', {templateUrl: '/app/reserve/partials/venue_detail.html', controller: 'VenueDetail'}); - $routeProvider.when('/:orgSlug/user/', {templateUrl: '/app/reserve/partials/user_list.html', controller: 'UserList'}); - $routeProvider.when('/:orgSlug/user/:userId', {templateUrl: '/app/reserve/partials/user_detail.html', controller: 'UserDetail'}); + $routeProvider.when('/reserve/user/', {templateUrl: '/app/reserve/partials/user_list.html', controller: 'UserList'}); + $routeProvider.when('/reserve/user/:userId', {templateUrl: '/app/reserve/partials/user_detail.html', controller: 'UserDetail'}); }]); diff --git a/resources/frontend_client/app/reserve/reserve.services.js b/resources/frontend_client/app/reserve/reserve.services.js index 042b8dc25837cd07ec95bd9c49fa4aa39fc876f5..81c3cd253f9eac33a7150673a92478e5f32e974a 100644 --- a/resources/frontend_client/app/reserve/reserve.services.js +++ b/resources/frontend_client/app/reserve/reserve.services.js @@ -30,12 +30,10 @@ ReserveServices.service('Reserve', ['$resource', '$q', 'Metabase', var USER_FIRST_NAME_FIELD_NAME = "firstName"; var USER_LAST_NAME_FIELD_NAME = "lastName"; - this.queryInfo = function(orgId){ + this.queryInfo = function() { var deferred = $q.defer(); var queryInfo = {}; - Metabase.db_list({ - 'orgId': orgId - }, function(dbs){ + Metabase.db_list(function(dbs){ dbs.forEach(function(db){ if(db.name == RESERVE_DB_NAME){ queryInfo.database = db.id; diff --git a/resources/frontend_client/app/services.js b/resources/frontend_client/app/services.js index 363d01451d2001345e736ccda777af441ca10fe7..8b01ba2b6ec3ef8e69106db28e7c7c81b4218010 100644 --- a/resources/frontend_client/app/services.js +++ b/resources/frontend_client/app/services.js @@ -5,12 +5,11 @@ var CorvusServices = angular.module('corvus.services', ['http-auth-interceptor', 'ipCookie', 'corvus.core.services']); -CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$location', '$timeout', 'ipCookie', 'Session', 'User', 'Organization', 'PermissionViolation', - function($rootScope, $routeParams, $q, $location, $timeout, ipCookie, Session, User, Organization, PermissionViolation) { +CorvusServices.factory('AppState', ['$rootScope', '$q', '$location', '$timeout', 'ipCookie', 'Session', 'User', 'Settings', + function($rootScope, $q, $location, $timeout, ipCookie, Session, User, Settings) { // this is meant to be a global service used for keeping track of our overall app state // we fire 2 events as things change in the app // 1. appstate:user - // 2. appstate:organization var initPromise; var currentUserPromise; @@ -20,8 +19,7 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati model: { setupToken: null, currentUser: null, - currentOrgSlug: null, - currentOrg: null, + siteSettings: null, appContext: 'unknown' }, @@ -31,6 +29,9 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati var deferred = $q.defer(); initPromise = deferred.promise; + // grab our global settings + service.refreshSiteSettings(); + // just make sure we grab the current user service.refreshCurrentUser().then(function(user) { deferred.resolve(); @@ -45,60 +46,18 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati clearState: function() { currentUserPromise = null; service.model.currentUser = null; - service.model.currentOrgSlug = null; - service.model.currentOrg = null; + service.model.siteSettings = null; // clear any existing session cookies if they exist ipCookie.remove('metabase.SESSION_ID'); }, - setCurrentOrgCookie: function(slug) { - var isSecure = ($location.protocol() === "https") ? true : false; - ipCookie('metabase.CURRENT_ORG', slug, { - path: '/', - secure: isSecure - }); - }, - refreshCurrentUser: function() { // this is meant to be called once on app startup var userRefresh = User.current(function(result) { service.model.currentUser = result; - // add isMember(orgSlug) method to the object - service.model.currentUser.isMember = function(orgSlug) { - return this.org_perms.some(function(org_perm) { - return org_perm.organization.slug === orgSlug; - }); - }; - - // add isAdmin(orgSlug) method to the object - service.model.currentUser.isAdmin = function(orgSlug) { - return this.org_perms.some(function(org_perm) { - return org_perm.organization.slug === orgSlug && org_perm.admin; - }) || this.is_superuser; - }; - - // add memberOf() method to the object enumerating Organizations user is member of - service.model.currentUser.memberOf = function() { - return this.org_perms.map(function(org_perm) { - return org_perm.organization; - }); - }; - - // add adminOf() method to the object enumerating Organizations user is admin of - service.model.currentUser.adminOf = function() { - return this.org_perms.filter(function(org_perm) { - return org_perm.admin; - }).map(function(org_perm) { - return org_perm.organization; - }); - }; - - // apply a convenience variable indicating if the user is a member of multiple orgs - service.model.currentUser.is_multi_org = (service.model.currentUser.memberOf().length > 1); - $rootScope.$broadcast('appstate:user', result); }, function(error) { @@ -112,51 +71,41 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati return currentUserPromise; }, - switchOrg: function(org_slug) { - Organization.get_by_slug({ - 'slug': org_slug - }, function(org) { - service.model.currentOrgSlug = org.slug; - service.model.currentOrg = org; - $rootScope.$broadcast('appstate:organization', service.model.currentOrg); + refreshSiteSettings: function() { + + var settingsRefresh = Settings.list(function(result) { + + var settings = _.indexBy(result, 'key'); + + service.model.siteSettings = settings; + + $rootScope.$broadcast('appstate:site-settings', settings); + }, function(error) { - console.log('error getting current org', error); + console.log('unable to get site settings', error); }); + + return settingsRefresh.$promise; }, // This function performs whatever state cleanup and next steps are required when a user tries to access // something they are not allowed to. invalidAccess: function(user, url, message) { - service.model.currentOrgSlug = null; - service.model.currentOrg = null; - - PermissionViolation.create({ - 'user': user.id, - 'url': url - }); $location.path('/unauthorized/'); - }, routeChanged: function(event) { // establish our application context based on the route (URI) - // valid app contexts are: 'setup', 'auth', 'org', 'org-admin', 'site-admin', 'other', or 'unknown' + // valid app contexts are: 'setup', 'auth', 'main', 'admin', or 'unknown' var routeContext; if ($location.path().indexOf('/auth/') === 0) { routeContext = 'auth'; } else if ($location.path().indexOf('/setup/') === 0) { routeContext = 'setup'; - } else if ($location.path().indexOf('/superadmin/') === 0) { - routeContext = 'site-admin'; - } else if ($routeParams.orgSlug) { - // couple of options when within an org - if ($location.path().indexOf('/' + $routeParams.orgSlug + '/admin/') === 0) { - routeContext = 'org-admin'; - } else { - routeContext = 'org'; - } + } else if ($location.path().indexOf('/admin/') === 0) { + routeContext = 'admin'; } else { - routeContext = 'other'; + routeContext = 'main'; } // if the context of the app has changed due to this route change then send out an event @@ -167,11 +116,13 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati // this code is here to ensure that we have resolved our currentUser BEFORE we execute any other // code meant to establish app context based on the current route - currentUserPromise.then(function(user) { - service.routeChangedImpl(event); - }, function(error) { - service.routeChangedImpl(event); - }); + if (currentUserPromise) { + currentUserPromise.then(function(user) { + service.routeChangedImpl(event); + }, function(error) { + service.routeChangedImpl(event); + }); + } }, routeChangedImpl: function(event) { @@ -191,71 +142,13 @@ CorvusServices.factory('AppState', ['$rootScope', '$routeParams', '$q', '$locati return; } - var onSuperadminPage = $location.path().indexOf('/superadmin/') === 0; - - // NOTE: if you try to do this outside this event you'll run into issues where $routeParams is not set. - // so that's why we explicitly wait until we know when $routeParams will be available - if (onSuperadminPage) { + if ($location.path().indexOf('/admin/') === 0) { // the user is trying to change to a superuser page - if (!service.model.currentUser.is_superuser) { service.invalidAccess(service.model.currentUser, $location.url(), "user is not a superuser!!!"); return; } - } else if ($routeParams.orgSlug) { - // the url is telling us what Organization we are working in - // PERMISSIONS CHECK!! user must be member of this org to proceed - // Making convenience vars so it's easier to scan conditions for correctness - var isSuperuser = service.model.currentUser.is_superuser; - var isOrgMember = service.model.currentUser.isMember($routeParams.orgSlug); - var isOrgAdmin = service.model.currentUser.isAdmin($routeParams.orgSlug); - var onAdminPage = $location.path().indexOf('/' + $routeParams.orgSlug + '/admin') === 0; - - if (!isSuperuser && !isOrgMember) { - service.invalidAccess(service.model.currentUser, $location.url(), "user is not authorized for this org!!!"); - return; - } else if (onAdminPage && !isSuperuser && !isOrgAdmin) { - service.invalidAccess(service.model.currentUser, $location.url(), "user is not an admin for this org!!!"); - return; - } - - if (service.model.currentOrgSlug != $routeParams.orgSlug) { - // we just navigated to a new organization - this.switchOrg($routeParams.orgSlug); - service.model.currentOrgSlug = $routeParams.orgSlug; - service.setCurrentOrgCookie(service.model.currentOrgSlug); - } - - // if we get here it just means we navigated somewhere within the existing org, so do nothing - - } else if (!service.model.currentOrgSlug) { - // the url doesn't tell us what Organization this is, so lets try a different approach - // Check to see if the user has a current org cookie var set - var currentOrgFromCookie = ipCookie('metabase.CURRENT_ORG'); - if (currentOrgFromCookie) { - // check to see if the org slug exists - var orgsWithSlug = service.model.currentUser.org_perms.filter(function(org_perm) { - return org_perm.organization.slug == currentOrgFromCookie; - }); - if (orgsWithSlug.length > 0) { - var currentOrgPerm = orgsWithSlug[0]; - service.model.currentOrg = currentOrgPerm.organization; - service.model.currentOrgSlug = service.model.currentOrg.slug; - service.setCurrentOrgCookie(service.model.currentOrgSlug); - $rootScope.$broadcast('appstate:organization', service.model.currentOrg); - return; - } - } - // Otherwise fall through and set the current org to the first org a user is a member of - if (service.model.currentUser.org_perms.length > 0) { - service.model.currentOrg = service.model.currentUser.org_perms[0].organization; - service.model.currentOrgSlug = service.model.currentOrg.slug; - service.setCurrentOrgCookie(service.model.currentOrgSlug); - $rootScope.$broadcast('appstate:organization', service.model.currentOrg); - } else { - // TODO: this is a real issue. we have a user with no organizations. where do we send them? - } } } }; @@ -311,10 +204,7 @@ CorvusServices.service('CorvusCore', ['$resource', 'User', function($resource, U 'name': 'Private' }, { 'id': 1, - 'name': 'Others can read' - }, { - 'id': 2, - 'name': 'Others can read and modify' + 'name': 'Public (others can read)' }]; this.permName = function(permId) { @@ -915,6 +805,10 @@ CoreServices.factory('Session', ['$resource', '$cookies', function($resource, $c CoreServices.factory('User', ['$resource', '$cookies', function($resource, $cookies) { return $resource('/api/user/:userId', {}, { + create: { + url: '/api/user', + method: 'POST' + }, list: { url: '/api/user/', method: 'GET', @@ -937,11 +831,6 @@ CoreServices.factory('User', ['$resource', '$cookies', function($resource, $cook method: 'PUT', params: { 'userId': '@id' - }, - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } } }, update_password: { @@ -949,139 +838,39 @@ CoreServices.factory('User', ['$resource', '$cookies', function($resource, $cook method: 'PUT', params: { 'userId': '@id' - }, - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } + } + }, + delete: { + method: 'DELETE', + params: { + 'userId': '@userId' } } }); }]); -CoreServices.factory('Organization', ['$resource', '$cookies', function($resource, $cookies) { - return $resource('/api/org/:orgId', {}, { - form_input: { - url: '/api/org/form_input', - method: 'GET' - }, +CoreServices.factory('Settings', ['$resource', function($resource) { + return $resource('/api/setting', {}, { list: { - url: '/api/org/', + url: '/api/setting', method: 'GET', isArray: true }, - create: { - url: '/api/org', - method: 'POST', - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } - } - }, - get: { - url: '/api/org/:orgId', - method: 'GET', - params: { - orgId: '@orgId' - } - }, - get_by_slug: { - url: '/api/org/slug/:slug', - method: 'GET', - params: { - slug: '@slug' - } - }, - update: { - url: '/api/org/:orgId', + + // POST endpoint handles create + update in this case + put: { + url: '/api/setting/:key', method: 'PUT', params: { - orgId: '@id' - }, - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } + key: '@key' } }, + delete: { - url: '/api/org/:orgId', - method: 'DELETE', - params: { - orgId: '@id' - }, - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } - } - }, - members: { - url: '/api/org/:orgId/members', - method: 'GET', - params: { - orgId: '@orgId' - }, - isArray: true - }, - member_create: { - url: '/api/org/:orgId/members', - method: 'POST', - params: { - orgId: '@orgId' - }, - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } - } - }, - member_get: { - url: '/api/org/:orgId/members/:userId', - method: 'GET', - params: { - orgId: '@orgId', - userId: '@userId' - } - }, - member_add: { - url: '/api/org/:orgId/members/:userId', - method: 'POST', - params: { - orgId: '@orgId', - userId: '@userId' - }, - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } - } - }, - member_update: { - url: '/api/org/:orgId/members/:userId', - method: 'PUT', - params: { - orgId: '@orgId', - userId: '@userId' - }, - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } - } - }, - member_remove: { - url: '/api/org/:orgId/members/:userId', + url: '/api/setting/:key', method: 'DELETE', params: { - orgId: '@orgId', - userId: '@userId' - }, - headers: { - 'X-CSRFToken': function() { - return $cookies.csrftoken; - } + key: '@key' } } }); diff --git a/resources/frontend_client/app/setup/partials/setup_info.html b/resources/frontend_client/app/setup/partials/setup_info.html index a382f88276eb28916a8c10c2dde138f7a219f650..aef56e661b3fa55748fc234b2842ca3818331acd 100644 --- a/resources/frontend_client/app/setup/partials/setup_info.html +++ b/resources/frontend_client/app/setup/partials/setup_info.html @@ -46,9 +46,9 @@ <span class="Form-charm"></span> </div> - <div class="Form-field" mb-form-field="organization"> - <mb-form-label display-name="Your organization or company name" field-name="organization"></mb-form-label> - <input class="Form-input Form-offset full" name="organization" type="text" placeholder="Department of awesome" ng-model="newUser.orgName" required> + <div class="Form-field" mb-form-field="value"> + <mb-form-label display-name="Your organization or company name" field-name="value"></mb-form-label> + <input class="Form-input Form-offset full" name="value" type="text" placeholder="Department of awesome" ng-model="newUser.siteName" required> <span class="Form-charm"></span> </div> diff --git a/resources/frontend_client/app/setup/setup.controllers.js b/resources/frontend_client/app/setup/setup.controllers.js index 13fa1ebef7cd7042503106d924b8012e06ba10fa..3b6b5b33546b8511a7654d25f97fc215b052a44f 100644 --- a/resources/frontend_client/app/setup/setup.controllers.js +++ b/resources/frontend_client/app/setup/setup.controllers.js @@ -1,6 +1,6 @@ 'use strict'; -var SetupControllers = angular.module('corvus.setup.controllers', ['corvus.metabase.services', 'corvus.setup.services']); +var SetupControllers = angular.module('corvus.setup.controllers', ['corvus.metabase.services', 'corvusadmin.settings.services', 'corvus.setup.services']); SetupControllers.controller('SetupInit', ['$scope', '$location', '$routeParams', 'AppState', function($scope, $location, $routeParams, AppState) { @@ -14,8 +14,8 @@ SetupControllers.controller('SetupInit', ['$scope', '$location', '$routeParams', } ]); -SetupControllers.controller('SetupInfo', ['$scope', '$routeParams', '$location', '$timeout', 'ipCookie', 'Organization', 'User', 'AppState', 'Setup', 'Metabase', 'CorvusCore', - function($scope, $routeParams, $location, $timeout, ipCookie, Organization, User, AppState, Setup, Metabase, CorvusCore) { +SetupControllers.controller('SetupInfo', ['$scope', '$routeParams', '$location', '$timeout', 'ipCookie', 'User', 'AppState', 'Setup', 'SettingsAdminServices', + function($scope, $routeParams, $location, $timeout, ipCookie, User, AppState, Setup, SettingsAdminServices) { $scope.activeStep = "user"; $scope.completedSteps = { user: false, @@ -108,26 +108,13 @@ SetupControllers.controller('SetupInfo', ['$scope', '$routeParams', '$location', } } - function createOrUpdateOrg() { - // TODO - we need some logic to slugify the name specified. can't have spaces, caps, etc. - if (AppState.model.currentOrg) { - return Organization.update({ - 'id': AppState.model.currentOrg.id, - 'name': $scope.newUser.orgName, - 'slug': $scope.newUser.orgName - }).$promise; - } else { - return Organization.create({ - 'name': $scope.newUser.orgName, - 'slug': $scope.newUser.orgName - }).$promise.then(function(org) { - console.log('first org created', org); - - // switch the org - // TODO - make sure this is up to snuff from a security standpoint - AppState.switchOrg(org.slug); - }); - } + function setSiteName() { + return SettingsAdminServices.put({ + 'key': 'site-name', + 'value': $scope.newUser.siteName + }).$promise.then(function(success) { + // anything we need to do here? + }); } $scope.createOrgAndUser = function() { @@ -135,8 +122,7 @@ SetupControllers.controller('SetupInfo', ['$scope', '$routeParams', '$location', // NOTE: this should both create the user AND log us in and return a session id createOrUpdateUser().then(function() { // now that we should be logged in and our session cookie is established, lets do the rest of the work - // create our first Organization - return createOrUpdateOrg(); + return setSiteName(); }).then(function() { // reset error in case there were previous errors $scope.error = null; diff --git a/resources/frontend_client/app/superadmin/organization/organization.controllers.js b/resources/frontend_client/app/superadmin/organization/organization.controllers.js deleted file mode 100644 index 05bc60b2e78cbe6a168e90bab6d01a79b87462f1..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/superadmin/organization/organization.controllers.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; -/*global _*/ - -var OrganizationControllers = angular.module('superadmin.organization.controllers', [ - 'corvus.services', - 'metabase.forms' -]); - -OrganizationControllers.controller('OrganizationListController', ['$scope', 'Organization', - function($scope, Organization) { - - $scope.deleteOrganization = function(organization) { - Organization.delete({ - orgId: organization.id - }, function() { - $scope.organizations = _.filter($scope.organizations, function(org) { - return org.id !== organization.id; - }); - }, function(err) { - console.log("Error deleting Org:", err); - }); - }; - - $scope.organizations = []; - - // initialize on load - Organization.list(function(orgs) { - $scope.organizations = orgs; - }, function(error) { - console.log("Error getting organizations: ", error); - }); - } -]); - -OrganizationControllers.controller('OrganizationDetailController', ['$scope', '$routeParams', '$location', 'Organization', - function($scope, $routeParams, $location, Organization) { - - $scope.organization = undefined; - - // initialize on load - if ($routeParams.orgId) { - // editing an existing organization - Organization.get({ - 'orgId': $routeParams.orgId - }, function (org) { - $scope.organization = org; - }, function (error) { - console.log("Error getting organization: ", error); - // TODO - should be a 404 response - }); - - // provide a relevant save() function - $scope.save = function(organization) { - $scope.$broadcast("form:reset"); - - // use NULL for unset timezone - if (!organization.report_timezone) { - organization.report_timezone = null; - } - - Organization.update(organization, function (org) { - $scope.organization = org; - $scope.$broadcast("form:api-success", "Successfully saved!"); - }, function (error) { - $scope.$broadcast("form:api-error", error); - }); - }; - - } else { - // assume we are creating a new org - $scope.organization = {}; - - // provide a relevant save() function - $scope.save = function(organization) { - $scope.$broadcast("form:reset"); - Organization.create(organization, function (org) { - $scope.$broadcast("form:api-success", "Successfully saved!"); - $location.path('/superadmin/organization/' + org.id); - }, function (error) { - $scope.$broadcast("form:api-error", error); - }); - }; - } - - // always get our form input - Organization.form_input(function (result) { - $scope.form_input = result; - }, function (error) { - console.log('error getting form input', error); - }); - } -]); diff --git a/resources/frontend_client/app/superadmin/organization/organization.module.js b/resources/frontend_client/app/superadmin/organization/organization.module.js deleted file mode 100644 index e5e5152492923c16bd7ead4530c6d8313a929401..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/superadmin/organization/organization.module.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -var SettingsAdmin = angular.module('superadmin.organization', ['superadmin.organization.controllers']); - -SettingsAdmin.config(['$routeProvider', function($routeProvider) { - $routeProvider.when('/superadmin/organization/', { - templateUrl: '/app/superadmin/organization/partials/org_list.html', - controller: 'OrganizationListController' - }); - - $routeProvider.when('/superadmin/organization/create', { - templateUrl: '/app/superadmin/organization/partials/org_detail.html', - controller: 'OrganizationDetailController' - }); - - $routeProvider.when('/superadmin/organization/:orgId', { - templateUrl: '/app/superadmin/organization/partials/org_detail.html', - controller: 'OrganizationDetailController' - }); -}]); diff --git a/resources/frontend_client/app/superadmin/organization/partials/org_detail.html b/resources/frontend_client/app/superadmin/organization/partials/org_detail.html deleted file mode 100644 index d71b6d90cabc5784900e74866430215a9683f619..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/superadmin/organization/partials/org_detail.html +++ /dev/null @@ -1,54 +0,0 @@ -<div class="wrapper"> - <section class="Breadcrumbs"> - <a class="Breadcrumb Breadcrumb--path" href="/superadmin/organization/">Organizations</a> - <mb-icon name="chevronright" class="Breadcrumb-divider" width="12px" height="12px"></mb-icon> - <h2 class="Breadcrumb Breadcrumb--page" ng-if="!organization.id">Add Organization</h2> - <h2 class="Breadcrumb Breadcrumb--page" ng-if="organization.id">{{organization.name}}</h2> - </section> - - <section class="Grid Grid--gutters Grid--full Grid--2of3"> - <!-- form --> - <div class="Grid-cell Cell--2of3"> - <form class="Form-new bordered rounded shadowed" name="form" novalidate> - <div class="Form-field" mb-form-field="slug"> - <mb-form-label display-name="Slug" field-name="slug"></mb-form-label> - <input class="Form-input Form-offset full" name="slug" placeholder="A short name to go in URLs" ng-model="organization.slug" ng-disabled="organization.id" required /> - </div> - - <div class="Form-field" mb-form-field="name"> - <mb-form-label display-name="Name" field-name="name"></mb-form-label> - <input class="Form-input Form-offset full" name="name" placeholder="What is the name of your organization?" ng-model="organization.name" required autofocus /> - <span class="Form-charm"></span> - </div> - - <div class="Form-field" mb-form-field="description"> - <mb-form-label display-name="Description" field-name="description"></mb-form-label> - <input class="Form-input Form-offset full" name="description" placeholder="What else should people know about this org?" ng-model="organization.description" /> - <span class="Form-charm"></span> - </div> - - <div class="Form-field" mb-form-field="logo_url"> - <mb-form-label display-name="Logo url" field-name="logo_url"></mb-form-label> - <input class="Form-input Form-offset full" name="logo_url" placeholder="Link to an image of your logo" ng-model="organization.logo_url" /> - <span class="Form-charm"></span> - </div> - - <div class="Form-field" mb-form-field="report_timezone"> - <mb-form-label display-name="Reporting timezone" field-name="report_timezone"></mb-form-label> - <label class="Select Form-offset"> - <select ng-model="organization.report_timezone" ng-options="tz for tz in form_input['timezones']"> - <option value="">--- default timezone ---</option> - </select> - </label> - </div> - - <div class="Form-actions"> - <button class="Button" ng-class="{'Button--primary': form.$valid}" ng-click="save(organization)" ng-disabled="!form.$valid"> - Save - </button> - <mb-form-message></mb-form-message> - </div> - </form> - </div> - </section> -</div> diff --git a/resources/frontend_client/app/superadmin/organization/partials/org_list.html b/resources/frontend_client/app/superadmin/organization/partials/org_list.html deleted file mode 100644 index 9f6f41881236877851078409e5abc38d145e45e4..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/superadmin/organization/partials/org_list.html +++ /dev/null @@ -1,37 +0,0 @@ -<div class="wrapper"> - <section class="PageHeader clearfix"> - <a class="Button Button--primary float-right" href="/superadmin/organization/create">Add Organization</a> - <h2 class="PageTitle">Organizations</h2> - </section> - - <section> - <table class="ContentTable"> - <thead> - <tr> - <th>Name</th> - <th>Slug</th> - <th>Description</th> - <th></th> - </tr> - </thead> - <tbody> - <tr ng-show="!organizations"> - <td colspan=4> - <mb-loading-icon></mb-loading-icon> - <h3>Loading ...</h3> - </td> - </tr> - <tr ng-repeat="org in organizations"> - <td> - <a class="link" href="/superadmin/organization/{{org.id}}">{{org.name}}</a> - </td> - <td>{{org.slug}}</td> - <td>{{org.description}}</td> - <td class="Table-actions"> - <button class="Button Button--danger" ng-click="deleteOrganization(org)" delete-confirm>Delete</button> - </td> - </tr> - </tbody> - </table> - </section> -</div> diff --git a/resources/frontend_client/app/superadmin/settings/settings.module.js b/resources/frontend_client/app/superadmin/settings/settings.module.js deleted file mode 100644 index 25f9f18dfd937c6b72558e3738531959e117c1b3..0000000000000000000000000000000000000000 --- a/resources/frontend_client/app/superadmin/settings/settings.module.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; - -var SettingsAdmin = angular.module('superadmin.settings', [ - 'superadmin.settings.controllers', - 'superadmin.settings.services' -]); - -SettingsAdmin.config(['$routeProvider', function($routeProvider) { - $routeProvider.when('/superadmin/settings/', { - templateUrl: '/app/superadmin/settings/partials/settings.html', - controller: 'SettingsAdminController' - }); -}]); diff --git a/resources/frontend_client/index.html b/resources/frontend_client/index.html index e1f4f09b5424265608b963a08db13a18620c1b83..f3993006c8efba643e198fef5d6c5d0887cc9650 100644 --- a/resources/frontend_client/index.html +++ b/resources/frontend_client/index.html @@ -21,40 +21,31 @@ <!-- MAIN NAV --> <nav class="CoreNav clearfix" ng-show="nav === 'main'"> <div class="col col-sm-12"> - <div class="NavItem float-left" ng-if="!user.is_multi_org"> - <span class="NavItem-text">{{currentOrg.name}}</span> - </div> - <div class="NavItem Dropdown float-left" dropdown on-toggle="toggled(open)" ng-if="user.is_multi_org"> - <span dropdown-toggle> - <span class="NavItem-text" ng-bind="currentOrg.name"></span> - <mb-icon name="chevrondown" width="8px" height="8px"></mb-icon> - </span> - <ul class="Dropdown-content"> - <li ng-repeat="organization in userMemberOf"> - <a class="link block py1" href="#" ng-click="changeCurrOrg(organization.slug)">{{organization.name}}</a> - </li> - </ul> + <div class="NavItem float-left"> + <span class="NavItem-text">{{siteName}}</span> </div> + <ul class="float-left ml4"> <li class="inline-block"> - <a class="NavItem NavItem-withIcon" cv-org-href="/dash/" selectable-nav-item="dashboards"> + <a class="NavItem NavItem-withIcon" href="/dash/" selectable-nav-item="dashboards"> <mb-icon class="IconWrapper" name="dashboards"/></mb-icon> <span class="NavItem-text">Dashboards</span> </a> </li> <li class="inline-block"> - <a class="NavItem NavItem-withIcon" cv-org-href="/card/" selectable-nav-item="cards"> + <a class="NavItem NavItem-withIcon" href="/card/" selectable-nav-item="cards"> <mb-icon class="IconWrapper" name="cards"></mb-icon> <span class="NavItem-text">Cards</span> </a> </li> <li class="inline-block"> - <a class="NavItem NavItem-withIcon" cv-org-href="/explore/" selectable-nav-item="explore"> + <a class="NavItem NavItem-withIcon" href="/explore/" selectable-nav-item="explore"> <mb-icon class="IconWrapper" name="explore"></mb-icon> <span class="NavItem-text">Explore</span> </a> </li> </ul> + <ul class="float-right"> <li class="Dropdown inline-block" dropdown on-toggle="toggled(open)"> <a class="NavItem" selectable-nav-item="settings" dropdown-toggle> @@ -64,8 +55,7 @@ </a> <ul class="Dropdown-content right"> <li class="" ><a class="link" href="/user/edit_current">Account Settings</a></li> - <li class="" ><a class="link" ng-if="userIsAdmin" cv-org-href="/admin/">Admin</a></li> - <li class="" ><a class="link" ng-if="userIsSuperuser" href="/superadmin/">Site Administration</a></li> + <li class="" ><a class="link" ng-if="userIsSuperuser" href="/admin/">Administration</a></li> <li class="" ><a class="link" href="/auth/logout">Logout</a></li> </ul> </li> @@ -76,38 +66,25 @@ <!-- ADMIN NAV --> <nav class="AdminNav" ng-show="nav === 'admin'"> <div class="wrapper flex align-center"> - <!-- org title --> - <div class="NavTitle flex align-center" ng-if="!user.is_multi_org"> - <mb-icon name="gear" class="AdminGear" width="22px" height="22px"></mb-icon> - <span class="NavItem-text">{{currentOrg.name}} Admin</span> - </div> - <div class="NavTitle flex align-center Dropdown" dropdown on-toggle="toggled(open)" ng-if="user.is_multi_org"> + <div class="NavTitle flex align-center"> <mb-icon name="gear" class="AdminGear" width="22px" height="22px"></mb-icon> - <span dropdown-toggle> - <span class="NavItem-text">{{currentOrg.name}} Admin</span> - <mb-icon name="chevrondown" width="8px" height="8px"></mb-icon> - </span> - <ul class="Dropdown-content left"> - <li ng-repeat="organization in userMemberOf"> - <a class="link block py1" href="#" ng-click="changeCurrOrg(organization.slug, true)">{{organization.name}}</a> - </li> - </ul> + <span class="NavItem-text ml1">Site Administration</span> </div> <!-- admin sections --> <ul class="ml4 flex flex-full"> <li> - <a class="NavItem" ng-class="{ 'is--selected' : isActive('/admin/org')}" cv-org-href="/admin/org/"> - <span class="NavItem-text">Organization</span> + <a class="NavItem" ng-class="{ 'is--selected' : isActive('/admin/settings')}" href="/admin/settings/"> + <span class="NavItem-text">Settings</span> </a> </li> <li> - <a class="NavItem" ng-class="{ 'is--selected' : isActive('/admin/people')}" cv-org-href="/admin/people/"> + <a class="NavItem" ng-class="{ 'is--selected' : isActive('/admin/people')}" href="/admin/people/"> <span class="NavItem-text">People</span> </a> </li> <li> - <a class="NavItem" ng-class="{ 'is--selected' : isActive('/admin/databases')}" cv-org-href="/admin/databases/"> + <a class="NavItem" ng-class="{ 'is--selected' : isActive('/admin/databases')}" href="/admin/databases/"> <span class="NavItem-text">Databases</span> </a> </li> @@ -117,33 +94,6 @@ <mb-profile-link user="user" context="nav"></mb-profile-link> </div> </nav> - - <!-- SITE ADMINISTRATION (SUPER ADMIN) NAV --> - <nav class="AdminNav" ng-show="nav === 'superadmin'"> - <div class="wrapper flex align-center"> - <div class="NavTitle flex align-center"> - <mb-icon name="gear" class="AdminGear" width="22px" height="22px"></mb-icon> - <span class="NavItem-text ml1">Site Administration</span> - </div> - - <ul class="ml4 flex flex-full"> - <li> - <a class="NavItem" ng-class="{ 'is--selected' : isActive('/superadmin/settings')}" href="/superadmin/settings/"> - <span class="NavItem-text">Global Settings</span> - </a> - </li> - <li> - <a class="NavItem" ng-class="{ 'is--selected' : isActive('/superadmin/organization')}" href="/superadmin/organization/"> - <span class="NavItem-text">Organizations</span> - </a> - </li> - </ul> - - <!-- user profile dropdown --> - <mb-profile-link user="user" context="nav"></mb-profile-link> - </div> - </nav> - </div> <div class="MainContent flex flex-column full-height" ng-view></div> diff --git a/resources/migrations/006_disconnect_orgs.json b/resources/migrations/006_disconnect_orgs.json new file mode 100644 index 0000000000000000000000000000000000000000..fb792b15d204d8ecf61683c5192db479b037715d --- /dev/null +++ b/resources/migrations/006_disconnect_orgs.json @@ -0,0 +1,90 @@ +{ + "databaseChangeLog": [ + { + "changeSet": { + "id": "6", + "author": "agilliland", + "changes": [ + { + "dropNotNullConstraint": { + "tableName": "metabase_database", + "columnDataType": "int", + "columnName": "organization_id" + } + }, + { + "dropForeignKeyConstraint": { + "baseTableName": "metabase_database", + "constraintName": "fk_database_ref_organization_id" + } + }, + { + "dropNotNullConstraint": { + "tableName": "report_card", + "columnDataType": "int", + "columnName": "organization_id" + } + }, + { + "dropForeignKeyConstraint": { + "baseTableName": "report_card", + "constraintName": "fk_card_ref_organization_id" + } + }, + { + "dropNotNullConstraint": { + "tableName": "report_dashboard", + "columnDataType": "int", + "columnName": "organization_id" + } + }, + { + "dropForeignKeyConstraint": { + "baseTableName": "report_dashboard", + "constraintName": "fk_dashboard_ref_organization_id" + } + }, + { + "dropNotNullConstraint": { + "tableName": "report_emailreport", + "columnDataType": "int", + "columnName": "organization_id" + } + }, + { + "dropForeignKeyConstraint": { + "baseTableName": "report_emailreport", + "constraintName": "fk_emailreport_ref_organization_id" + } + }, + { + "dropNotNullConstraint": { + "tableName": "report_emailreportexecutions", + "columnDataType": "int", + "columnName": "organization_id" + } + }, + { + "dropForeignKeyConstraint": { + "baseTableName": "report_emailreportexecutions", + "constraintName": "fk_emailreportexecutions_ref_organization_id" + } + }, + { + "dropNotNullConstraint": { + "tableName": "annotation_annotation", + "columnDataType": "int", + "columnName": "organization_id" + } + }, + { + "dropForeignKeyConstraint": { + "baseTableName": "annotation_annotation", + "constraintName": "fk_annotation_ref_organization_id" + } + } + ] + } + } + ] +} diff --git a/resources/migrations/liquibase.json b/resources/migrations/liquibase.json index 0695faf0ae5975561f772c1ed6fa9ba10139d56e..ba2d99753d644777803e605526db1a1f82a2e165 100644 --- a/resources/migrations/liquibase.json +++ b/resources/migrations/liquibase.json @@ -3,6 +3,7 @@ {"include": {"file": "migrations/001_initial_schema.json"}}, {"include": {"file": "migrations/002_add_session_table.json"}}, {"include": {"file": "migrations/004_add_setting_table.json"}}, - {"include": {"file": "migrations/005_add_org_report_tz_column.json"}} + {"include": {"file": "migrations/005_add_org_report_tz_column.json"}}, + {"include": {"file": "migrations/006_disconnect_orgs.json"}} ] } diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj index 5cc79bc5cc40d86675bd92a7978158e39b9913d5..800eb3c62c1d4516d1cbe2b7485e054a3db6b11b 100644 --- a/src/metabase/api/card.clj +++ b/src/metabase/api/card.clj @@ -8,7 +8,6 @@ [card :refer [Card] :as card] [card-favorite :refer [CardFavorite]] [common :as common] - [org :refer [Org]] [user :refer [User]]))) (defannotation CardFilterOption @@ -22,18 +21,17 @@ (checkp-contains? card/display-types symb (keyword value))) (defendpoint GET "/" - "Get all the `Cards` for an `Org`. With param `f` (default is `all`), restrict cards as follows: + "Get all the `Cards`. With param `f` (default is `all`), restrict cards as follows: - * `all` Return all `Cards` for `Org` which were created by current user or are publicly visible - * `mine` Return all `Cards` for `Org` created by current user + * `all` Return all `Cards` which were created by current user or are publicly visible + * `mine` Return all `Cards` created by current user * `fav` Return all `Cards` favorited by the current user" - [org f] - {org Required, f CardFilterOption} - (read-check Org org) + [f] + {f CardFilterOption} (-> (case (or f :all) ; default value for `f` is `:all` - :all (sel :many Card :organization_id org (order :name :ASC) (where (or {:creator_id *current-user-id*} - {:public_perms [> common/perms-none]}))) - :mine (sel :many Card :organization_id org :creator_id *current-user-id* (order :name :ASC)) + :all (sel :many Card (order :name :ASC) (where (or {:creator_id *current-user-id*} + {:public_perms [> common/perms-none]}))) + :mine (sel :many Card :creator_id *current-user-id* (order :name :ASC)) :fav (->> (-> (sel :many [CardFavorite :card_id] :owner_id *current-user-id*) (hydrate :card)) (map :card) @@ -42,19 +40,17 @@ (defendpoint POST "/" "Create a new `Card`." - [:as {{:keys [dataset_query description display name organization public_perms visualization_settings]} :body}] + [:as {{:keys [dataset_query description display name public_perms visualization_settings]} :body}] {name [Required NonEmptyString] public_perms [Required PublicPerms] display [Required CardDisplayType]} ;; TODO - which other params are required? - (read-check Org organization) (ins Card :creator_id *current-user-id* :dataset_query dataset_query :description description :display display :name name - :organization_id organization :public_perms public_perms :visualization_settings visualization_settings)) @@ -63,7 +59,7 @@ [id] (->404 (sel :one Card :id id) read-check - (hydrate :can_read :can_write :organization))) + (hydrate :can_read :can_write))) (defendpoint PUT "/:id" "Update a `Card`." diff --git a/src/metabase/api/common.clj b/src/metabase/api/common.clj index a7d33ff3509262f952ac1545bb8684ba4e7ca971..66f75aedb6c9f683f91f389d5ccbe781a49eb11c 100644 --- a/src/metabase/api/common.clj +++ b/src/metabase/api/common.clj @@ -39,33 +39,6 @@ *current-user* (delay (sel :one 'metabase.models.user/User :id ~user-id))] ~@body)) -(defn current-user-perms-for-org - "TODO - A very similar implementation exists in `metabase.models`. Find some way to combine them." - [org-id] - (when *current-user-id* - (let [[{org-id :id - [{admin? :admin}] :org-perms}] (select (-> (entity->korma 'metabase.models.org/Org) ; this is a complicated join but Org permissions checking - (entity-fields [:id])) ; is a very common case so optimization is worth it here - (where {:id org-id}) - (with (-> (entity->korma 'metabase.models.org-perm/OrgPerm) - (entity-fields [:admin])) - (where {:organization_id org-id - :user_id *current-user-id*}))) - superuser? (sel :one :field ['metabase.models.user/User :is_superuser] :id *current-user-id*)] - (check-404 org-id) - (cond - superuser? :admin - admin? :admin - (false? admin?) :default ; perm still exists but admin = false - :else nil)))) - -(defmacro org-perms-case - "Evaluates BODY inside a case statement based on `*current-user*`'s perms for Org with ORG-ID. - Case will be `nil`, `:default`, or `:admin`." - [org-id & body] - `(case (current-user-perms-for-org ~org-id) - ~@body)) - ;;; ## CONDITIONAL RESPONSE FUNCTIONS / MACROS @@ -469,9 +442,7 @@ (check-403 @~'can_read) obj#)) ([entity id] - (if (= (name entity) "Org") - `(check-403 (current-user-perms-for-org ~id)) ; current-user-perms-for-org is faster, optimize this common usage - `(read-check (sel :one ~entity :id ~id))))) + `(read-check (sel :one ~entity :id ~id)))) (defmacro write-check "Checks that `@can_write` is true for this object." @@ -480,6 +451,4 @@ (check-403 @~'can_write) obj#)) ([entity id] - (if (= (name entity) "Org") - `(check-403 (= (current-user-perms-for-org ~id) :admin)) - `(write-check (sel :one ~entity :id ~id))))) + `(write-check (sel :one ~entity :id ~id)))) diff --git a/src/metabase/api/common/internal.clj b/src/metabase/api/common/internal.clj index a444719747e64b6319e8a58bf6a87fec11afa512..7399182b15d8e4bc362d676ef34d3b04151be612 100644 --- a/src/metabase/api/common/internal.clj +++ b/src/metabase/api/common/internal.clj @@ -129,8 +129,7 @@ A param with name matching PARAM-PATTERN should be considered to be of AUTO-PARSE-TYPE." [[#"^uuid$" :uuid] [#"^session_id$" :uuid] - [#"^[\w-_]*id$" :int] - [#"^org$" :int]]) + [#"^[\w-_]*id$" :int]]) (defn arg-type "Return a key into `*auto-parse-types*` if ARG has a matching pattern in `*auto-parse-arg-name-patterns*`. diff --git a/src/metabase/api/dash.clj b/src/metabase/api/dash.clj index 860f5cdaa96600846b0a367dcf86ffb5e63f0524..742cfb841dbf7e19bede9a22d035468db5f5deb5 100644 --- a/src/metabase/api/dash.clj +++ b/src/metabase/api/dash.clj @@ -2,40 +2,33 @@ "/api/dash endpoints." (:require [compojure.core :refer [GET POST PUT DELETE]] [korma.core :refer :all] - [medley.core :as medley] [metabase.api.common :refer :all] [metabase.db :refer :all] (metabase.models [hydrate :refer [hydrate]] [card :refer [Card]] [common :as common] [dashboard :refer [Dashboard]] - [dashboard-card :refer [DashboardCard]] - [org :refer [Org]]) - [metabase.util :as u])) + [dashboard-card :refer [DashboardCard]]))) (defendpoint GET "/" - "Get `Dashboards` for an `Org`. With filter option `f` (default `all`), restrict results as follows: + "Get `Dashboards`. With filter option `f` (default `all`), restrict results as follows: - * `all` - Return all `Dashboards` for `Org`. - * `mine` - Return `Dashboards` created by the current user for `Org`." - [org f] - {org Required, f FilterOptionAllOrMine} - (read-check Org org) + * `all` - Return all `Dashboards`. + * `mine` - Return `Dashboards` created by the current user." + [f] + {f FilterOptionAllOrMine} (-> (case (or f :all) - :all (sel :many Dashboard :organization_id org (where (or {:creator_id *current-user-id*} - {:public_perms [> common/perms-none]}))) - :mine (sel :many Dashboard :organization_id org :creator_id *current-user-id*)) - (hydrate :creator :organization))) + :all (sel :many Dashboard (where (or {:creator_id *current-user-id*} + {:public_perms [> common/perms-none]}))) + :mine (sel :many Dashboard :creator_id *current-user-id*)) + (hydrate :creator))) (defendpoint POST "/" "Create a new `Dashboard`." - [:as {{:keys [organization name public_perms] :as body} :body}] + [:as {{:keys [name public_perms] :as body} :body}] {name [Required NonEmptyString] - organization Required public_perms [Required PublicPerms]} - (read-check Org organization) ; any user who has permissions for this Org can make a dashboard (ins Dashboard - :organization_id organization :name name :public_perms public_perms :creator_id *current-user-id*)) @@ -45,7 +38,7 @@ [id] (let-404 [db (-> (sel :one Dashboard :id id) read-check - (hydrate :creator :organization [:ordered_cards [:card :creator]] :can_read :can_write))] + (hydrate :creator [:ordered_cards [:card :creator]] :can_read :can_write))] {:dashboard db})) ; why is this returned with this {:dashboard} wrapper? (defendpoint PUT "/:id" diff --git a/src/metabase/api/meta/db.clj b/src/metabase/api/meta/db.clj index d5fbcde3d662bb684120ead6be2a5cbad1e595ae..3739dfb28f5c88f43f884dbe62373131bda71ec9 100644 --- a/src/metabase/api/meta/db.clj +++ b/src/metabase/api/meta/db.clj @@ -1,9 +1,7 @@ (ns metabase.api.meta.db "/api/meta/db endpoints." - (:require [clojure.tools.logging :as log] - [compojure.core :refer [GET POST PUT DELETE]] + (:require [compojure.core :refer [GET POST PUT DELETE]] [korma.core :refer :all] - [medley.core :as medley] [metabase.api.common :refer :all] [metabase.db :refer :all] [metabase.driver :as driver] @@ -11,7 +9,6 @@ [hydrate :refer [hydrate]] [database :refer [Database]] [field :refer [Field]] - [org :refer [Org org-can-read org-can-write]] [table :refer [Table]]) [metabase.util :as u])) @@ -21,26 +18,19 @@ (checkp-contains? (set (map name (keys driver/available-drivers))) symb value)) (defendpoint GET "/" - "Fetch all `Databases` for an `Org`." - [org] - {org Required} - (let-404 [{:keys [id inherits] :as org} (sel :one Org :id org)] - (read-check org) - (-> (sel :many Database (order :name) (where (if inherits - {} - {:organization_id id}))) - (hydrate :organization)))) + "Fetch all `Databases`." + [] + (sel :many Database (order :name))) (defendpoint POST "/" - "Add a new `Database` for `Org`." - [:as {{:keys [org name engine details] :as body} :body}] - {org Required - name [Required NonEmptyString] + "Add a new `Database`." + [:as {{:keys [name engine details] :as body} :body}] + {name [Required NonEmptyString] engine [Required DBEngine] details [Required Dict]} ;; TODO - we should validate the contents of `details` here based on the engine - (write-check Org org) - (let-500 [new-db (ins Database :organization_id org :name name :engine engine :details details)] + (check-superuser) + (let-500 [new-db (ins Database :name name :engine engine :details details)] ;; kick off background job to gather schema metadata about our new db (future (driver/sync-database! new-db)) ;; make sure we return the newly created db object @@ -74,8 +64,7 @@ (defendpoint GET "/:id" "Get `Database` with ID." [id] - (->404 (sel :one Database :id id) - (hydrate :organization))) + (check-404 (sel :one Database :id id))) (defendpoint PUT "/:id" "Update a `Database`." diff --git a/src/metabase/api/meta/table.clj b/src/metabase/api/meta/table.clj index b161d7776a90d12c7d2b94b03754460192a13017..530e7b75bfac84e6a91c35fc4eb42b8edf4dc7d6 100644 --- a/src/metabase/api/meta/table.clj +++ b/src/metabase/api/meta/table.clj @@ -8,7 +8,6 @@ [database :refer [Database]] [field :refer [Field]] [foreign-key :refer [ForeignKey]] - [org :refer [Org]] [table :refer [Table] :as table]) [metabase.driver :as driver])) @@ -18,18 +17,15 @@ (checkp-contains? table/entity-types symb (keyword value))) (defendpoint GET "/" - "Get all `Tables` for an `Org`." - [org] - {org Required} - (read-check Org org) - (let [db-ids (sel :many :id Database :organization_id org)] - (-> (sel :many Table :active true :db_id [in db-ids] (order :name :ASC)) - (hydrate :db) - ;; if for some reason a Table doesn't have rows set then set it to 0 so UI doesn't barf - (#(map (fn [table] - (cond-> table - (not (:rows table)) (assoc :rows 0))) - %))))) + "Get all `Tables`." + [] + (-> (sel :many Table :active true (order :name :ASC)) + (hydrate :db) + ;; if for some reason a Table doesn't have rows set then set it to 0 so UI doesn't barf + (#(map (fn [table] + (cond-> table + (not (:rows table)) (assoc :rows 0))) + %)))) (defendpoint GET "/:id" diff --git a/src/metabase/api/org.clj b/src/metabase/api/org.clj deleted file mode 100644 index 70d5b14d63d55e88f0d679650a9e378b2edf086b..0000000000000000000000000000000000000000 --- a/src/metabase/api/org.clj +++ /dev/null @@ -1,150 +0,0 @@ -(ns metabase.api.org - (:require [compojure.core :refer [defroutes GET PUT POST DELETE]] - [korma.core :refer [where subselect fields order limit]] - [medley.core :refer :all] - [metabase.api.common :refer :all] - [metabase.db :refer :all] - [metabase.models.hydrate :refer :all] - (metabase.models [common :as common] - [org :refer [Org]] - [org-perm :refer [OrgPerm grant-org-perm]] - [user :refer [User create-user]]) - [metabase.util :as util] - [ring.util.request :as req])) - -(defannotation TimezoneOption - "Param must be a valid option from `metabase.models.common/timezones`." - [symb value :nillable] - (checkp-contains? (set common/timezones) symb value)) - -;; ## /api/org Endpoints - -(defendpoint GET "/form_input" - "Values of options for the create/edit `Organization` UI." - [] - {:timezones common/timezones}) - -(defendpoint GET "/" - "Fetch a list of all `Orgs`. Superusers get all organizations; normal users simpliy see the orgs they are members of." - [] - (if (:is_superuser @*current-user*) - (sel :many Org) - (sel :many Org (where {:id [in (subselect OrgPerm (fields :organization_id) (where {:user_id *current-user-id*}))]})))) - - -(defendpoint POST "/" - "Create a new `Org`. You must be a superuser to do this." - [:as {{:keys [name slug report_timezone] :as body} :body}] - {name [Required NonEmptyString] - slug [Required NonEmptyString] - report_timezone TimezoneOption} ; TODO - check logo_url ? - (check-superuser) - (let-500 [{:keys [id] :as new-org} (->> (util/select-non-nil-keys body :slug :name :description :logo_url :report_timezone) - (mapply ins Org))] - (grant-org-perm id *current-user-id* true) ; now that the Org exists, add the creator as the first admin member - new-org)) ; make sure the api response is still the newly created org - - -;; ## /api/org/:id Endpoints - -(defendpoint GET "/:id" - "Fetch `Org` with ID." - [id] - (->404 (sel :one Org :id id) - read-check)) - - -(defendpoint GET "/slug/:slug" - "Fetch `Org` with given SLUG." - [slug] - (->404 (sel :one Org :slug slug) - read-check)) - - -(defendpoint PUT "/:id" - "Update an `Org`." - [id :as {{:keys [name description logo_url report_timezone]} :body}] - {name NonEmptyString - report_timezone TimezoneOption} - (write-check Org id) - (check-500 (upd Org id - :description description - :logo_url logo_url - :report_timezone report_timezone - :name name)) - (sel :one Org :id id)) - -(defendpoint DELETE "/:id" - "Delete an `Org`. You must be a superuser to do this." - [id] - (check-superuser) - (cascade-delete Org :id id)) - - -;; ## /api/org/:id/members Endpoints - -(defendpoint GET "/:id/members" - "Get a list of `Users` who are members of (i.e., have `OrgPerms` for) `Org`." - [id] - (read-check Org id) - (-> (sel :many OrgPerm :organization_id id) - (hydrate :user))) - - -(defendpoint POST "/:id/members" - "Add a `User` to an `Org`. If user already exists, they'll simply be granted `OrgPerms`; - otherwise, a new `User` will be created." - [id :as {{:keys [first_name last_name email admin] :or {admin false}} :body :as request}] - {admin Boolean - first_name [Required NonEmptyString] - last_name [Required NonEmptyString] - email [Required Email]} - (write-check Org id) - (let [password-reset-url (str (java.net.URL. (java.net.URL. (req/request-url request)) "/auth/forgot_password")) - user-id (:id (or (sel :one [User :id] :email email) ; find user with existing email - if exists then grant perm - (create-user first_name last_name email :send-welcome true :reset-url password-reset-url)))] - (grant-org-perm id user-id admin) - (-> (sel :one OrgPerm :user_id user-id :organization_id id) - (hydrate :user :organization)))) - - -(defendpoint GET "/:id/members/:user-id" - "Get the `OrgPerm` for `Org` with ID and `User` with USER-ID, if it exists. - `User` is returned along with the `OrgPerm`." - [id user-id] - (read-check Org id) - (check-exists? User user-id) - (-> (sel :one OrgPerm :user_id user-id :organization_id id) - (hydrate :user :organization))) - - -(defendpoint POST "/:id/members/:user-id" - "Add an existing `User` to an `Org` (i.e., create an `OrgPerm` for them)." - [id user-id :as {{:keys [admin]} :body}] - {admin Boolean} - (write-check Org id) - (check-exists? User user-id) - (grant-org-perm id user-id admin) - {:success true}) - -;; TODO `POST` and `PUT` endpoints are exactly the same. Do we need both? - -(defendpoint PUT "/:id/members/:user-id" - "Add an existing `User` to an `Org` (i.e., create an `OrgPerm` for them)." - [id user-id :as {{:keys [admin]} :body}] - {admin Boolean} - (write-check Org id) - (check-exists? User user-id) - (grant-org-perm id user-id admin) - {:success true}) - - -(defendpoint DELETE "/:id/members/:user-id" - "Remove a `User` from an `Org` (i.e., delete the `OrgPerm`)" - [id user-id :as {body :body}] - (write-check Org id) - (check-exists? User user-id) - (del OrgPerm :user_id user-id :organization_id id)) - - -(define-routes) diff --git a/src/metabase/api/routes.clj b/src/metabase/api/routes.clj index 4c161223132fff31ceaad55b8bc80242bfa67133..470f277d07800456920f616773fc04cbdb1a2c67 100644 --- a/src/metabase/api/routes.clj +++ b/src/metabase/api/routes.clj @@ -4,7 +4,6 @@ (metabase.api [card :as card] [dash :as dash] [notify :as notify] - [org :as org] [session :as session] [setting :as setting] [setup :as setup] @@ -38,7 +37,6 @@ (context "/meta/field" [] (+auth field/routes)) (context "/meta/table" [] (+auth table/routes)) (context "/notify" [] (+apikey notify/routes)) - (context "/org" [] (+auth org/routes)) (context "/session" [] session/routes) (context "/setting" [] (+auth setting/routes)) (context "/setup" [] setup/routes) diff --git a/src/metabase/api/setting.clj b/src/metabase/api/setting.clj index 88a5574b9af8042e4d24c592f7052b165e2341d4..c14f2e3d7b0db7dd1e738ced307e29e2ff9bc2ce 100644 --- a/src/metabase/api/setting.clj +++ b/src/metabase/api/setting.clj @@ -5,10 +5,12 @@ (metabase.models [setting :as setting]))) (defendpoint GET "/" - "Get all `Settings` and their values. You must be a superuser to do this." + "Get all `Settings` and their values. Superusers get all settings, normal users get public settings only." [] - (check-superuser) - (setting/all-with-descriptions)) + (if (:is_superuser @*current-user*) + (setting/all-with-descriptions) + ;; TODO - we could make this a little more dynamic + (filter #(= (:key %) :site-name) (setting/all-with-descriptions)))) (defendpoint GET "/:key" "Fetch a single `Setting`. You must be a superuser to do this." diff --git a/src/metabase/api/user.clj b/src/metabase/api/user.clj index f31e6a9901d8200482afcd05242a81c772c96142..2f6a9f078d3c554db5c1523ae44943e119e226fd 100644 --- a/src/metabase/api/user.clj +++ b/src/metabase/api/user.clj @@ -1,12 +1,13 @@ (ns metabase.api.user (:require [cemerick.friend.credentials :as creds] - [compojure.core :refer [defroutes GET PUT]] + [compojure.core :refer [defroutes GET DELETE POST PUT]] [medley.core :refer [mapply]] [metabase.api.common :refer :all] [metabase.db :refer [sel upd upd-non-nil-keys exists?]] (metabase.models [hydrate :refer [hydrate]] - [user :refer [User set-user-password]]) - [metabase.util.password :as password])) + [user :refer [User create-user set-user-password]]) + [metabase.util.password :as password] + [ring.util.request :as req])) (defn ^:private check-self-or-superuser "Check that USER-ID is `*current-user-id*` or that `*current-user*` is a superuser, or throw a 403." @@ -16,24 +17,36 @@ (:is_superuser @*current-user*)))) (defendpoint GET "/" - "Fetch a list of all `Users`. You must be a superuser to do this." + "Fetch a list of all active `Users`. You must be a superuser to do this." [] (check-superuser) - (sel :many User)) + (sel :many User :is_active true)) + + +(defendpoint POST "/" + "Create a new `User`." + [:as {{:keys [first_name last_name email]} :body :as request}] + {first_name [Required NonEmptyString] + last_name [Required NonEmptyString] + email [Required Email]} + (check-superuser) + (check-400 (not (exists? User :email email :is_active true))) + (let [password-reset-url (str (java.net.URL. (java.net.URL. (req/request-url request)) "/auth/forgot_password"))] + (-> (create-user first_name last_name email :send-welcome true :reset-url password-reset-url) + (hydrate :user :organization)))) (defendpoint GET "/current" - "Fetch the current user, their `OrgPerms`, and associated `Orgs`." + "Fetch the current `User`." [] - (->404 @*current-user* - (hydrate [:org_perms :organization]))) + (check-404 @*current-user*)) (defendpoint GET "/:id" "Fetch a `User`. You must be fetching yourself *or* be a superuser." [id] (check-self-or-superuser id) - (check-404 (sel :one User :id id))) + (check-404 (sel :one User :id id :is_active true))) (defendpoint PUT "/:id" @@ -43,6 +56,7 @@ first_name NonEmptyString last_name NonEmptyString} (check-self-or-superuser id) + (check-404 (exists? User :id id :is_active true)) ; only allow updates if the specified account is active (check-400 (not (exists? User :email email :id [not= id]))) ; can't change email if it's already taken BY ANOTHER ACCOUNT (check-500 (upd-non-nil-keys User id :email email @@ -57,11 +71,20 @@ {password [Required ComplexPassword] old_password Required} (check-self-or-superuser id) - (let-404 [user (sel :one [User :password_salt :password] :id id)] + (let-404 [user (sel :one [User :password_salt :password] :id id :is_active true)] (check (creds/bcrypt-verify (str (:password_salt user) old_password) (:password user)) [400 "password mismatch"])) (set-user-password id password) (sel :one User :id id)) +(defendpoint DELETE "/:id" + "Disable a `User`. This does not remove the `User` from the db and instead disables their account." + [id] + (check-superuser) + (check-500 (upd User id + :is_active false)) + {:success true}) + + (define-routes) diff --git a/src/metabase/core.clj b/src/metabase/core.clj index 08d8ab7d38551d636749995aeb539e61ca757076..2e269bcd1f7b72c9dbc4d0f54eb4fc3e76748101 100644 --- a/src/metabase/core.clj +++ b/src/metabase/core.clj @@ -9,6 +9,7 @@ (metabase.middleware [auth :as auth] [log-api-call :refer :all] [format :refer :all]) + [metabase.models.setting :refer [defsetting]] [metabase.models.user :refer [User]] [metabase.routes :as routes] [metabase.setup :as setup] @@ -22,6 +23,10 @@ [params :refer [wrap-params]] [session :refer [wrap-session]]))) +;; ## CONFIG + +(defsetting site-name "The name used for this instance of Metabase." "Metabase") + (def app "The primary entry point to the HTTP server" diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj index 149d859eaaecf99da8725b6e42441c50dc0c27d8..2d0078a7ff8f64b9804c3d75a145872ece94bb92 100644 --- a/src/metabase/driver.clj +++ b/src/metabase/driver.clj @@ -1,17 +1,22 @@ (ns metabase.driver (:require clojure.java.classpath [clojure.tools.logging :as log] - [clojure.tools.namespace.find :as ns-find] [medley.core :refer :all] [metabase.db :refer [exists? ins sel upd]] (metabase.driver [interface :as i] [query-processor :as qp]) (metabase.models [database :refer [Database]] [query-execution :refer [QueryExecution]]) + [metabase.models.setting :refer [defsetting]] [metabase.util :as u])) (declare -dataset-query query-fail query-complete save-query-execution) +;; ## CONFIG + +(defsetting report-timezone "Connection timezone to use when executing queries. Defaults to system timezone.") + + ;; ## Constants (def ^:const available-drivers diff --git a/src/metabase/driver/generic_sql/native.clj b/src/metabase/driver/generic_sql/native.clj index 6e6f1045bda3191e5dffdb9dbeb93d36d8dbd664..184f82ea9fd04cadcb1e60fda91286ff46f4c419 100644 --- a/src/metabase/driver/generic_sql/native.clj +++ b/src/metabase/driver/generic_sql/native.clj @@ -35,7 +35,7 @@ [columns & [first-row :as rows]] (jdbc/with-db-transaction [conn db :read-only? true] ;; If timezone is specified in the Query and the driver supports setting the timezone then execute SQL to set it (when-let [timezone (or (-> query :native :timezone) - (-> @(:organization database) :report_timezone))] + (driver/report-timezone))] (when-let [timezone->set-timezone-sql (:timezone->set-timezone-sql (driver/database-id->driver database-id))] (log/debug "Setting timezone to:" timezone) (jdbc/db-do-prepared conn (timezone->set-timezone-sql timezone)))) diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj index 37179349173b0b5de03251e2a2700176223aae29..97ab1ff8de6d537f9511c5b070e670b647803224 100644 --- a/src/metabase/models/card.clj +++ b/src/metabase/models/card.clj @@ -1,10 +1,8 @@ (ns metabase.models.card - (:require [cheshire.core :as cheshire] - [korma.core :refer :all] - [metabase.api.common :refer [*current-user-id* org-perms-case]] + (:require [korma.core :refer :all] + [metabase.api.common :refer [*current-user-id*]] [metabase.db :refer :all] (metabase.models [common :refer :all] - [org :refer [Org]] [user :refer [User]]))) (def ^:const display-types @@ -28,10 +26,9 @@ timestamped (assoc :hydration-keys #{:card})) -(defmethod post-select Card [_ {:keys [organization_id creator_id] :as card}] +(defmethod post-select Card [_ {:keys [creator_id] :as card}] (-> (assoc card - :creator (delay (sel :one User :id creator_id)) - :organization (delay (sel :one Org :id organization_id))) + :creator (delay (sel :one User :id creator_id))) assoc-permissions-sets)) (defmethod pre-cascade-delete Card [_ {:keys [id]}] diff --git a/src/metabase/models/common.clj b/src/metabase/models/common.clj index 55a044f0560115dc0e91155c9b2efdfeb5782e60..93c47e80a33ba6820887bfdcb37d52c2c997bf8c 100644 --- a/src/metabase/models/common.clj +++ b/src/metabase/models/common.clj @@ -1,5 +1,5 @@ (ns metabase.models.common - (:require [metabase.api.common :refer [*current-user-id* check org-perms-case]] + (:require [metabase.api.common :refer [*current-user* *current-user-id* check]] [metabase.util :as u])) (def timezones @@ -38,17 +38,14 @@ 2 #{:read :write}} public_perms)) (defn user-permissions - "Return the set of current user's permissions for some object with keys `:creator_id`, `:organization_id`, and `:public_perms`." - [{:keys [creator_id organization_id public_perms] :as obj}] + "Return the set of current user's permissions for some object with keys `:creator_id` and `:public_perms`." + [{:keys [creator_id public_perms] :as obj}] (check creator_id 500 "Can't check user permissions: object doesn't have :creator_id." - organization_id 500 "Can't check user permissions: object doesn't have :organization_id." public_perms 500 "Can't check user permissions: object doesn't have :public_perms.") - (if (= creator_id *current-user-id*) #{:read :write} ; if user created OBJ they have all permissions - (org-perms-case (if (delay? organization_id) @organization_id - organization_id) - nil #{} ; if user has no permissions for OBJ's Org then they have none for OBJ - :admin #{:read :write} ; if user is an admin they have all permissions - :default (public-permissions obj)))) + (cond (:is_superuser *current-user*) #{:read :write} ; superusers have full access to everything + (= creator_id *current-user-id*) #{:read :write} ; if user created OBJ they have all permissions + (<= perms-read public_perms) #{:read} ; if the object is public then everyone gets :read + :else #{})) ; default is user has no permissions a.k.a private (defn user-can? "Check if *current-user* has a given PERMISSION for OBJ. @@ -64,9 +61,7 @@ * `:can_read` * `:can_write` - Note that these delays depend upon the presence of `creator_id`, `organization_id`, and `public_perms` - fields in OBJ. `organization_id` may be a delay in case a DB call is neccesary to determine it (e.g. - determining the `organization_id` of a `Query` requires fetching the corresponding `Database`." + Note that these delays depend upon the presence of `creator_id`, and `public_perms` fields in OBJ." [obj] (u/assoc* obj :public-permissions-set (delay (public-permissions <>)) diff --git a/src/metabase/models/dashboard.clj b/src/metabase/models/dashboard.clj index 16a6dcbed468ccb62ad40fd75e02dea17144df55..4c769ca7628153a32ca3d1882c8374ecc830f880 100644 --- a/src/metabase/models/dashboard.clj +++ b/src/metabase/models/dashboard.clj @@ -3,7 +3,6 @@ [metabase.db :refer :all] (metabase.models [common :refer :all] [dashboard-card :refer [DashboardCard]] - [org :refer [Org]] [user :refer [User]]) [metabase.util :as u])) @@ -11,11 +10,10 @@ (table :report_dashboard) timestamped) -(defmethod post-select Dashboard [_ {:keys [id creator_id organization_id description] :as dash}] +(defmethod post-select Dashboard [_ {:keys [id creator_id description] :as dash}] (-> dash (assoc :creator (delay (sel :one User :id creator_id)) :description (u/jdbc-clob->str description) - :organization (delay (sel :one Org :id organization_id)) :ordered_cards (delay (sel :many DashboardCard :dashboard_id id (order :created_at :asc)))) assoc-permissions-sets)) diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj index c08cc5485cde5fcb66a6ed329dbac6388eb92ae1..9291b9fdb3877839062cef93df28782c7796bbd9 100644 --- a/src/metabase/models/database.clj +++ b/src/metabase/models/database.clj @@ -1,7 +1,8 @@ (ns metabase.models.database (:require [korma.core :refer :all] + [metabase.api.common :refer [*current-user*]] [metabase.db :refer :all] - [metabase.models.org :refer [Org org-can-read org-can-write]])) + [metabase.models.common :refer [assoc-permissions-sets]])) (defentity Database @@ -12,21 +13,10 @@ (assoc :hydration-keys #{:database :db})) -(defmethod post-select Database [_ {:keys [organization_id] :as db}] +(defmethod post-select Database [_ db] (assoc db - :organization (delay (sel :one Org :id organization_id)) - :can_read (delay (org-can-read organization_id)) - :can_write (delay (org-can-write organization_id)))) + :can_read (delay true) + :can_write (delay (:is_superuser @*current-user*)))) (defmethod pre-cascade-delete Database [_ {:keys [id] :as database}] (cascade-delete 'metabase.models.table/Table :db_id id)) - -(defn databases-for-org - "Selects the ID and NAME for all databases available to the given org-id." - [org-id] - (when-let [org (sel :one Org :id org-id)] - (if (:inherits org) - ;; inheriting orgs see ALL databases - (sel :many [Database :id :name] (order :name :ASC)) - ;; otherwise filter by org-id - (sel :many [Database :id :name] :organization_id org-id (order :name :ASC))))) diff --git a/src/metabase/models/org.clj b/src/metabase/models/org.clj deleted file mode 100644 index 8494cfb47bff4045075fe7aa183e6f6b958cd152..0000000000000000000000000000000000000000 --- a/src/metabase/models/org.clj +++ /dev/null @@ -1,39 +0,0 @@ -(ns metabase.models.org - (:require [korma.core :refer :all] - [metabase.api.common :refer :all] - [metabase.db :refer :all] - [metabase.models.org-perm :refer [OrgPerm]])) - -(defentity Org - (table :core_organization) - (has-many OrgPerm {:fk :organization_id}) - (transform #(clojure.set/rename-keys % {:core_userorgperm :org-perms})) - (assoc :hydration-keys #{:organization})) - -(defn org-can-read - "Does `*current-user*` have read permissions for `Org` with ORG-ID?" - [org-id] - (org-perms-case org-id - :admin true - :default true - nil false)) - -(defn org-can-write - "Does `*current-user*` have write permissions for `Org` with ORG-ID?" - [org-id] - (org-perms-case org-id - :admin true - :default false - nil false)) - -(defmethod post-select Org [_ {:keys [id] :as org}] - (assoc org - :can_read (delay (org-can-read id)) - :can_write (delay (org-can-write id)))) - -(defmethod pre-insert Org [_ org] - (let [defaults {:inherits false}] - (merge defaults org))) - -(defmethod pre-cascade-delete Org [_ {:keys [id]}] - (cascade-delete OrgPerm :organization_id id)) diff --git a/src/metabase/models/org_perm.clj b/src/metabase/models/org_perm.clj deleted file mode 100644 index a632b6969956c5659197417d2c90a3efbd163c5f..0000000000000000000000000000000000000000 --- a/src/metabase/models/org_perm.clj +++ /dev/null @@ -1,27 +0,0 @@ -(ns metabase.models.org-perm - (:require [korma.core :refer :all] - [metabase.db :refer :all])) - - -(defentity OrgPerm - (table :core_userorgperm)) - - -(defmethod post-select OrgPerm [_ {:keys [organization_id user_id] :as org-perm}] - (assoc org-perm - :organization (delay (sel :one 'metabase.models.org/Org :id organization_id)) - :user (delay (sel :one 'metabase.models.user/User :id user_id)))) - - -(defn grant-org-perm - "Grants permission for given User on Org. Creates record if needed, otherwise updates existing record." - [org-id user-id is-admin] - (let [perm (sel :one OrgPerm :user_id user-id :organization_id org-id) - is-admin (boolean is-admin)] - (if-not perm - (ins OrgPerm - :user_id user-id - :organization_id org-id - :admin is-admin) - (upd OrgPerm (:id perm) - :admin is-admin)))) diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj index a56a30b0331fab450626448ed6287314f4c5e271..2358f4b3ff352a2e0cdf6889266ff8376b870ef7 100644 --- a/src/metabase/models/user.clj +++ b/src/metabase/models/user.clj @@ -3,14 +3,12 @@ [korma.core :refer :all] [metabase.db :refer :all] [metabase.email.messages :as email] - (metabase.models [org-perm :refer [OrgPerm]]) [metabase.util :as u])) ;; ## Enity + DB Multimethods (defentity User (table :core_user) - (has-many OrgPerm {:fk :user_id}) (assoc :hydration-keys #{:author :creator :user})) ;; fields to return for Users other `*than current-user*` @@ -29,23 +27,9 @@ [:is_active :is_staff])) ; but not `password` ! -(defn user-perms-for-org - "Return the permissions level User with USER-ID has for Org with ORG-ID. - - nil -> no permissions - :default -> default permissions - :admin -> admin permissions" - [user-id org-id] - (when-let [{superuser? :is_superuser} (sel :one [User :is_superuser] :id user-id)] - (if superuser? :admin - (when-let [{admin? :admin} (sel :one [OrgPerm :admin] :user_id user-id :organization_id org-id)] - (if admin? :admin :default))))) - -(defmethod post-select User [_ {:keys [id] :as user}] +(defmethod post-select User [_ user] (-> user - (assoc :org_perms (delay (sel :many OrgPerm :user_id id)) - :perms-for-org (memoize (partial user-perms-for-org id)) - :common_name (str (:first_name user) " " (:last_name user))))) + (assoc :common_name (str (:first_name user) " " (:last_name user))))) (defmethod pre-insert User [_ {:keys [email password] :as user}] (assert (u/is-email? email)) @@ -69,7 +53,6 @@ user) (defmethod pre-cascade-delete User [_ {:keys [id]}] - (cascade-delete 'metabase.models.org-perm/OrgPerm :user_id id) (cascade-delete 'metabase.models.session/Session :user_id id)) @@ -103,13 +86,3 @@ :password password :reset_token nil :reset_triggered nil))) - - -(defn users-for-org - "Selects the ID and NAME for all users available to the given org-id." - [org-id] - (->> - (sel :many [User :id :first_name :last_name] - (where {:id [in (subselect OrgPerm (fields :user_id) (where {:organization_id org-id}))]})) - (map #(select-keys % [:id :common_name])) - (map #(clojure.set/rename-keys % {:common_name :name})))) diff --git a/test/metabase/api/card_test.clj b/test/metabase/api/card_test.clj index 44cc9de54ef70b03e32783cd12d8dc0d82c2e86d..68034e4ab4215a38fc74e611162af7912eadc791 100644 --- a/test/metabase/api/card_test.clj +++ b/test/metabase/api/card_test.clj @@ -12,8 +12,7 @@ ;; ## Helper fns (defn post-card [card-name] - ((user->client :rasta) :post 200 "card" {:organization @org-id - :name card-name + ((user->client :rasta) :post 200 "card" {:name card-name :public_perms 0 :can_read true :can_write true @@ -33,13 +32,12 @@ false] (with-temp Card [{:keys [id]} {:name (random-name) :public_perms common/perms-none - :organization_id @org-id :creator_id (user->id :crowberto) :display :table :dataset_query {} :visualization_settings {}}] (let [can-see-card? (fn [user] - (contains? (->> ((user->client user) :get 200 "card" :org @org-id :f :all) + (contains? (->> ((user->client user) :get 200 "card" :f :all) (map :id) set) id))] @@ -52,7 +50,7 @@ (let [card-name (random-name)] (expect-eval-actual-first (match-$ (sel :one Card :name card-name) {:description nil - :organization_id @org-id + :organization_id nil :name card-name :creator_id (user->id :rasta) :updated_at $ @@ -78,15 +76,8 @@ {:description nil :can_read true :can_write true - :organization_id @org-id + :organization_id nil :name card-name - :organization {:inherits true - :report_timezone nil - :logo_url nil - :description nil - :name "Test Organization" - :slug "test" - :id @org-id} :creator_id (user->id :rasta) :updated_at $ :dataset_query {:type "query" diff --git a/test/metabase/api/common/internal_test.clj b/test/metabase/api/common/internal_test.clj index fa71ce7660331759c0a4e09f996baaf6ff709b3e..841f2a3a14c461b7d780df82c7ac0acfe9e5607f 100644 --- a/test/metabase/api/common/internal_test.clj +++ b/test/metabase/api/common/internal_test.clj @@ -26,9 +26,6 @@ (expect :int (arg-type :card-id)) -(expect :int - (arg-type 'org)) - ;;; TESTS FOR ROUTE-PARAM-REGEX diff --git a/test/metabase/api/common_test.clj b/test/metabase/api/common_test.clj index 0e95917493db33e32e3501f0774f70906e937eb8..a812d4d04a26201ae2d5a513f9933b339e58eba8 100644 --- a/test/metabase/api/common_test.clj +++ b/test/metabase/api/common_test.clj @@ -2,43 +2,12 @@ (:require [expectations :refer :all] [metabase.api.common :refer :all] [metabase.api.common.internal :refer :all] - [metabase.api.org-test :refer [create-org]] [metabase.test-data :refer :all] [metabase.test-data.create :refer [create-user]] [metabase.test.util :refer :all] [metabase.util :refer [regex= regex?]]) (:import com.metabase.corvus.api.ApiException)) -;;; TESTS FOR CURRENT-USER-PERMS-FOR-ORG -;; admins should get :admin -(expect :admin - (with-current-user (user->id :rasta) - (current-user-perms-for-org @org-id))) - -;; superusers should always get :admin whether they have org perms or not -(expect-let [{org-id :id} (create-org (random-name))] - :admin - (with-current-user (user->id :crowberto) - (current-user-perms-for-org org-id))) - -(expect :admin - (with-current-user (user->id :crowberto) - (current-user-perms-for-org @org-id))) - -;; other users should get :default or nil depending on if they have an Org Perm -(expect :default - (with-current-user (user->id :lucky) - (current-user-perms-for-org @org-id))) - -(expect nil - (with-current-user (:id (create-user)) - (current-user-perms-for-org @org-id))) - -;; Should get a 404 for an Org that doesn't exist -(expect ApiException - (with-current-user (user->id :crowberto) - (current-user-perms-for-org 1000))) - ;;; TESTS FOR CHECK (ETC) diff --git a/test/metabase/api/dash_test.clj b/test/metabase/api/dash_test.clj index 7c2df78d8e0201d21d5b473387a3d420a188b038..ea4e364137b1d656ae9323afb9e640a40de4bf25 100644 --- a/test/metabase/api/dash_test.clj +++ b/test/metabase/api/dash_test.clj @@ -18,7 +18,6 @@ ;; ## Helper Fns (defn create-dash [dash-name] ((user->client :rasta) :post 200 "dash" {:name dash-name - :organization @org-id :public_perms 0})) ;; ## POST /api/dash @@ -27,7 +26,7 @@ (expect-eval-actual-first (match-$ (sel :one Dashboard :name dash-name) {:description nil - :organization_id 1 + :organization_id nil :name dash-name :creator_id (user->id :rasta) :updated_at $ @@ -47,11 +46,8 @@ :creator (-> (sel :one User :id (user->id :rasta)) (select-keys [:email :first_name :last_login :is_superuser :id :last_name :date_joined :common_name])) :can_write true - :organization_id @org-id + :organization_id nil :name $ - :organization (-> @test-org - (select-keys [:inherits :report_timezone :logo_url :description :name :slug :id - ])) :creator_id (user->id :rasta) :updated_at $ :id $ @@ -64,10 +60,9 @@ false] (with-temp Dashboard [{:keys [id]} {:name (random-name) :public_perms common/perms-none - :organization_id @org-id :creator_id (user->id :crowberto)}] (let [can-see-dash? (fn [user] - (contains? (->> ((user->client user) :get 200 "dash" :org @org-id :f :all) + (contains? (->> ((user->client user) :get 200 "dash" :f :all) (map :id) set) id))] @@ -121,7 +116,7 @@ {:description nil :creator (-> (sel :one User :id (user->id :rasta)) (select-keys [:date_joined :last_name :id :is_superuser :last_login :first_name :email :common_name])) - :organization_id @org-id + :organization_id nil :name $ :creator_id (user->id :rasta) :updated_at $ diff --git a/test/metabase/api/meta/db_test.clj b/test/metabase/api/meta/db_test.clj index 1f87610344102110e0965d41d9337fb1f523d202..bfa7c7399b6b22a2e2ac93e5657ae14fc58a3548 100644 --- a/test/metabase/api/meta/db_test.clj +++ b/test/metabase/api/meta/db_test.clj @@ -12,10 +12,9 @@ ;; HELPER FNS (defn create-db [db-name] - ((user->client :rasta) :post 200 "meta/db" {:org @org-id - :engine :postgres - :name db-name - :details {:conn_str "host=localhost port=5432 dbname=fakedb user=cam"}})) + ((user->client :crowberto) :post 200 "meta/db" {:engine :postgres + :name db-name + :details {:conn_str "host=localhost port=5432 dbname=fakedb user=cam"}})) ;; # FORM INPUT @@ -44,15 +43,8 @@ :id $ :details $ :updated_at $ - :organization {:id @org-id - :slug "test" - :name "Test Organization" - :description nil - :logo_url nil - :report_timezone nil - :inherits true} :name "Test Database" - :organization_id @org-id + :organization_id nil :description nil}) ((user->client :rasta) :get 200 (format "meta/db/%d" (:id @test-db)))) @@ -67,7 +59,7 @@ :details {:conn_str "host=localhost port=5432 dbname=fakedb user=cam"} :updated_at $ :name db-name - :organization_id @org-id + :organization_id nil :description nil}) (create-db db-name))) @@ -79,7 +71,7 @@ [db-name nil] [(sel-db-name) - (do ((user->client :rasta) :delete 204 (format "meta/db/%d" db-id)) + (do ((user->client :crowberto) :delete 204 (format "meta/db/%d" db-id)) (sel-db-name))]) ;; ## PUT /api/meta/db/:id @@ -98,12 +90,12 @@ :name old-name}] [(sel-db) ;; Check that we can update all the fields - (do ((user->client :rasta) :put 200 (format "meta/db/%d" db-id) {:name new-name - :engine "h2" - :details {:conn_str "host=localhost port=5432 dbname=fakedb user=rastacan"}}) + (do ((user->client :crowberto) :put 200 (format "meta/db/%d" db-id) {:name new-name + :engine "h2" + :details {:conn_str "host=localhost port=5432 dbname=fakedb user=rastacan"}}) (sel-db)) ;; Check that we can update just a single field - (do ((user->client :rasta) :put 200 (format "meta/db/%d" db-id) {:name old-name}) + (do ((user->client :crowberto) :put 200 (format "meta/db/%d" db-id) {:name old-name}) (sel-db))]) ;; # DATABASES FOR ORG @@ -112,57 +104,47 @@ ;; Test that we can get all the DBs for an Org, ordered by name (let [db-name (str "A" (random-name))] ; make sure this name comes before "Test Database" (expect-eval-actual-first - (let [org {:id @org-id - :slug "test" - :name "Test Organization" - :description nil - :logo_url nil - :report_timezone nil - :inherits true}] - (filter identity - [(datasets/when-testing-dataset :generic-sql - (match-$ (sel :one Database :name db-name) - {:created_at $ - :engine "postgres" - :id $ - :details {:conn_str "host=localhost port=5432 dbname=fakedb user=cam"} - :updated_at $ - :organization org - :name $ - :organization_id @org-id - :description nil})) - (datasets/when-testing-dataset :mongo - (match-$ @mongo-test-data/mongo-test-db - {:created_at $ - :engine "mongo" - :id $ - :details $ - :updated_at $ - :organization org - :name "Mongo Test" - :organization_id @org-id - :description nil})) - (match-$ @test-db - {:created_at $ - :engine "h2" - :id $ - :details $ - :updated_at $ - :organization org - :name "Test Database" - :organization_id @org-id - :description nil})])) + (filter identity + [(datasets/when-testing-dataset :generic-sql + (match-$ (sel :one Database :name db-name) + {:created_at $ + :engine "postgres" + :id $ + :details {:conn_str "host=localhost port=5432 dbname=fakedb user=cam"} + :updated_at $ + :name $ + :organization_id nil + :description nil})) + (datasets/when-testing-dataset :mongo + (match-$ @mongo-test-data/mongo-test-db + {:created_at $ + :engine "mongo" + :id $ + :details $ + :updated_at $ + :name "Mongo Test" + :organization_id nil + :description nil})) + (match-$ @test-db + {:created_at $ + :engine "h2" + :id $ + :details $ + :updated_at $ + :name "Test Database" + :organization_id nil + :description nil})]) (do ;; Delete all the randomly created Databases we've made so far - (cascade-delete Database :organization_id @org-id :id [not-in (set (filter identity - [(datasets/when-testing-dataset :generic-sql - @db-id) - (datasets/when-testing-dataset :mongo - @mongo-test-data/mongo-test-db-id)]))]) + (cascade-delete Database :id [not-in (set (filter identity + [(datasets/when-testing-dataset :generic-sql + @db-id) + (datasets/when-testing-dataset :mongo + @mongo-test-data/mongo-test-db-id)]))]) ;; Add an extra DB so we have something to fetch besides the Test DB (create-db db-name) ;; Now hit the endpoint - ((user->client :rasta) :get 200 "meta/db" :org @org-id)))) + ((user->client :rasta) :get 200 "meta/db")))) ;; # DB TABLES ENDPOINTS diff --git a/test/metabase/api/meta/field_test.clj b/test/metabase/api/meta/field_test.clj index c9ca2869a9817c7dd1eb81d5a2aa648cd87f9627..a04f637b388fdbd0e93eee67a439446af8c2251c 100644 --- a/test/metabase/api/meta/field_test.clj +++ b/test/metabase/api/meta/field_test.clj @@ -24,7 +24,7 @@ :details $ :updated_at $ :name "Test Database" - :organization_id @org-id + :organization_id nil :description nil}) :name "USERS" :rows 15 @@ -74,7 +74,7 @@ :preview_display true :created_at $ :base_type "FloatField"}) - ((user->client :rasta) :put 200 (format "meta/field/%d" (field->id :venues :latitude)) {:special_type :fk})) + ((user->client :crowberto) :put 200 (format "meta/field/%d" (field->id :venues :latitude)) {:special_type :fk})) (defn- field->field-values "Fetch the `FieldValues` object that corresponds to a given `Field`." @@ -116,10 +116,10 @@ :updated_at $ :created_at $ :id $})] - [((user->client :rasta) :post 200 (format "meta/field/%d/value_map_update" (field->id :venues :price)) {:values_map {:1 "$" - :2 "$$" - :3 "$$$" - :4 "$$$$"}}) + [((user->client :crowberto) :post 200 (format "meta/field/%d/value_map_update" (field->id :venues :price)) {:values_map {:1 "$" + :2 "$$" + :3 "$$$" + :4 "$$$$"}}) ((user->client :rasta) :get 200 (format "meta/field/%d/values" (field->id :venues :price)))]) ;; Check that we can unset values @@ -136,13 +136,13 @@ :2 "$$" :3 "$$$" :4 "$$$$"}) - ((user->client :rasta) :post 200 (format "meta/field/%d/value_map_update" (field->id :venues :price)) + ((user->client :crowberto) :post 200 (format "meta/field/%d/value_map_update" (field->id :venues :price)) {:values_map {}})) ((user->client :rasta) :get 200 (format "meta/field/%d/values" (field->id :venues :price)))]) ;; Check that we get an error if we call value_map_update on something that isn't a category (expect "You can only update the mapped values of a Field whose 'special_type' is 'category'/'city'/'state'/'country' or whose 'base_type' is 'BooleanField'." - ((user->client :rasta) :post 400 (format "meta/field/%d/value_map_update" (field->id :venues :id)) + ((user->client :crowberto) :post 400 (format "meta/field/%d/value_map_update" (field->id :venues :id)) {:values_map {:1 "$" :2 "$$" :3 "$$$" diff --git a/test/metabase/api/meta/table_test.clj b/test/metabase/api/meta/table_test.clj index 3a190e94620f6c43383415ac5dbbe5024c04cae4..c24cd2611498ce64d347f723bba189dc36f4ec06 100644 --- a/test/metabase/api/meta/table_test.clj +++ b/test/metabase/api/meta/table_test.clj @@ -32,7 +32,7 @@ {:name (datasets/format-name *dataset* "users"), :db_id db-id, :active true, :rows 15, :id (datasets/table-name->id *dataset* :users)} {:name (datasets/format-name *dataset* "venues"), :db_id db-id, :active true, :rows 100, :id (datasets/table-name->id *dataset* :venues)}]))) @datasets/test-dataset-names)) - (->> ((user->client :rasta) :get 200 "meta/table" :org @org-id) + (->> ((user->client :rasta) :get 200 "meta/table") (map #(dissoc % :db :created_at :updated_at :entity_name :description :entity_type)) set)) @@ -48,7 +48,7 @@ :details $ :updated_at $ :name "Test Database" - :organization_id @org-id + :organization_id nil :description nil}) :name "VENUES" :rows 100 @@ -102,7 +102,7 @@ :details $ :updated_at $ :name "Test Database" - :organization_id @org-id + :organization_id nil :description nil}) :name "CATEGORIES" :fields [(match-$ (sel :one Field :id (field->id :categories :id)) @@ -417,7 +417,7 @@ :created_at $ :db (match-$ @test-db {:description nil, - :organization_id 1, + :organization_id nil, :name "Test Database", :updated_at $, :id $, @@ -456,7 +456,7 @@ {:result "success"} (let [categories-id-field (sel :one Field :table_id (table->id :categories) :name "ID") categories-name-field (sel :one Field :table_id (table->id :categories) :name "NAME") - api-response ((user->client :rasta) :post 200 (format "meta/table/%d/reorder" (table->id :categories)) + api-response ((user->client :crowberto) :post 200 (format "meta/table/%d/reorder" (table->id :categories)) {:new_order [(:id categories-name-field) (:id categories-id-field)]})] ;; check the modified values (have to do it here because the api response tells us nothing) (assert (= 0 (:position (sel :one :fields [Field :position] :id (:id categories-name-field))))) diff --git a/test/metabase/api/org_test.clj b/test/metabase/api/org_test.clj deleted file mode 100644 index ea6cc687f950a291b0ed78510a1fe7e59fdf9486..0000000000000000000000000000000000000000 --- a/test/metabase/api/org_test.clj +++ /dev/null @@ -1,565 +0,0 @@ -(ns metabase.api.org-test - (:require [expectations :refer :all] - [metabase.db :refer :all] - [metabase.http-client :as http] - [metabase.middleware.auth :as auth] - (metabase.models [org :refer [Org]] - [org-perm :refer [OrgPerm]] - [user :refer [User]]) - [metabase.test-data :refer :all] - [metabase.test-data.create :refer [create-user]] - [metabase.test.util :refer [match-$ random-name expect-eval-actual-first]])) - -;; Helper Fns - -(defn create-org [org-name] - {:pre [(string? org-name)]} - ((user->client :crowberto) :post 200 "org" {:name org-name - :slug org-name})) - -(defn org-perm-exists? [org-id user-id] - (exists? OrgPerm :organization_id org-id :user_id user-id)) - -(defn create-org-perm [org-id user-id & {:keys [admin] - :or {admin false}}] - ((user->client :crowberto) :post 200 (format "org/%d/members/%d" org-id user-id) {:admin admin})) - - -;; ## /api/org/* AUTHENTICATION Tests -;; We assume that all endpoints for a given context are enforced by the same middleware, so we don't run the same -;; authentication test on every single individual endpoint - -(expect (get auth/response-unauthentic :body) (http/client :get 401 "org")) -(expect (get auth/response-unauthentic :body) (http/client :get 401 (format "org/%d" @org-id))) - - -;; # GENERAL ORG ENDPOINTS - -;; ## GET /api/org/form_input -;; this endpoint is available to any user that is authentic, so no security check here -(expect-eval-actual-first - {:timezones ["GMT" - "UTC" - "US/Alaska" - "US/Arizona" - "US/Central" - "US/Eastern" - "US/Hawaii" - "US/Mountain" - "US/Pacific" - "America/Costa_Rica"]} - ((user->client :rasta) :get 200 "org/form_input")) - -;; ## GET /api/org -;; Non-superusers should only be able to see Orgs they are members of -(let [org-name (random-name)] - (expect-eval-actual-first - [{:id @org-id - :slug "test" - :name "Test Organization" - :description nil - :logo_url nil - :report_timezone nil - :inherits true}] - (do - ;; Delete all the random test Orgs we've created - (cascade-delete Org :id [not= @org-id]) - ;; Create a random Org so we ensure there is an Org that should NOT show up in our list - (create-org org-name) - ;; Now perform the API request - ((user->client :rasta) :get 200 "org")))) - -;; Superusers should be able to see all Orgs -(let [org-name (random-name)] - (expect-eval-actual-first - [{:id @org-id - :slug "test" - :name "Test Organization" - :description nil - :logo_url nil - :report_timezone nil - :inherits true} - (match-$ (sel :one Org :name org-name) - {:id $ - :slug $ - :name $ - :description nil - :logo_url nil - :report_timezone nil - :inherits false})] - (do - ;; Delete all the random test Orgs we've created - (cascade-delete Org :id [not= @org-id]) - ;; Create a random Org so we can check that we still get Orgs we're not members of - (create-org org-name) - ;; Now perform the API request - ((user->client :crowberto) :get 200 "org")))) - - -;; ## POST /api/org -;; Check that non-superusers can't create Orgs -(expect "You don't have permissions to do that." - (let [org-name (random-name)] - ((user->client :rasta) :post 403 "org" {:name org-name - :slug org-name}))) - -;; Check that superusers *can* create Orgs -(let [org-name (random-name)] - (expect-eval-actual-first - (match-$ (sel :one Org :name org-name) - {:id $ - :slug org-name - :name org-name - :description nil - :logo_url nil - :report_timezone nil - :inherits false}) - (let [new-org (create-org org-name) - org-perm (sel :one OrgPerm :organization_id (:id new-org))] - ;; do a quick validation that the creator is now an admin of the new org - (assert (= (:user_id org-perm) (user->id :crowberto))) - (assert (:admin org-perm)) - ;; return the original api response, which should be the newly created org - new-org))) - -;; Test input validations on org create -(expect {:errors {:name "field is a required param."}} - ((user->client :crowberto) :post 400 "org" {})) - -(expect {:errors {:slug "field is a required param."}} - ((user->client :crowberto) :post 400 "org" {:name "anything"})) - - -;; ## GET /api/org/:id -(expect - {:id @org-id - :slug "test" - :name "Test Organization" - :description nil - :logo_url nil - :report_timezone nil - :inherits true} - ((user->client :rasta) :get 200 (format "org/%d" @org-id))) - -;; Check that non-superusers can't access orgs they don't have permissions to -(expect "You don't have permissions to do that." - (let [org-name (random-name) - my-org (create-org org-name)] - ((user->client :rasta) :get 403 (format "org/%d" (:id my-org))))) - -;; Test that invalid org id returns 404 -(expect "Not found." - ((user->client :rasta) :get 404 "org/1000")) - -;; ## GET /api/org/slug/:slug -(expect - {:id @org-id - :slug "test" - :name "Test Organization" - :description nil - :logo_url nil - :report_timezone nil - :inherits true} - ((user->client :rasta) :get 200 (format "org/slug/%s" (:slug @test-org)))) - -;; Check that non-superusers can't access orgs they don't have permissions to -(expect "You don't have permissions to do that." - (let [org-name (random-name) - my-org (create-org org-name)] - ((user->client :rasta) :get 403 (format "org/slug/%s" (:slug my-org))))) - -;; Test that invalid org slug returns 404 -(expect "Not found." - ((user->client :rasta) :get 404 "org/slug/ksdlfkjdkfd")) - - -;; ## PUT /api/org/:id -;; Test that we can update an Org -(expect-let [orig-name (random-name) - upd-name (random-name) - {:keys [id slug inherits] :as org} (create-org orig-name)] - {:id id - :slug slug - :name upd-name - :description upd-name - :logo_url upd-name - :report_timezone "US/Eastern" - :inherits false} - ;; we try setting `slug` & `inherits` which should both remain unmodified - ((user->client :crowberto) :put 200 (format "org/%d" id) {:slug upd-name - :name upd-name - :description upd-name - :logo_url upd-name - :report_timezone "US/Eastern" - :inherits true})) - -;; Check that non-superusers can't modify orgs they don't have permissions to -(expect "You don't have permissions to do that." - (let [org-name (random-name) - my-org (create-org org-name)] - ((user->client :rasta) :put 403 (format "org/%d" (:id my-org)) {}))) - -;; Validate that write-perms are required to modify the org details (with user having read perms on org) -(expect "You don't have permissions to do that." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin false) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :put 403 (format "org/%d" org-id) {}))) - -;; Test that invalid org id returns 404 -(expect "Not found." - ((user->client :rasta) :put 404 "org/1000" {})) - -;; ## DELETE /api/org/:id -(expect - [true - false] - (let [org-name (random-name) - {org-id :id} (ins Org :name org-name :slug org-name)] - [(exists? Org :name org-name) - (do ((user->client :crowberto) :delete 204 (format "org/%d" org-id)) - (exists? Org :name org-name))])) - -;; Check that an admin for Org (non-superuser) can't delete it -(expect "You don't have permissions to do that." - ((user->client :rasta) :delete 403 (format "org/%d" (:id @test-org)))) - - -;; # MEMBERS ENDPOINTS - -;; ## GET /api/org/:id/members -(expect - #{(match-$ (user->org-perm :crowberto) - {:id $ - :admin true - :user_id (user->id :crowberto) - :organization_id @org-id - :user (match-$ (fetch-user :crowberto) - {:common_name "Crowberto Corv" - :date_joined $ - :last_name "Corv" - :id $ - :is_superuser true - :last_login $ - :first_name "Crowberto" - :email "crowberto@metabase.com"})}) - (match-$ (user->org-perm :trashbird) - {:id $ - :admin false - :user_id (user->id :trashbird) - :organization_id @org-id - :user (match-$ (fetch-user :trashbird) - {:common_name "Trash Bird" - :date_joined $ - :last_name "Bird" - :id $ - :is_superuser false - :last_login $ - :first_name "Trash" - :email "trashbird@metabase.com"})}) - (match-$ (user->org-perm :lucky) - {:id $ - :admin false - :user_id (user->id :lucky) - :organization_id @org-id - :user (match-$ (fetch-user :lucky) - {:common_name "Lucky Pigeon" - :date_joined $ - :last_name "Pigeon" - :id $ - :is_superuser false - :last_login $ - :first_name "Lucky" - :email "lucky@metabase.com"})}) - (match-$ (user->org-perm :rasta) - {:id $ - :admin true - :user_id (user->id :rasta) - :organization_id @org-id - :user (match-$ (fetch-user :rasta) - {:common_name "Rasta Toucan" - :date_joined $ - :last_name "Toucan" - :id $ - :is_superuser false - :last_login $ - :first_name "Rasta" - :email "rasta@metabase.com"})})} - (set ((user->client :rasta) :get 200 (format "org/%d/members" @org-id)))) - -;; Check that users without any org perms cannot list members -(expect "You don't have permissions to do that." - (let [{:keys [id]} (create-org (random-name))] - ((user->client :rasta) :get 403 (format "org/%d/members" id) {}))) - -;; Test that invalid org id returns 404 -(expect "Not found." - ((user->client :rasta) :get 404 "org/1000/members")) - - -;; ## POST /api/org/:id/members -;; Check that we can create a new User w/ OrgPerm -(let [test-org-name (random-name) - user-to-create (random-name)] - (expect-eval-actual-first - (let [{my-user-id :id, :as my-user} (sel :one User :first_name user-to-create) - {my-org-id :id} (sel :one Org :name test-org-name)] - (match-$ (first (sel :many OrgPerm :user_id my-user-id)) - {:id $ - :admin false - :user_id my-user-id - :organization_id my-org-id - :organization {:id my-org-id - :slug test-org-name - :name test-org-name - :description nil - :logo_url nil - :report_timezone nil - :inherits false} - :user {:common_name (:common_name my-user) - :date_joined (:date_joined my-user) - :last_name user-to-create - :id my-user-id - :is_superuser false - :last_login (:last_login my-user) - :first_name user-to-create - :email (:email my-user)}})) - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org test-org-name) - my-perm (create-org-perm org-id user-id :admin true) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :post 200 (format "org/%d/members" org-id) {:first_name user-to-create - :last_name user-to-create - :email (str user-to-create "@metabase.com") - :admin false})))) - -;; Check that we can add an Existing User to an Org (so we are NOT creating a new user account) -(let [test-org-name (random-name)] - (expect-eval-actual-first - (let [{my-org-id :id} (sel :one Org :name test-org-name)] - (match-$ (sel :one OrgPerm :organization_id my-org-id :user_id [not= (user->id :crowberto)]) - {:id $ - :admin true - :user_id $ - :organization_id my-org-id})) - (let [{email :email} (create-user) - {org-id :id} (create-org test-org-name)] - (-> ((user->client :crowberto) :post 200 (format "org/%d/members" org-id) {:first_name "anything" - :last_name "anything" - :email email - :admin true}) - (select-keys [:id :admin :user_id :organization_id]))))) - -;; Test input validations on org member create -(expect {:errors {:first_name "field is a required param."}} - ((user->client :crowberto) :post 400 (format "org/%d/members" @org-id) {})) - -(expect {:errors {:last_name "field is a required param."}} - ((user->client :crowberto) :post 400 (format "org/%d/members" @org-id) {:first_name "anything"})) - -(expect {:errors {:email "field is a required param."}} - ((user->client :crowberto) :post 400 (format "org/%d/members" @org-id) {:first_name "anything" - :last_name "anything"})) - -;; this should fail due to invalid formatted email address -(expect {:errors {:email "Invalid value 'anything' for 'email': Not a valid email address."}} - ((user->client :crowberto) :post 400 (format "org/%d/members" @org-id) {:first_name "anything" - :last_name "anything" - :email "anything"})) - -;; Check that users without any org perms cannot modify members -(expect "You don't have permissions to do that." - (let [{:keys [id]} (create-org (random-name))] - ((user->client :rasta) :post 403 (format "org/%d/members" id) {:first_name "anything" - :last_name "anything" - :email "anything@anything.com"}))) - -;; Check that users without WRITE org perms cannot modify members (test user with READ perms on org) -(expect "You don't have permissions to do that." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin false) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :post 403 (format "org/%d/members" org-id) {:first_name "anything" - :last_name "anything" - :email "anything@anything.com"}))) - -;; Test that invalid org id returns 404 -(expect "Not found." - ((user->client :rasta) :post 404 "org/1000/members" {:first_name "anything" - :last_name "anything" - :email "anything@anything.com"})) - - -;; ## GET /api/org/:id/members/:user-id -;; Check that we can get an OrgPerm between existing User + Org -(expect - (match-$ (user->org-perm :lucky) - {:id $ - :admin false - :user_id (user->id :lucky) - :organization_id @org-id - :user (match-$ (fetch-user :lucky) - {:common_name "Lucky Pigeon" - :date_joined $ - :last_name "Pigeon" - :id $ - :is_superuser false - :last_login $ - :first_name "Lucky" - :email "lucky@metabase.com"}) - :organization (match-$ (sel :one Org :id @org-id) - {:id $ - :slug "test" - :name "Test Organization" - :description nil - :logo_url nil - :report_timezone nil - :inherits true})}) - ((user->client :crowberto) :get 200 (format "org/%d/members/%d" @org-id (user->id :lucky)))) - -;; Check that users without any org perms cannot get members -(expect "You don't have permissions to do that." - (let [{:keys [id]} (create-org (random-name))] - ((user->client :rasta) :get 403 (format "org/%d/members/1000" id) {}))) - -;; Test that invalid org id returns 404 -(expect "Not found." - ((user->client :rasta) :get 404 "org/1000/members/1000")) - -;; Test that invalid user id returns 404 -(expect "Not found." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin true) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :get 404 (format "org/%d/members/1000" org-id) {}))) - - -;; ## POST /api/org/:id/members/:user-id -;; Check that we can create an OrgPerm between existing User + Org -(expect [false - true] - (let [{org-id :id} (create-org (random-name)) - {user-id :id} (create-user) - org-perm-exists? (partial org-perm-exists? org-id user-id)] - [(org-perm-exists?) - (do (create-org-perm org-id user-id) - (org-perm-exists?))])) - -;; Check that users without any org perms cannot modify members -(expect "You don't have permissions to do that." - (let [{:keys [id]} (create-org (random-name))] - ((user->client :rasta) :post 403 (format "org/%d/members/1000" id) {}))) - -;; Check that users without WRITE org perms cannot modify members (test user with READ perms on org) -(expect "You don't have permissions to do that." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin false) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :post 403 (format "org/%d/members/1000" org-id) {}))) - -;; Test that invalid org id returns 404 -(expect "Not found." - ((user->client :rasta) :post 404 "org/1000/members/1000")) - -;; Test that invalid user id returns 404 -(expect "Not found." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin true) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :post 404 (format "org/%d/members/1000" org-id) {}))) - - -;; ## DELETE /api/org/:id/members/:user-id -;; Check we can delete OrgPerms between a User + Org -(expect [false - true - false] - (let [{org-id :id} (create-org (random-name)) - {user-id :id} (create-user) - org-perm-exists? (partial org-perm-exists? org-id user-id)] - [(org-perm-exists?) - (do (create-org-perm org-id user-id) - (org-perm-exists?)) - (do ((user->client :crowberto) :delete 204 (format "org/%d/members/%d" org-id user-id)) - (org-perm-exists?))])) - -;; Check that users without any org perms cannot modify members -(expect "You don't have permissions to do that." - (let [{:keys [id]} (create-org (random-name))] - ((user->client :rasta) :delete 403 (format "org/%d/members/1000" id) {}))) - -;; Check that users without WRITE org perms cannot modify members (test user with READ perms on org) -(expect "You don't have permissions to do that." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin false) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :delete 403 (format "org/%d/members/1000" org-id) {}))) - -;; Test that invalid org id returns 404 -(expect "Not found." - ((user->client :rasta) :delete 404 "org/1000/members/1000")) - -;; Test that invalid user id returns 404 -(expect "Not found." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin true) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :delete 404 (format "org/%d/members/1000" org-id) {}))) - - -;; ## PUT /api/org/:id/members/:user-id -;; Check that we can edit an exisiting OrgPerm (i.e., toggle 'admin' status) -(expect - [nil - false - true] - (let [{org-id :id} (create-org (random-name)) - {user-id :id} (create-user) - is-admin? (fn [] (sel :one :field [OrgPerm :admin] :user_id user-id :organization_id org-id))] - [(is-admin?) - (do (create-org-perm org-id user-id) - (is-admin?)) - (do ((user->client :crowberto) :put 200 (format "org/%d/members/%d" org-id user-id) {:admin true}) - (is-admin?))])) - -;; Check that users without any org perms cannot modify members -(expect "You don't have permissions to do that." - (let [{:keys [id]} (create-org (random-name))] - ((user->client :rasta) :put 403 (format "org/%d/members/1000" id) {}))) - -;; Check that users without WRITE org perms cannot modify members (test user with READ perms on org) -(expect "You don't have permissions to do that." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin false) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :put 403 (format "org/%d/members/1000" org-id) {}))) - -;; Test that invalid org id returns 404 -(expect "Not found." - ((user->client :rasta) :put 404 "org/1000/members/1000")) - -;; Test that invalid user id returns 404 -(expect "Not found." - (let [{user-id :id, email :email, password :first_name} (create-user) - {org-id :id} (create-org (random-name)) - my-perm (create-org-perm org-id user-id :admin true) - session-id (http/authenticate {:email email - :password password})] - (http/client session-id :put 404 (format "org/%d/members/1000" org-id) {}))) diff --git a/test/metabase/api/setting_test.clj b/test/metabase/api/setting_test.clj index 6ca73cea4f07a7c3aec521ac6d5202740ba3f64f..ad30afbab5b4a0ba6f1db1a1e22920153522367b 100644 --- a/test/metabase/api/setting_test.clj +++ b/test/metabase/api/setting_test.clj @@ -26,8 +26,12 @@ (fetch-all-settings))) ;; Check that a non-superuser can't read settings -(expect "You don't have permissions to do that." - ((user->client :rasta) :get 403 "setting")) +(expect + [{:value nil + :key "site-name" + :description "The name used for this instance of Metabase." + :default "Metabase"}] + ((user->client :rasta) :get 200 "setting")) ;; ## GET /api/setting/:key diff --git a/test/metabase/api/user_test.clj b/test/metabase/api/user_test.clj index a7cad3e599fc302e7635e3c0e68d399f0c27dd27..fcf64b0087b3e01036210514acf126702d39862a 100644 --- a/test/metabase/api/user_test.clj +++ b/test/metabase/api/user_test.clj @@ -5,15 +5,12 @@ [metabase.db :refer :all] [metabase.http-client :as http] [metabase.middleware.auth :as auth] - (metabase.models [org-perm :refer [OrgPerm]] - [session :refer [Session]] + (metabase.models [session :refer [Session]] [user :refer [User]]) [metabase.test.util :refer [match-$ random-name expect-eval-actual-first]] [metabase.test-data :refer :all] [metabase.test-data.create :refer [create-user]])) -(def rasta-org-perm-id (delay (sel :one :id OrgPerm :organization_id @org-id :user_id (user->id :rasta)))) - ;; ## /api/user/* AUTHENTICATION Tests ;; We assume that all endpoints for a given context are enforced by the same middleware, so we don't run the same ;; authentication test on every single individual endpoint @@ -22,8 +19,14 @@ (expect (get auth/response-unauthentic :body) (http/client :get 401 "user/current")) +;; ## Helper Fns +(defn create-user-api [user-name] + ((user->client :crowberto) :post 200 "user" {:first_name user-name + :last_name user-name + :email (str user-name "@metabase.com")})) + ;; ## GET /api/user -;; Check that superusers can get a list of all Users +;; Check that superusers can get a list of all active Users (expect #{(match-$ (fetch-user :crowberto) {:common_name "Crowberto Corv" @@ -34,15 +37,6 @@ :last_login $ :first_name "Crowberto" :email "crowberto@metabase.com"}) - (match-$ (fetch-user :trashbird) - {:common_name "Trash Bird" - :date_joined $ - :last_name "Bird" - :id $ - :is_superuser false - :last_login $ - :first_name "Trash" - :email "trashbird@metabase.com"}) (match-$ (fetch-user :lucky) {:common_name "Lucky Pigeon" :date_joined $ @@ -72,6 +66,45 @@ (expect "You don't have permissions to do that." ((user->client :rasta) :get 403 "user")) + +;; ## POST /api/user +;; Test that we can create a new User +(let [rand-name (random-name)] + (expect-eval-actual-first + (match-$ (sel :one User :first_name rand-name) + {:id $ + :email $ + :first_name rand-name + :last_name rand-name + :date_joined $ + :last_login $ + :common_name $ + :is_superuser false}) + (create-user-api rand-name))) + +;; Check that non-superusers are denied access +(expect "You don't have permissions to do that." + ((user->client :rasta) :post 403 "user" {:first_name "whatever" + :last_name "whatever" + :email "whatever@whatever.com"})) + +;; Test input validations +(expect {:errors {:first_name "field is a required param."}} + ((user->client :crowberto) :post 400 "user" {})) + +(expect {:errors {:last_name "field is a required param."}} + ((user->client :crowberto) :post 400 "user" {:first_name "whatever"})) + +(expect {:errors {:email "field is a required param."}} + ((user->client :crowberto) :post 400 "user" {:first_name "whatever" + :last_name "whatever"})) + +(expect {:errors {:email "Invalid value 'whatever' for 'email': Not a valid email address."}} + ((user->client :crowberto) :post 400 "user" {:first_name "whatever" + :last_name "whatever" + :email "whatever"})) + + ;; ## GET /api/user/current ;; Check that fetching current user will return extra fields like `is_active` and will return OrgPerms (expect (match-$ (fetch-user :rasta) @@ -84,18 +117,7 @@ :is_active true :is_staff true :is_superuser false - :id $ - :org_perms [{:organization {:inherits true - :report_timezone nil - :logo_url nil - :description nil - :name "Test Organization" - :slug "test" - :id @org-id} - :organization_id @org-id - :user_id $id - :admin true - :id @rasta-org-perm-id}]}) + :id $}) ((user->client :rasta) :get 200 "user/current")) @@ -128,6 +150,10 @@ :common_name "Rasta Toucan"}) ((user->client :crowberto) :get 200 (str "user/" (user->id :rasta)))) +;; We should get a 404 when trying to access a disabled account +(expect "Not found." + ((user->client :crowberto) :get 404 (str "user/" (user->id :trashbird)))) + ;; ## PUT /api/user/:id ;; Test that we can edit a User @@ -152,6 +178,10 @@ (expect "You don't have permissions to do that." ((user->client :rasta) :put 403 (str "user/" (user->id :trashbird)) {:email "toucan@metabase.com"})) +;; We should get a 404 when trying to access a disabled account +(expect "Not found." + ((user->client :crowberto) :put 404 (str "user/" (user->id :trashbird)) {:email "toucan@metabase.com"})) + ;; ## PUT /api/user/:id/password ;; Test that a User can change their password @@ -190,3 +220,16 @@ (expect "password mismatch" ((user->client :rasta) :put 400 (format "user/%d/password" (user->id :rasta)) {:password "whateverUP12!!" :old_password "mismatched"})) + + +;; ## DELETE /api/user/:id +;; Disable a user account +(let [rand-name (random-name)] + (expect-eval-actual-first + {:success true} + (let [user (create-user-api rand-name)] + ((user->client :crowberto) :delete 200 (format "user/%d" (:id user)) {})))) + +;; Check that a non-superuser CANNOT update someone else's password +(expect "You don't have permissions to do that." + ((user->client :rasta) :delete 403 (format "user/%d" (user->id :rasta)) {})) diff --git a/test/metabase/bootstrap.clj b/test/metabase/bootstrap.clj index e5e98179654d3892e428348b03826919f0e05a5d..060975b918595e7605990f77bd74cc608db69272 100644 --- a/test/metabase/bootstrap.clj +++ b/test/metabase/bootstrap.clj @@ -3,8 +3,6 @@ (:require (metabase [db :refer :all] [test-data :as data]) (metabase.models [database :refer [Database]] - [org :refer [Org]] - [org-perm :refer [OrgPerm]] [user :refer [User]]))) (declare bootstrap-user @@ -14,9 +12,8 @@ ;; # BOOTSTRAPPING (defn bootstrap - "Create a `User` (and, optionally, `Org`) for development purposes. - You may optionally load the test data and use the test `Org`. - Permissions will be created for `User` <-> `Org`." + "Create a `User` for development purposes. + You may optionally load the test data." [] (setup-db :auto-migrate true) (let [{:keys [email]} (bootstrap-user)] @@ -25,18 +22,8 @@ ;; # (INTERNAL) -(defn- bootstrap-org - "Create a new Organization." - [] - (let [org-name (prompt-read-line "Org name" "Default")] - (or (sel :one Org :name org-name) - (ins Org - :name org-name - :slug (prompt-read-line "Org slug" "default") - :inherits true)))) - (defn- bootstrap-user - "Create a new User (creating a new Org too if needed). Org perms between User & Org will be created." + "Create a new User." [] (let [email (prompt-read-line "User email" "cam@metabase.com") ;; create User if needed @@ -46,18 +33,7 @@ :first_name (prompt-read-line "User first name" "Cam") :last_name (prompt-read-line "User last name" "Saul") :is_superuser (prompt-read-line-boolean "Make this user a superuser?" "true") - :password (prompt-read-line "User password" "password"))) - use-test-org? (prompt-read-line-boolean "Should we use the test data? (User will be added to \"Test Organization\")" "true") - org (if use-test-org? (do @data/test-db ; load the test data reaaallly quick - @data/test-org) - (bootstrap-org))] - ;; create OrgPerm if needed - (or (sel :one OrgPerm :organization_id (:id org) :user_id (:id user)) - (let [admin? (prompt-read-line-boolean "Make user an admin?" "true")] - (ins OrgPerm - :organization_id (:id org) - :user_id (:id user) - :admin admin?))) + :password (prompt-read-line "User password" "password")))] user)) (defn- prompt-read-line diff --git a/test/metabase/driver/mongo/test_data.clj b/test/metabase/driver/mongo/test_data.clj index 7c5f48da5538a5b2ce9f70f1a2ab4d73f6cbb89e..ef284fdc268b494a1193c1a594f4d9380d9450fd 100644 --- a/test/metabase/driver/mongo/test_data.clj +++ b/test/metabase/driver/mongo/test_data.clj @@ -11,7 +11,6 @@ (metabase.models [database :refer [Database]] [field :refer [Field]] [table :refer [Table]]) - [metabase.test-data :refer [org-id]] [metabase.test-data.data :as data])) (declare load-data @@ -46,7 +45,6 @@ "Why are we attempting to use the Mongo test Database when we're not testing against mongo?") (let [db (or (sel :one Database :name mongo-test-db-name) (let [db (ins Database - :organization_id @org-id :name mongo-test-db-name :engine :mongo :details {:conn_str mongo-test-db-conn-str})] diff --git a/test/metabase/models/common_test.clj b/test/metabase/models/common_test.clj index 216a670028847f96c1ac4647cb677b00396b49e4..2ac473653d073d8fbcfecf79d639211e6d24aa08 100644 --- a/test/metabase/models/common_test.clj +++ b/test/metabase/models/common_test.clj @@ -21,7 +21,6 @@ (expect #{:read :write} (binding [*current-user-id* 100] (user-permissions {:creator_id 100 - :organization_id 12 :public_perms 0}))) ;; TODO - write tests for the rest of the `user-permissions` diff --git a/test/metabase/test_data.clj b/test/metabase/test_data.clj index 82b017258c744cb1ecc182fc811725a40a39436d..ab8a1c174b5e0edefc5d3d1367407e4af02e4b55 100644 --- a/test/metabase/test_data.clj +++ b/test/metabase/test_data.clj @@ -6,7 +6,6 @@ (metabase [db :refer :all] [http-client :as http]) (metabase.models [field :refer [Field]] - [org-perm :refer [OrgPerm]] [table :refer [Table]] [user :refer [User]]) [metabase.test-data.load :as load]) @@ -84,22 +83,9 @@ (sel :one Table :id (table->id table-name))) -;; ## Test Organization - -(def test-org - "The test Organization." - (delay (setup-db-if-needed :auto-migrate true) - (load/test-org))) - -(def org-id - "The ID of the test Organization." - (delay (assert @test-org) - (:id @test-org))) - - ;; ## Test Users ;; -;; These users have permissions for the Test Org. They are lazily created as needed. +;; These users have permissions for the Test. They are lazily created as needed. ;; Three test users are defined: ;; * rasta - an admin ;; * crowberto - an admin + superuser @@ -165,12 +151,6 @@ (do (reset! tokens {}) (apply call-client args)))))))) -(defn user->org-perm - "Return the `OrgPerm` for User with USERNAME for the Test Org." - [username] - {:pre [(contains? usernames username)]} - (sel :one OrgPerm :organization_id @org-id :user_id (user->id username))) - ;; ## Temporary Tables / Etc. @@ -282,7 +262,7 @@ (set (keys user->info))) (defn- fetch-or-create-user - "Create User + OrgPerms if they don't already exist and return User." + "Create User if they don't already exist and return User." [& {:keys [email first last password admin superuser active] :or {admin false superuser false @@ -293,15 +273,12 @@ (string? password) (medley/boolean? admin) (medley/boolean? superuser)]} - (let [org @test-org] ; we're derefing test-org here to force lazy loading of DB - (or (sel :one User :email email) - (let [user (ins User - :email email - :first_name first - :last_name last - :password password - :is_superuser superuser - :is_active active)] - (or (exists? OrgPerm :organization_id (:id org) :user_id (:id user)) - (ins OrgPerm :organization_id (:id org) :user_id (:id user) :admin admin)) - user)))) + (or (sel :one User :email email) + (let [user (ins User + :email email + :first_name first + :last_name last + :password password + :is_superuser superuser + :is_active active)] + user))) diff --git a/test/metabase/test_data/load.clj b/test/metabase/test_data/load.clj index 7182a2b2819f977fb89ef03c9874eaca44cbdb3d..10a9b2ded111abec0c89bb9273f4da4c83acb3f0 100644 --- a/test/metabase/test_data/load.clj +++ b/test/metabase/test_data/load.clj @@ -10,9 +10,7 @@ (metabase.models [database :refer [Database]] [field :refer [Field]] [foreign-key :refer [ForeignKey]] - [table :refer [Table]] - [org :refer [Org]] - [org-perm :refer [OrgPerm]]) + [table :refer [Table]]) [metabase.util :as u])) (declare add-foreign-key-constraints! @@ -20,7 +18,6 @@ create-and-populate-tables!) (def ^:const ^:private db-name "Test Database") -(def ^:const ^:private org-name "Test Organization") (def ^:private test-db-filename (delay (format "%s/target/test-data" (System/getProperty "user.dir")))) (def test-db-connection-string @@ -28,15 +25,6 @@ ;; # PUBLIC INTERFACE -(defn test-org - "Returns a test `Organization` that the test database will belong to, creating it if needed." - [] - (or (sel :one Org :name org-name) - (ins Org - :name org-name - :slug "test" - :inherits true))) - (defn test-db "Returns the test `Database`. If it does not exist, it creates it, loads relevant data, and calls `sync-tables`." @@ -47,7 +35,6 @@ (create-and-populate-tables!)) (log/info "Creating new metabase Database object...") (let [db (ins Database - :organization_id (:id (test-org)) :name db-name :engine :h2 :details {:conn_str @test-db-connection-string})]