diff --git a/endpoints/api.py b/endpoints/api.py index 234e47563..897e56b85 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -282,6 +282,37 @@ def team_view(orgname, t): } +@app.route('/api/organization/', methods=['POST']) +@required_json_args('name', 'email') +@api_login_required +def create_organization_api(): + org_data = request.get_json() + existing = None + + try: + existing = model.get_organization(org_data['name']) or model.get_user(org_data['name']) + except: + pass + + if existing: + error_resp = jsonify({ + 'message': 'A user or organization with this name already exists' + }) + error_resp.status_code = 400 + return error_resp + + try: + organization = model.create_organization(org_data['name'], org_data['email'], + current_user.db_user()) + return make_response('Created', 201) + except model.DataModelException as ex: + error_resp = jsonify({ + 'message': ex.message, + }) + error_resp.status_code = 400 + return error_resp + + @app.route('/api/organization/', methods=['GET']) @api_login_required def get_organization(orgname): diff --git a/static/css/quay.css b/static/css/quay.css index 5671c1109..ff6f9ab68 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1611,6 +1611,103 @@ p.editable:hover i { margin-right: 16px; } +.create-org .steps-container { + text-align: center; +} + +.create-org .steps { + + background: #222; + + display: inline-block; + margin-top: 16px; + margin-left: 0px; + border-radius: 4px; + padding: 0px; + list-style: none; + height: 46px; + width: 675px; + text-align: left; +} + + +.create-org .steps .step { + width: 225px; + float: left; + padding: 10px; + border-right: 1px solid #222; + margin: 0px; + background: rgba(255, 255, 255, 0.2); + color: #aaa; + border-left: 4px solid transparent; +} + +.create-org .steps .step i { + font-size: 26px; + margin-right: 6px; + vertical-align: middle; +} + +.create-org .steps .step.active { + color: white; + border-left: 4px solid steelblue; + background: transparent; +} + +.create-org .steps .step:last-child { + border-right: 0px; +} + +.create-org .steps .step b { + display: block; +} + +.create-org .button-bar { + margin-bottom: 40px; +} + +.create-org .form-group { + margin-bottom: 32px; +} + +.create-org .plan-group { + padding-left: 10px; +} + +.create-org .plan-group table { + margin: 20px; + border: 1px solid #eee; +} + +.create-org .plan-group strong { + margin-bottom: 10px; +} + +.create-org .plan-group td { + vertical-align: middle; +} + +.create-org .plan-group .plan-price { + font-size: 16px; +} + +.create-org .step-container .description { + margin-top: 10px; + display: block; + color: #888; + font-size: 12px; + margin-left: 10px; +} + +.create-org .form-group input { + margin-top: 10px; + margin-left: 10px; +} + +.create-org h3 { + margin-bottom: 20px; +} + .plan-manager-element .plans-table thead td { color: #aaa; font-weight: bold; diff --git a/static/directives/signin-form.html b/static/directives/signin-form.html new file mode 100644 index 000000000..ef0b5f795 --- /dev/null +++ b/static/directives/signin-form.html @@ -0,0 +1,25 @@ +
+ + +
Invalid username or password.
+
+ You must verify your email address before you can sign in. +
+
diff --git a/static/js/app.js b/static/js/app.js index 8d7bf2aa9..a4d65c357 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -343,6 +343,50 @@ quayApp.directive('repoCircle', function () { }); +quayApp.directive('signinForm', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/signin-form.html', + replace: false, + transclude: true, + restrict: 'C', + scope: { + 'redirectUrl': '=redirectUrl' + }, + controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) { + $scope.githubClientId = KeyService.githubClientId; + + var appendMixpanelId = function() { + if (mixpanel.get_distinct_id !== undefined) { + $scope.mixpanelDistinctIdClause = "&state=" + mixpanel.get_distinct_id(); + } else { + // Mixpanel not yet loaded, try again later + $timeout(appendMixpanelId, 200); + } + }; + + appendMixpanelId(); + + $scope.signin = function() { + var signinPost = Restangular.one('signin'); + signinPost.customPOST($scope.user).then(function() { + $scope.needsEmailVerification = false; + $scope.invalidCredentials = false; + + // Redirect to the specified page or the landing page + UserService.load(); + $location.path($scope.redirectUrl ? $scope.redirectUrl : '/'); + }, function(result) { + $scope.needsEmailVerification = result.data.needsEmailVerification; + $scope.invalidCredentials = result.data.invalidCredentials; + }); + }; + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('organizationHeader', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/js/controllers.js b/static/js/controllers.js index 971073b94..a4773fefe 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -75,35 +75,6 @@ function HeaderCtrl($scope, $location, UserService, Restangular) { } function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserService) { - $scope.githubClientId = KeyService.githubClientId; - - var appendMixpanelId = function() { - if (mixpanel.get_distinct_id !== undefined) { - $scope.mixpanelDistinctIdClause = "&state=" + mixpanel.get_distinct_id(); - } else { - // Mixpanel not yet loaded, try again later - $timeout(appendMixpanelId, 200); - } - }; - - appendMixpanelId(); - - $scope.signin = function() { - var signinPost = Restangular.one('signin'); - signinPost.customPOST($scope.user).then(function() { - $scope.needsEmailVerification = false; - $scope.invalidCredentials = false; - - // Redirect to the landing page - UserService.load(); - $location.path('/'); - }, function(result) { - $scope.needsEmailVerification = result.data.needsEmailVerification; - $scope.invalidCredentials = result.data.invalidCredentials; - }); - - }; - $scope.sendRecovery = function() { var signinPost = Restangular.one('recovery'); signinPost.customPOST($scope.recovery).then(function() { @@ -1282,6 +1253,66 @@ function OrgsCtrl($scope, UserService) { browserchrome.update(); } -function NewOrgCtrl($scope, UserService) { +function NewOrgCtrl($scope, $timeout, $location, UserService, PlanService, Restangular) { + $scope.loading = true; + $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { + $scope.user = currentUser; + $scope.loading = false; + }, true); + + // Load the list of plans. + PlanService.getPlans(function(plans) { + $scope.plans = plans.business; + $scope.currentPlan = null; + }); + + $scope.setPlan = function(plan) { + $scope.currentPlan = plan; + }; + + $scope.createNewOrg = function() { + $('#orgName').popover('hide'); + + $scope.creating = true; + var org = $scope.org; + var data = { + 'name': org.name, + 'email': org.email + }; + + var createPost = Restangular.one('organization/'); + createPost.customPOST(data).then(function(created) { + $scope.creating = false; + $scope.created = created; + + // Reset the organizations list. + UserService.load(); + + // If the selected plan is free, simply move to the org page. + if ($scope.currentPlan.price == 0) { + $location.path('/organization/' + org.name + '/'); + return; + } + + // Otherwise, show the subscribe for the plan. + PlanService.changePlan($scope, org.name, $scope.currentPlan.stripeId, false, function() { + // Started. + $scope.creating = true; + }, function(sub) { + // Success. + $location.path('/organization/' + org.name + '/'); + }, function() { + // Failure. + $location.path('/organization/' + org.name + '/'); + }); + + }, function(result) { + $scope.creating = false; + $scope.createError = result.data.message || result.data; + $timeout(function() { + $('#orgName').popover('show'); + }); + }); + }; } \ No newline at end of file diff --git a/static/js/graphing.js b/static/js/graphing.js index 5ba91b879..d6ae25db7 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -1173,7 +1173,7 @@ RepositoryUsageChart.prototype.drawInternal_ = function() { var count = this.count_; var total = this.total_; - var data = [count, Math.max(0, total - count)]; + var data = [Math.max(count, 1), Math.max(0, total - count)]; var arcTween = function(a) { var i = d3.interpolate(this._current, a); diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html index 8fd32cd45..2e2828a9b 100644 --- a/static/partials/new-organization.html +++ b/static/partials/new-organization.html @@ -1 +1,113 @@ -new org +
+ +
+ +
+ +
+
+
+

Create Organization

+ +
+
    +
  • + + Login with an account +
  • +
  • + + Setup your organization +
  • +
  • + + Create teams +
  • +
+
+ +
+
+ + +
+
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+

Setup the new organization

+ +
+
+ + + This will also be the namespace for your repositories +
+ +
+ + + This address must be different from your account's email +
+ + +
+ Choose your organization's plan + + + + + + + + + + + + + + +
PlanPrivate RepositoriesPrice
{{ plan.title }}{{ plan.privateRepos }}
${{ plan.price / 100 }}
+ + {{ currentPlan == plan ? 'Selected' : 'Choose' }} + +
+
+ +
+ +
+
+
+
+
+ + +
+
+
+
+

Organization Created

+

Manage Teams Now

+
+
+
+
diff --git a/static/partials/signin.html b/static/partials/signin.html index 71821ec0a..b4b5f7685 100644 --- a/static/partials/signin.html +++ b/static/partials/signin.html @@ -12,22 +12,7 @@
- - -
Invalid username or password.
- -
You must verify your email address before you can sign in.
+
@@ -56,17 +41,3 @@ - -