Turn off all references and API calls to billing if the feature is disabled

This commit is contained in:
Joseph Schorr 2014-04-06 00:36:19 -04:00
parent c374e8146a
commit 19a20a6c94
12 changed files with 135 additions and 62 deletions

View file

@ -124,6 +124,9 @@ def format_date(date):
def add_method_metadata(name, value):
def modifier(func):
if func is None:
return None
if '__api_metadata' not in dir(func):
func.__api_metadata = {}
func.__api_metadata[name] = value
@ -132,6 +135,9 @@ def add_method_metadata(name, value):
def method_metadata(func, name):
if func is None:
return None
if '__api_metadata' in dir(func):
return func.__api_metadata.get(name, None)
return None

View file

@ -4,13 +4,14 @@ from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin)
require_user_admin, show_if, hide_if)
from endpoints.api.subscribe import subscribe, subscription_view
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from data import model
from data.plans import PLANS
import features
def carderror_response(e):
return {'carderror': e.message}, 402
@ -79,6 +80,7 @@ def get_invoices(customer_id):
@resource('/v1/plans/')
@show_if(features.BILLING)
class ListPlans(ApiResource):
""" Resource for listing the available plans. """
@nickname('listPlans')
@ -91,6 +93,7 @@ class ListPlans(ApiResource):
@resource('/v1/user/card')
@internal_only
@show_if(features.BILLING)
class UserCard(ApiResource):
""" Resource for managing a user's credit card. """
schemas = {
@ -132,6 +135,7 @@ class UserCard(ApiResource):
@resource('/v1/organization/<orgname>/card')
@internal_only
@related_user_resource(UserCard)
@show_if(features.BILLING)
class OrganizationCard(ApiResource):
""" Resource for managing an organization's credit card. """
schemas = {
@ -178,6 +182,7 @@ class OrganizationCard(ApiResource):
@resource('/v1/user/plan')
@internal_only
@show_if(features.BILLING)
class UserPlan(ApiResource):
""" Resource for managing a user's subscription. """
schemas = {
@ -234,6 +239,7 @@ class UserPlan(ApiResource):
@resource('/v1/organization/<orgname>/plan')
@internal_only
@related_user_resource(UserPlan)
@show_if(features.BILLING)
class OrganizationPlan(ApiResource):
""" Resource for managing a org's subscription. """
schemas = {
@ -294,6 +300,7 @@ class OrganizationPlan(ApiResource):
@resource('/v1/user/invoices')
@internal_only
@show_if(features.BILLING)
class UserInvoiceList(ApiResource):
""" Resource for listing a user's invoices. """
@require_user_admin
@ -310,6 +317,7 @@ class UserInvoiceList(ApiResource):
@resource('/v1/organization/<orgname>/invoices')
@internal_only
@related_user_resource(UserInvoiceList)
@show_if(features.BILLING)
class OrgnaizationInvoiceList(ApiResource):
""" Resource for listing an orgnaization's invoices. """
@nickname('listOrgInvoices')
@ -323,4 +331,4 @@ class OrgnaizationInvoiceList(ApiResource):
return get_invoices(organization.stripe_id)
raise Unauthorized()
raise Unauthorized()

View file

@ -5,7 +5,7 @@ from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin, log_action)
require_user_admin, log_action, show_if)
from endpoints.api.team import team_view
from endpoints.api.user import User, PrivateRepositories
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
@ -15,6 +15,8 @@ from data import model
from data.plans import get_plan
from util.gravatar import compute_hash
import features
logger = logging.getLogger(__name__)
@ -163,6 +165,7 @@ class Organization(ApiResource):
@resource('/v1/organization/<orgname>/private')
@internal_only
@related_user_resource(PrivateRepositories)
@show_if(features.BILLING)
class OrgPrivateRepositories(ApiResource):
""" Custom verb to compute whether additional private repositories are available. """
@nickname('getOrganizationPrivateAllowed')

View file

@ -6,6 +6,7 @@ from endpoints.common import check_repository_usage
from data import model
from data.plans import PLANS
import features
logger = logging.getLogger(__name__)
@ -24,6 +25,9 @@ def subscription_view(stripe_subscription, used_repos):
def subscribe(user, plan, token, require_business_plan):
if not features.BILLING:
return
plan_found = None
for plan_obj in PLANS:
if plan_obj['stripeId'] == plan:

View file

@ -194,6 +194,7 @@ class User(ApiResource):
@resource('/v1/user/private')
@internal_only
@show_if(features.BILLING)
class PrivateRepositories(ApiResource):
""" Operations dealing with the available count of private repositories. """
@require_user_admin
@ -249,8 +250,7 @@ class ConvertToOrganization(ApiResource):
'description': 'Information required to convert a user to an organization.',
'required': [
'adminUser',
'adminPassword',
'plan',
'adminPassword'
],
'properties': {
'adminUser': {
@ -263,7 +263,7 @@ class ConvertToOrganization(ApiResource):
},
'plan': {
'type': 'string',
'description': 'The plan to which the organizatino should be subscribed',
'description': 'The plan to which the organization should be subscribed',
},
},
},
@ -290,8 +290,9 @@ class ConvertToOrganization(ApiResource):
message='The admin user credentials are not valid')
# Subscribe the organization to the new plan.
plan = convert_data['plan']
subscribe(user, plan, None, True) # Require business plans
if features.BILLING:
plan = convert_data.get('plan', 'free')
subscribe(user, plan, None, True) # Require business plans
# Convert the user to an organization.
model.convert_user_to_organization(user, model.get_user(admin_username))

View file

@ -20,6 +20,8 @@ from util.names import parse_repository_name
from util.gravatar import compute_hash
from auth import scopes
import features
logger = logging.getLogger(__name__)
web = Blueprint('web', __name__)
@ -54,6 +56,7 @@ def snapshot(path = ''):
@web.route('/plans/')
@no_cache
@route_show_if(features.BILLING)
def plans():
return index('')
@ -152,6 +155,7 @@ def privacy():
@web.route('/receipt', methods=['GET'])
@route_show_if(features.BILLING)
def receipt():
if not current_user.is_authenticated():
abort(401)

View file

@ -14,7 +14,7 @@
<li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
<li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
<li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}">Tutorial</a></li>
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}">Pricing</a></li>
<li><a ng-href="/plans/" target="{{ appLinkTarget() }}" quay-require="['BILLING']">Pricing</a></li>
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
</ul>

View file

@ -876,8 +876,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return keyService;
}]);
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService',
function(KeyService, UserService, CookieService, ApiService) {
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features',
function(KeyService, UserService, CookieService, ApiService, Features) {
var plans = null;
var planDict = {};
var planService = {};
@ -903,7 +903,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.notePlan = function(planId) {
CookieService.putSession('quay.notedplan', planId);
if (Features.BILLING) {
CookieService.putSession('quay.notedplan', planId);
}
};
planService.isOrgCompatible = function(plan) {
@ -929,7 +931,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
planService.handleNotedPlan = function() {
var planId = planService.getAndResetNotedPlan();
if (!planId) { return false; }
if (!planId || !Features.BILLING) { return false; }
UserService.load(function() {
if (UserService.currentUser().anonymous) {
@ -974,6 +976,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.verifyLoaded = function(callback) {
if (!Features.BILLING) { return; }
if (plans) {
callback(plans);
return;
@ -1033,10 +1037,14 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.getSubscription = function(orgname, success, failure) {
ApiService.getSubscription(orgname).then(success, failure);
if (!Features.BILLING) { return; }
ApiService.getSubscription(orgname).then(success, failure);
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
if (!Features.BILLING) { return; }
var subscriptionDetails = {
plan: planId
};
@ -1056,6 +1064,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.getCardInfo = function(orgname, callback) {
if (!Features.BILLING) { return; }
ApiService.getCard(orgname).then(function(resp) {
callback(resp.card);
}, function() {
@ -1064,6 +1074,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.changePlan = function($scope, orgname, planId, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['started']) {
callbacks['started']();
}
@ -1089,6 +1101,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.changeCreditCard = function($scope, orgname, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['opening']) {
callbacks['opening']();
}
@ -1145,6 +1159,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['opening']) {
callbacks['opening']();
}

View file

@ -1607,7 +1607,9 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
}
function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService,
$routeParams, $http) {
$routeParams, $http, Features) {
$scope.Features = Features;
if ($routeParams['migrate']) {
$('#migrateTab').tab('show')
}
@ -1690,13 +1692,15 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
};
$scope.showConvertForm = function() {
PlanService.getMatchingBusinessPlan(function(plan) {
$scope.org.plan = plan;
});
if (Features.BILLING) {
PlanService.getMatchingBusinessPlan(function(plan) {
$scope.org.plan = plan;
});
PlanService.getPlans(function(plans) {
$scope.orgPlans = plans;
});
PlanService.getPlans(function(plans) {
$scope.orgPlans = plans;
});
}
$scope.convertStep = 1;
};
@ -1711,7 +1715,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
var data = {
'adminUser': $scope.org.adminUser,
'adminPassword': $scope.org.adminPassword,
'plan': $scope.org.plan.stripeId
'plan': $scope.org.plan ? $scope.org.plan.stripeId : ''
};
ApiService.convertUserToOrganization(data).then(function(resp) {
@ -1912,7 +1916,7 @@ function V1Ctrl($scope, $location, UserService) {
UserService.updateUserIn($scope);
}
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService) {
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService, Features) {
UserService.updateUserIn($scope);
$scope.githubRedirectUri = KeyService.githubRedirectUri;
@ -2034,13 +2038,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
var checkPrivateAllowed = function() {
if (!$scope.repo || !$scope.repo.namespace) { return; }
if (!Features.BILLING) {
$scope.checkingPlan = false;
$scope.planRequired = null;
return;
}
$scope.checkingPlan = true;
var isUserNamespace = $scope.isUserNamespace;
ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) {
$scope.checkingPlan = false;
if (resp['privateAllowed']) {
if (resp['privateAllowed']) {
$scope.planRequired = null;
return;
}
@ -2160,18 +2170,20 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
loadOrganization();
}
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService) {
function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, Features) {
var orgname = $routeParams.orgname;
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.plan_map = {};
for (var i = 0; i < plans.length; ++i) {
$scope.plan_map[plans[i].stripeId] = plans[i];
}
});
if (Features.BILLING) {
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.plan_map = {};
for (var i = 0; i < plans.length; ++i) {
$scope.plan_map[plans[i].stripeId] = plans[i];
}
});
}
$scope.orgname = orgname;
$scope.membersLoading = true;
@ -2354,30 +2366,39 @@ function OrgsCtrl($scope, UserService) {
browserchrome.update();
}
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService) {
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService, Features) {
$scope.Features = Features;
$scope.holder = {};
UserService.updateUserIn($scope);
var requested = $routeParams['plan'];
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.currentPlan = null;
if (requested) {
PlanService.getPlan(requested, function(plan) {
$scope.currentPlan = plan;
});
}
});
if (Features.BILLING) {
// Load the list of plans.
PlanService.getPlans(function(plans) {
$scope.plans = plans;
$scope.currentPlan = null;
if (requested) {
PlanService.getPlan(requested, function(plan) {
$scope.currentPlan = plan;
});
}
});
}
$scope.signedIn = function() {
PlanService.handleNotedPlan();
if (Features.BILLING) {
PlanService.handleNotedPlan();
}
};
$scope.signinStarted = function() {
PlanService.getMinimumPlan(1, true, function(plan) {
PlanService.notePlan(plan.stripeId);
});
if (Features.BILLING) {
PlanService.getMinimumPlan(1, true, function(plan) {
PlanService.notePlan(plan.stripeId);
});
}
};
$scope.setPlan = function(plan) {
@ -2409,7 +2430,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
};
// If the selected plan is free, simply move to the org page.
if ($scope.currentPlan.price == 0) {
if (!Features.BILLING || $scope.currentPlan.price == 0) {
showOrg();
return;
}

View file

@ -66,13 +66,14 @@
</div>
<!-- Plans Table -->
<div class="form-group nested plan-group">
<div class="form-group nested plan-group" quay-require="['BILLING']">
<strong>Choose your organization's plan</strong>
<div class="plans-table" plans="plans" current-plan="currentPlan"></div>
<div class="plans-table" plans="plans" current-plan="holder.currentPlan"></div>
</div>
<div class="button-bar">
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan"
<button class="btn btn-large btn-success" type="submit"
ng-disabled="newOrgForm.$invalid || (Features.BILLING && !holder.currentPlan)"
analytics-on analytics-event="create_organization">
Create Organization
</button>

View file

@ -6,15 +6,23 @@
<!-- Side tabs -->
<div class="col-md-2">
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Organization Settings</a></li>
<li class="active" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a>
</li>
<li quay-classes="{'!BILLING': 'active'}">
<a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Organization Settings</a>
</li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#prototypes">Default Permissions</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#applications" ng-click="loadApplications()">Applications</a></li>
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a>
</li>
<li ng-show="hasPaidPlan" quay-require="['BILLING']">
<a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
</li>
</ul>
</div>
@ -22,12 +30,12 @@
<div class="col-md-10">
<div class="tab-content">
<!-- Plans tab -->
<div id="plan" class="tab-pane active">
<div id="plan" class="tab-pane active" quay-require="['BILLING']">
<div class="plan-manager" organization="orgname" plan-changed="planChanged(plan)"></div>
</div>
<!-- Organization settings tab -->
<div id="settings" class="tab-pane">
<div id="settings" class="tab-pane" quay-classes="{'!BILLING': 'active'}">
<div class="quay-spinner" ng-show="changingOrganization"></div>
<div class="panel" ng-show="!changingOrganization">
@ -67,12 +75,12 @@
</div>
<!-- Billing Options tab -->
<div id="billingoptions" class="tab-pane">
<div id="billingoptions" class="tab-pane" quay-require="['BILLING']">
<div class="billing-options" organization="organization"></div>
</div>
<!-- Billing History tab -->
<div id="billing" class="tab-pane">
<div id="billing" class="tab-pane" quay-require="['BILLING']">
<div class="billing-invoices" organization="organization" visible="invoicesShown"></div>
</div>

View file

@ -243,13 +243,14 @@
</div>
<!-- Plans Table -->
<div class="form-group plan-group">
<div class="form-group plan-group" quay-require="['BILLING']">
<label>Organization Plan</label>
<div class="plans-table" plans="orgPlans" current-plan="org.plan"></div>
</div>
<div class="button-bar">
<button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan"
<button class="btn btn-large btn-danger" type="submit"
ng-disabled="convertForm.$invalid || (Features.BILLING && !org.plan)"
analytics-on analytics-event="convert_to_organization">
Convert To Organization
</button>