Add ability to create a new organization

This commit is contained in:
Joseph Schorr 2013-11-07 15:19:52 -05:00
parent 70c02eae16
commit 44f1ff0ef1
8 changed files with 373 additions and 62 deletions

View file

@ -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/<orgname>', methods=['GET'])
@api_login_required
def get_organization(orgname):

View file

@ -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;

View file

@ -0,0 +1,25 @@
<div class="signin-form-element">
<form class="form-signin" ng-submit="signin();">
<input type="text" class="form-control input-lg" name="username"
placeholder="Username" ng-model="user.username" autofocus>
<input type="password" class="form-control input-lg" name="password"
placeholder="Password" ng-model="user.password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
<span class="social-alternate">
<i class="fa fa-circle"></i>
<span class="inner-text">OR</span>
</span>
<a id="github-signin-link"
href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ mixpanelDistinctIdClause }}"
class="btn btn-primary btn-lg btn-block">
<i class="fa fa-github fa-lg"></i> Sign In with GitHub
</a>
</form>
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
<div class="alert alert-danger" ng-show="needsEmailVerification">
You must verify your email address before you can sign in.
</div>
</div>

View file

@ -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,

View file

@ -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');
});
});
};
}

View file

@ -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);

View file

@ -1 +1,113 @@
new org
<div class="loading" ng-show="loading || creating">
<i class="fa fa-spinner fa-spin fa-3x"></i>
</div>
<div class="container create-org" ng-show="!loading && !creating">
<div class="row header-row">
<div class="col-md-1"></div>
<div class="col-md-8">
<h2>Create Organization</h2>
<div class="steps-container" ng-show="false">
<ul class="steps">
<li class="step" ng-class="!user || user.anonymous ? 'active' : ''">
<i class="fa fa-sign-in"></i>
<span class="title">Login with an account</span>
</li>
<li class="step" ng-class="!user.anonymous && !created ? 'active' : ''">
<i class="fa fa-gear"></i>
<span class="title">Setup your organization</span>
</li>
<li class="step" ng-class="!user.anonymous && created ? 'active' : ''">
<i class="fa fa-group"></i>
<span class="title">Create teams</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Step 1 -->
<div class="row" ng-show="!user || user.anonymous">
<div class="col-sm-6 col-sm-offset-3">
<div class="step-container" >
<div class="panel panel-default">
<div class="panel-body">
<div class="signin-form" redirect-url="'/organizations/new'"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Step 2 -->
<div class="row" ng-show="user && !user.anonymous && !created">
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="step-container">
<h3>Setup the new organization</h3>
<form method="post" name="newOrgForm" id="newOrgForm" ng-submit="createNewOrg()">
<div class="form-group">
<label for="orgName">Organization Name</label>
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
data-placement="right">
<span class="description">This will also be the namespace for your repositories</span>
</div>
<div class="form-group">
<label for="orgName">Organization Email</label>
<input id="orgEmail" name="orgEmail" type="email" class="form-control" placeholder="Organization Email"
ng-model="org.email" required>
<span class="description">This address must be different from your account's email</span>
</div>
<!-- Plans Table -->
<div class="form-group plan-group">
<strong>Choose your organization's plan</strong>
<table class="table table-hover plans-table" ng-show="plans">
<thead>
<th>Plan</th>
<th>Private Repositories</th>
<th style="min-width: 64px">Price</th>
<th></th>
</thead>
<tr ng-repeat="plan in plans" ng-class="currentPlan == plan ? 'active' : ''">
<td>{{ plan.title }}</td>
<td>{{ plan.privateRepos }}</td>
<td><div class="plan-price">${{ plan.price / 100 }}</div></td>
<td class="controls">
<a class="btn" href="javascript:void(0)" ng-class="currentPlan == plan ? 'btn-primary' : 'btn-default'"
ng-click="setPlan(plan)">
{{ currentPlan == plan ? 'Selected' : 'Choose' }}
</a>
</td>
</tr>
</table>
</div>
<div class="button-bar">
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan">
Create Organization
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Step 3 -->
<div class="row" ng-show="user && !user.anonymous && created">
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="step-container">
<h3>Organization Created</h3>
<h4><a href="/organization/{{ org.name }}">Manage Teams Now</a></h4>
</div>
</div>
</div>
</div>

View file

@ -12,22 +12,7 @@
</div>
<div id="collapseSignin" class="panel-collapse collapse in">
<div class="panel-body">
<form class="form-signin" ng-submit="signin();">
<input type="text" class="form-control input-lg" name="username" placeholder="Username" ng-model="user.username" autofocus>
<input type="password" class="form-control input-lg" name="password" placeholder="Password" ng-model="user.password">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
<span class="social-alternate">
<i class="fa fa-circle"></i>
<span class="inner-text">OR</span>
</span>
<a id='github-signin-link' href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ mixpanelDistinctIdClause }}" class="btn btn-primary btn-lg btn-block"><i class="fa fa-github fa-lg"></i> Sign In with GitHub</a>
</form>
<div class="alert alert-danger" ng-show="invalidCredentials">Invalid username or password.</div>
<div class="alert alert-danger" ng-show="needsEmailVerification">You must verify your email address before you can sign in.</div>
<div class="signin-form"></div>
</div>
</div>
</div>
@ -56,17 +41,3 @@
</div>
</div>
</div>
<!-- <script type="text/javascript">
function appendMixpanelId() {
if (mixpanel.get_distinct_id !== undefined) {
var signinLink = document.getElementById("github-signin-link");
signinLink.href += ("&state=" + mixpanel.get_distinct_id());
} else {
// Mixpanel not yet loaded, try again later
window.setTimeout(appendMixpanelId, 200);
}
};
appendMixpanelId();
</script> -->