Better upwell messaging for everyone and show the upsell for organization admins
This commit is contained in:
parent
d12a53186a
commit
dbb234c76c
5 changed files with 62 additions and 64 deletions
|
@ -183,7 +183,7 @@ def get_logged_in_user():
|
||||||
@api.route('/user/private', methods=['GET'])
|
@api.route('/user/private', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
def get_user_private_count():
|
def get_user_private_allowed():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
private_repos = model.get_private_repo_count(user.username)
|
private_repos = model.get_private_repo_count(user.username)
|
||||||
repos_allowed = 0
|
repos_allowed = 0
|
||||||
|
@ -197,7 +197,7 @@ def get_user_private_count():
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'privateCount': private_repos,
|
'privateCount': private_repos,
|
||||||
'reposAllowed': repos_allowed
|
'privateAllowed': (private_repos < repos_allowed)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -729,12 +729,16 @@ def get_organization_member(orgname, membername):
|
||||||
@api.route('/organization/<orgname>/private', methods=['GET'])
|
@api.route('/organization/<orgname>/private', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
|
@org_api_call('get_user_private_allowed')
|
||||||
def get_organization_private_allowed(orgname):
|
def get_organization_private_allowed(orgname):
|
||||||
permission = CreateRepositoryPermission(orgname)
|
permission = CreateRepositoryPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
organization = model.get_organization(orgname)
|
organization = model.get_organization(orgname)
|
||||||
|
|
||||||
private_repos = model.get_private_repo_count(organization.username)
|
private_repos = model.get_private_repo_count(organization.username)
|
||||||
|
data = {
|
||||||
|
'privateAllowed': False
|
||||||
|
}
|
||||||
|
|
||||||
if organization.stripe_id:
|
if organization.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(organization.stripe_id)
|
cus = stripe.Customer.retrieve(organization.stripe_id)
|
||||||
if cus.subscription:
|
if cus.subscription:
|
||||||
|
@ -743,13 +747,13 @@ def get_organization_private_allowed(orgname):
|
||||||
if plan:
|
if plan:
|
||||||
repos_allowed = plan['privateRepos']
|
repos_allowed = plan['privateRepos']
|
||||||
|
|
||||||
return jsonify({
|
data['privateAllowed'] = (private_repos < repos_allowed)
|
||||||
'privateAllowed': (private_repos < repos_allowed)
|
|
||||||
})
|
|
||||||
|
|
||||||
return jsonify({
|
if AdministerOrganizationPermission(orgname).can():
|
||||||
'privateAllowed': False
|
data['privateCount'] = private_repos
|
||||||
})
|
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
|
@ -602,10 +602,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
planService.getPlans(function(plans) {
|
planService.getPlans(function(plans) {
|
||||||
for (var i = 0; i < plans.length; i++) {
|
for (var i = 0; i < plans.length; i++) {
|
||||||
var plan = plans[i];
|
var plan = plans[i];
|
||||||
if (isBusiness && !planService.isOrgCompatible(plan)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plan.privateRepos >= privateCount) {
|
if (plan.privateRepos >= privateCount) {
|
||||||
callback(plan);
|
callback(plan);
|
||||||
return;
|
return;
|
||||||
|
@ -613,7 +609,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
});
|
}, /* include personal */!isBusiness);
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.getSubscription = function(orgname, success, failure) {
|
planService.getSubscription = function(orgname, success, failure) {
|
||||||
|
@ -2077,8 +2073,8 @@ quayApp.directive('headerBar', function () {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApiService.getUserPrivateCount().then(function(resp) {
|
ApiService.getUserPrivateAllowed().then(function(resp) {
|
||||||
$scope.overPlan = resp.privateCount > resp.reposAllowed;
|
$scope.overPlan = !resp['privateAllowed'];
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1906,37 +1906,14 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
||||||
|
|
||||||
var isUserNamespace = (namespace == $scope.user.username);
|
var isUserNamespace = (namespace == $scope.user.username);
|
||||||
|
|
||||||
$scope.checkingPlan = true;
|
|
||||||
$scope.planRequired = null;
|
$scope.planRequired = null;
|
||||||
$scope.isUserNamespace = isUserNamespace;
|
$scope.isUserNamespace = isUserNamespace;
|
||||||
|
|
||||||
if (isUserNamespace) {
|
// Determine whether private repositories are allowed for the namespace.
|
||||||
// Load the user's subscription information in case they want to create a private
|
checkPrivateAllowed();
|
||||||
// repository.
|
|
||||||
ApiService.getUserPrivateCount().then(function(resp) {
|
|
||||||
if (resp.privateCount + 1 > resp.reposAllowed) {
|
|
||||||
PlanService.getMinimumPlan(resp.privateCount + 1, false, function(minimum) {
|
|
||||||
$scope.planRequired = minimum;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.checkingPlan = false;
|
// Default to private repos for organizations.
|
||||||
}, function() {
|
$scope.repo.is_public = isUserNamespace ? '1' : '0';
|
||||||
$scope.planRequired = {};
|
|
||||||
$scope.checkingPlan = false;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
ApiService.getOrganizationPrivateAllowed(null, {'orgname': namespace}).then(function(resp) {
|
|
||||||
$scope.planRequired = resp.privateAllowed ? null : {};
|
|
||||||
$scope.checkingPlan = false;
|
|
||||||
}, function() {
|
|
||||||
$scope.planRequired = {};
|
|
||||||
$scope.checkingPlan = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Auto-set to private repo.
|
|
||||||
$scope.repo.is_public = '0';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.changeNamespace = function(namespace) {
|
$scope.changeNamespace = function(namespace) {
|
||||||
|
@ -2015,7 +1992,35 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
|
var namespace = $scope.isUserNamespace ? null : $scope.repo.namespace;
|
||||||
|
PlanService.changePlan($scope, namespace, $scope.planRequired.stripeId, callbacks);
|
||||||
|
};
|
||||||
|
|
||||||
|
var checkPrivateAllowed = function() {
|
||||||
|
if (!$scope.repo || !$scope.repo.namespace) { return; }
|
||||||
|
|
||||||
|
$scope.checkingPlan = true;
|
||||||
|
|
||||||
|
var isUserNamespace = $scope.isUserNamespace;
|
||||||
|
ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) {
|
||||||
|
$scope.checkingPlan = false;
|
||||||
|
|
||||||
|
if (resp['privateAllowed']) {
|
||||||
|
$scope.planRequired = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp['privateCount'] == null) {
|
||||||
|
// Organization where we are not the admin.
|
||||||
|
$scope.planRequired = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, lookup the matching plan.
|
||||||
|
PlanService.getMinimumPlan(resp['privateCount'] + 1, !isUserNamespace, function(minimum) {
|
||||||
|
$scope.planRequired = minimum;
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var subscribedToPlan = function(sub) {
|
var subscribedToPlan = function(sub) {
|
||||||
|
@ -2025,16 +2030,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
|
||||||
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
||||||
$scope.subscribedPlan = subscribedPlan;
|
$scope.subscribedPlan = subscribedPlan;
|
||||||
$scope.planRequired = null;
|
$scope.planRequired = null;
|
||||||
|
checkPrivateAllowed();
|
||||||
// Check to see if the current plan allows for an additional private repository to
|
|
||||||
// be created.
|
|
||||||
var privateAllowed = $scope.subscription.usedPrivateRepos < $scope.subscribedPlan.privateRepos;
|
|
||||||
if (!privateAllowed) {
|
|
||||||
// If not, find the minimum repository that does.
|
|
||||||
PlanService.getMinimumPlan($scope.subscription.usedPrivateRepos + 1, !$scope.isUserNamespace, function(minimum) {
|
|
||||||
$scope.planRequired = minimum;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,25 +63,27 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment -->
|
<!-- Payment -->
|
||||||
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && isUserNamespace">
|
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && planRequired.title">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
In order to make this repository private, you’ll need to upgrade your plan to
|
In order to make this repository private
|
||||||
|
<span ng-if="isUserNamespace">under your personal namespace</span>
|
||||||
|
<span ng-if="!isUserNamespace">under the organization <b>{{ repo.namespace }}</b></span>, you will need to upgrade your plan to
|
||||||
<b style="border-bottom: 1px dotted black;" bs-tooltip="'<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories'">
|
<b style="border-bottom: 1px dotted black;" bs-tooltip="'<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories'">
|
||||||
{{ planRequired.title }}
|
{{ planRequired.title }}
|
||||||
</b>.
|
</b>.
|
||||||
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
||||||
<span ng-show="user.organizations.length == 1">or did you mean to create this repository
|
<span ng-if="isUserNamespace && user.organizations.length == 1">or did you mean to create this repository
|
||||||
under <a href="javascript:void(0)" ng-click="changeNamespace(user.organizations[0].name)"><b>{{ user.organizations[0].name }}</b></a>?</span>
|
under <a href="javascript:void(0)" ng-click="changeNamespace(user.organizations[0].name)"><b>{{ user.organizations[0].name }}</b></a>?</span>
|
||||||
<div class="quay-spinner" ng-show="planChanging"></div>
|
<div class="quay-spinner" ng-show="planChanging"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quay-spinner" ng-show="repo.is_public == '0' && checkingPlan"></div>
|
<div class="quay-spinner" ng-show="repo.is_public == '0' && checkingPlan"></div>
|
||||||
|
|
||||||
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && !isUserNamespace">
|
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && !isUserNamespace && !planRequired.title">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
This organization has reached its private repository limit. Please contact your administrator.
|
This organization has reached its private repository limit. Please contact your administrator.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -128,18 +128,18 @@ class TestLoggedInUser(ApiTestCase):
|
||||||
assert json['username'] == READ_ACCESS_USER
|
assert json['username'] == READ_ACCESS_USER
|
||||||
|
|
||||||
|
|
||||||
class TestGetUserPrivateCount(ApiTestCase):
|
class TestGetUserPrivateAllowed(ApiTestCase):
|
||||||
def test_nonallowed(self):
|
def test_nonallowed(self):
|
||||||
self.login(READ_ACCESS_USER)
|
self.login(READ_ACCESS_USER)
|
||||||
json = self.getJsonResponse('api.get_user_private_count')
|
json = self.getJsonResponse('api.get_user_private_allowed')
|
||||||
assert json['privateCount'] == 0
|
assert json['privateCount'] == 0
|
||||||
assert json['reposAllowed'] == 0
|
assert not json['privateAllowed']
|
||||||
|
|
||||||
def test_allowed(self):
|
def test_allowed(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
json = self.getJsonResponse('api.get_user_private_count')
|
json = self.getJsonResponse('api.get_user_private_allowed')
|
||||||
assert json['privateCount'] == 6
|
assert json['privateCount'] == 6
|
||||||
assert json['reposAllowed'] > 0
|
assert json['privateAllowed']
|
||||||
|
|
||||||
|
|
||||||
class TestConvertToOrganization(ApiTestCase):
|
class TestConvertToOrganization(ApiTestCase):
|
||||||
|
|
Reference in a new issue