Checkpointing stripe work.
This commit is contained in:
parent
211fd6bcd7
commit
7bd18c1bab
11 changed files with 172 additions and 6 deletions
3
app.py
3
app.py
|
@ -1,5 +1,6 @@
|
|||
import logging
|
||||
import os
|
||||
import stripe
|
||||
|
||||
from flask import Flask
|
||||
from flask.ext.principal import Principal
|
||||
|
@ -29,3 +30,5 @@ login_manager.login_view = 'signin'
|
|||
|
||||
mail = Mail()
|
||||
mail.init_app(app)
|
||||
|
||||
stripe.api_key = app.config['STRIPE_SECRET_KEY']
|
||||
|
|
19
config.py
19
config.py
|
@ -54,7 +54,18 @@ class LocalStorage(object):
|
|||
LOCAL_STORAGE_DIR = '/tmp/registry'
|
||||
|
||||
|
||||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB):
|
||||
class StripeTestConfig(object):
|
||||
STRIPE_SECRET_KEY = 'sk_test_PEbmJCYrLXPW0VRLSnWUiZ7Y'
|
||||
STRIPE_PUBLISHABLE_KEY = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh'
|
||||
|
||||
|
||||
class StripeLiveConfig(object):
|
||||
STRIPE_SECRET_KEY = 'sk_live_TRuTHYwTvmrLeU3ib7Z9hpqE'
|
||||
STRIPE_PUBLISHABLE_KEY = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu'
|
||||
|
||||
|
||||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
||||
StripeTestConfig):
|
||||
REGISTRY_SERVER = 'localhost:5000'
|
||||
LOGGING_CONFIG = {
|
||||
'level': logging.DEBUG,
|
||||
|
@ -62,7 +73,8 @@ class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB):
|
|||
}
|
||||
|
||||
|
||||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL):
|
||||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig):
|
||||
REGISTRY_SERVER = 'localhost:5000'
|
||||
LOGGING_CONFIG = {
|
||||
'level': logging.DEBUG,
|
||||
|
@ -70,7 +82,8 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL):
|
|||
}
|
||||
|
||||
|
||||
class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL):
|
||||
class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||
StripeLiveConfig):
|
||||
REGISTRY_SERVER = 'quay.io'
|
||||
LOGGING_CONFIG = {
|
||||
'stream': sys.stderr,
|
||||
|
|
|
@ -22,6 +22,7 @@ class User(BaseModel):
|
|||
password_hash = CharField()
|
||||
email = CharField(unique=True, index=True)
|
||||
verified = BooleanField(default=False)
|
||||
stripe_id = CharField(index=True, null=True)
|
||||
|
||||
|
||||
class Visibility(BaseModel):
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import stripe
|
||||
|
||||
from flask import request, make_response, jsonify, abort
|
||||
from flask.ext.login import login_required, current_user
|
||||
|
@ -317,3 +318,53 @@ def delete_permissions(namespace, repository, username):
|
|||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
||||
|
||||
def subscription_view(stripe_subscription):
|
||||
return {
|
||||
'current_period_start': stripe_subscription.current_period_start,
|
||||
'current_period_end': stripe_subscription.current_period_end,
|
||||
'plan': stripe_subscription.plan.id,
|
||||
}
|
||||
|
||||
|
||||
@app.route('/api/user/plan', methods=['PUT'])
|
||||
@api_login_required
|
||||
def subscribe():
|
||||
# Amount in cents
|
||||
amount = 500
|
||||
|
||||
request_data = request.get_json()
|
||||
plan = request_data['plan']
|
||||
card = request_data['token']
|
||||
|
||||
user = current_user.db_user
|
||||
|
||||
if not user.stripe_id:
|
||||
# Create the customer and plan simultaneously
|
||||
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
|
||||
user.stripe_id = cus.id
|
||||
user.save()
|
||||
|
||||
resp = jsonify(subscription_view(cus.subscription))
|
||||
resp.status_code = 201
|
||||
return resp
|
||||
|
||||
else:
|
||||
# Change the plan
|
||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||
cus.plan = plan
|
||||
cus.save()
|
||||
return jsonify(subscription_view(cus.subscription))
|
||||
|
||||
|
||||
@app.route('/api/user/plan', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_subscription():
|
||||
user = current_user.db_user
|
||||
|
||||
if user.stripe_id:
|
||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||
return jsonify(subscription_view(cus.subscription))
|
||||
|
||||
abort(404)
|
||||
|
|
|
@ -6,4 +6,5 @@ Flask-Login
|
|||
Flask-Mail
|
||||
python-dateutil
|
||||
boto
|
||||
pymysql
|
||||
pymysql
|
||||
stripe
|
|
@ -9,9 +9,14 @@ Werkzeug==0.9.4
|
|||
argparse==1.2.1
|
||||
blinker==1.3
|
||||
boto==2.13.3
|
||||
distribute==0.6.34
|
||||
ipdb==0.8
|
||||
ipython==1.1.0
|
||||
itsdangerous==0.23
|
||||
peewee==2.1.4
|
||||
py-bcrypt==0.4
|
||||
python-dateutil==2.1
|
||||
requests==2.0.0
|
||||
six==1.4.1
|
||||
stripe==1.9.5
|
||||
wsgiref==0.1.2
|
||||
|
|
|
@ -72,6 +72,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment'], function($pro
|
|||
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}).
|
||||
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
|
||||
when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||
when('/user', {title: 'User Admin', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
|
||||
when('/', {title: 'Quay', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
||||
otherwise({redirectTo: '/'});
|
||||
}]).
|
||||
|
|
|
@ -175,7 +175,7 @@ function RepoCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
$scope.listImages = function() {
|
||||
if ($scope.imageHistory) { return; }
|
||||
|
||||
var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/tag/' + $scope.currentTag.name + '/images');
|
||||
var imageFetch = Restangular.one('repository/' + namespace + '/' + name + '/tag/' + $scope.currentTag.name + '/images');
|
||||
imageFetch.get().then(function(resp) {
|
||||
$scope.imageHistory = resp.images;
|
||||
});
|
||||
|
@ -365,4 +365,74 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
$rootScope.title = 'Unknown Repository';
|
||||
$scope.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
function UserAdminCtrl($scope, Restangular) {
|
||||
$scope.plans = [
|
||||
{
|
||||
title: 'Micro',
|
||||
price: 700,
|
||||
privateRepos: 5,
|
||||
stripeId: 'micro',
|
||||
},
|
||||
{
|
||||
title: 'Small',
|
||||
price: 1200,
|
||||
privateRepos: 10,
|
||||
stripeId: 'small',
|
||||
},
|
||||
{
|
||||
title: 'Medium',
|
||||
price: 2200,
|
||||
privateRepos: 20,
|
||||
stripeId: 'medium',
|
||||
},
|
||||
];
|
||||
|
||||
var planDict = {};
|
||||
var i;
|
||||
for(i = 0; i < $scope.plans.length; i++) {
|
||||
planDict[$scope.plans[i].stripeId] = $scope.plans[i];
|
||||
}
|
||||
|
||||
var getSubscription = Restangular.one('user/plan');
|
||||
getSubscription.get().then(function(sub) {
|
||||
// User has a subscription
|
||||
$scope.subscription = sub;
|
||||
});
|
||||
|
||||
$scope.subscribe = function(planId) {
|
||||
var submitToken = function(token) {
|
||||
$scope.$apply(function() {
|
||||
var subscriptionDetails = {
|
||||
token: token.id,
|
||||
plan: planId,
|
||||
};
|
||||
|
||||
console.log(subscriptionDetails);
|
||||
|
||||
var createSubscriptionRequest = Restangular.one('user/plan');
|
||||
createSubscriptionRequest.customPUT(subscriptionDetails).then(function() {
|
||||
// Success
|
||||
console.log('successfully created subscription');
|
||||
}, function() {
|
||||
// Failure
|
||||
console.log('failed to created subscription');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
console.log('Got request for plan: ' + planId);
|
||||
var planDetails = planDict[planId]
|
||||
StripeCheckout.open({
|
||||
key: 'pk_test_uEDHANKm9CHCvVa2DLcipGRh',
|
||||
address: false, // TODO change to true
|
||||
amount: planDetails.price,
|
||||
currency: 'usd',
|
||||
name: 'Quay ' + planDetails.title + ' Subscription',
|
||||
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
|
||||
panelLabel: 'Subscribe',
|
||||
token: submitToken
|
||||
});
|
||||
};
|
||||
}
|
21
static/partials/user-admin.html
Normal file
21
static/partials/user-admin.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4" ng-repeat='plan in plans'>
|
||||
<div class="panel" ng-class="{'panel-success': subscription.plan == plan.stripeId, 'panel-default': subscription.plan != plan.stripeId}">
|
||||
<div class="panel-heading">
|
||||
{{ plan.title }}
|
||||
<span class="pull-right" ng-show="subscription.plan == plan.stripeId">
|
||||
<i class="icon-ok"></i>
|
||||
Subscribed
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{{ plan.privateRepos }} Private Repositories<br>
|
||||
<button class="btn btn-primary" ng-hide="subscription" ng-click="subscribe(plan.stripeId)">Subscribe</button>
|
||||
<button class="btn" ng-show="subscription && (subscription.plan != plan.stripeId)" ng-click="subscribe(plan.stripeId)">Change</button>
|
||||
<button class="btn btn-danger" ng-show="subscription.plan == plan.stripeId" ng-click="cancel()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -10,9 +10,9 @@
|
|||
|
||||
<link rel="stylesheet" href="/static/css/quay.css">
|
||||
|
||||
|
||||
<script src="//code.jquery.com/jquery.js"></script>
|
||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
|
||||
<script src="https://checkout.stripe.com/v2/checkout.js"></script>
|
||||
|
||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.min.js"></script>
|
||||
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
|
||||
|
|
BIN
test.db
BIN
test.db
Binary file not shown.
Reference in a new issue