From 19a20a6c94efb9b659ffa11f17ce5ffcc380ba00 Mon Sep 17 00:00:00 2001 From: Joseph Schorr <jschorr@gmail.com> Date: Sun, 6 Apr 2014 00:36:19 -0400 Subject: [PATCH] Turn off all references and API calls to billing if the feature is disabled --- endpoints/api/__init__.py | 6 ++ endpoints/api/billing.py | 12 +++- endpoints/api/organization.py | 5 +- endpoints/api/subscribe.py | 4 ++ endpoints/api/user.py | 11 ++-- endpoints/web.py | 4 ++ static/directives/header-bar.html | 2 +- static/js/app.js | 26 ++++++-- static/js/controllers.js | 91 ++++++++++++++++----------- static/partials/new-organization.html | 7 ++- static/partials/org-admin.html | 24 ++++--- static/partials/user-admin.html | 5 +- 12 files changed, 135 insertions(+), 62 deletions(-) diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 61c1f3e6a..97f766da7 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -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 diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index 1f31aa58b..89dda31f0 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -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() \ No newline at end of file + raise Unauthorized() diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 9cb6a267a..f89ddc5d5 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -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') diff --git a/endpoints/api/subscribe.py b/endpoints/api/subscribe.py index f9f9d7f14..efc2dfea7 100644 --- a/endpoints/api/subscribe.py +++ b/endpoints/api/subscribe.py @@ -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: diff --git a/endpoints/api/user.py b/endpoints/api/user.py index f89d7ed62..40194a436 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -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)) diff --git a/endpoints/web.py b/endpoints/web.py index 1e78b12af..e14c70e79 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -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) diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index 1a6ade0b7..05f7e24cf 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -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> diff --git a/static/js/app.js b/static/js/app.js index 532512bb0..4c60e1c4d 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -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'](); } diff --git a/static/js/controllers.js b/static/js/controllers.js index 08bcca561..33b17772f 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -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; } diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html index be99bae98..5f4756cfe 100644 --- a/static/partials/new-organization.html +++ b/static/partials/new-organization.html @@ -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> diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 139579aa8..2b7b0be80 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -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> diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index fcfb157c1..0a0bb1f82 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -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>