Merge branch 'master' of https://bitbucket.org/yackob03/quay
This commit is contained in:
commit
5d396ec8ea
7 changed files with 180 additions and 3 deletions
|
@ -662,7 +662,9 @@ def get_image_by_id(namespace_name, repository_name, docker_image_id):
|
||||||
Image.docker_image_id == docker_image_id))
|
Image.docker_image_id == docker_image_id))
|
||||||
|
|
||||||
if not fetched:
|
if not fetched:
|
||||||
raise DataModelException('Unable to find image for tag with repo.')
|
raise DataModelException('Unable to find image \'%s\' for repo \'%s/%s\'' %
|
||||||
|
(docker_image_id, namespace_name,
|
||||||
|
repository_name))
|
||||||
|
|
||||||
return fetched[0]
|
return fetched[0]
|
||||||
|
|
||||||
|
@ -910,6 +912,14 @@ def load_token_data(code):
|
||||||
raise InvalidTokenException('Invalid delegate token code: %s' % code)
|
raise InvalidTokenException('Invalid delegate token code: %s' % code)
|
||||||
|
|
||||||
|
|
||||||
|
def get_repository_build(request_dbid):
|
||||||
|
try:
|
||||||
|
return RepositoryBuild.get(RepositoryBuild.id == request_dbid)
|
||||||
|
except RepositoryBuild.DoesNotExist:
|
||||||
|
msg = 'Unable to locate a build by id: %s' % request_dbid
|
||||||
|
raise InvalidRepositoryBuildException(msg)
|
||||||
|
|
||||||
|
|
||||||
def list_repository_builds(namespace_name, repository_name,
|
def list_repository_builds(namespace_name, repository_name,
|
||||||
include_inactive=True):
|
include_inactive=True):
|
||||||
joined = RepositoryBuild.select().join(Repository)
|
joined = RepositoryBuild.select().join(Repository)
|
||||||
|
|
|
@ -1252,6 +1252,38 @@ def subscribe(user, plan, token, accepted_plans):
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/organization/<orgname>/invoices', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
def org_invoices_api(orgname):
|
||||||
|
def invoice_view(i):
|
||||||
|
return {
|
||||||
|
'id': i.id,
|
||||||
|
'date': i.date,
|
||||||
|
'period_start': i.period_start,
|
||||||
|
'period_end': i.period_end,
|
||||||
|
'paid': i.paid,
|
||||||
|
'amount_due': i.amount_due,
|
||||||
|
'next_payment_attempt': i.next_payment_attempt,
|
||||||
|
'attempted': i.attempted,
|
||||||
|
'closed': i.closed,
|
||||||
|
'total': i.total,
|
||||||
|
'plan': i.lines.data[0].plan.id
|
||||||
|
}
|
||||||
|
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
organization = model.get_organization(orgname)
|
||||||
|
if not organization.stripe_id:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
invoices = stripe.Invoice.all(customer=organization.stripe_id, count=12)
|
||||||
|
return jsonify({
|
||||||
|
'invoices': [invoice_view(i) for i in invoices.data]
|
||||||
|
})
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/plan', methods=['PUT'])
|
@app.route('/api/organization/<orgname>/plan', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def subscribe_org_api(orgname):
|
def subscribe_org_api(orgname):
|
||||||
|
|
|
@ -1638,6 +1638,54 @@ p.editable:hover i {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-title {
|
||||||
|
padding: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-status .success {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-status .pending {
|
||||||
|
color: steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-status .danger {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-amount:before {
|
||||||
|
content: '$';
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-details {
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 6px;
|
||||||
|
border-left: 2px solid #eee !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-details td {
|
||||||
|
border: 0px solid transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-details dl {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-details dd {
|
||||||
|
margin-left: 10px;
|
||||||
|
padding: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin .invoice-title:hover {
|
||||||
|
color: steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
.org-list h2 {
|
.org-list h2 {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1187,6 +1187,10 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
||||||
// Load the list of plans.
|
// Load the list of plans.
|
||||||
PlanService.getPlans(function(plans) {
|
PlanService.getPlans(function(plans) {
|
||||||
$scope.plans = plans.business;
|
$scope.plans = plans.business;
|
||||||
|
$scope.plan_map = {};
|
||||||
|
for (var i = 0; i < plans.business.length; ++i) {
|
||||||
|
$scope.plan_map[plans.business[i].stripeId] = plans.business[i];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var orgname = $routeParams.orgname;
|
var orgname = $routeParams.orgname;
|
||||||
|
@ -1194,6 +1198,23 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
||||||
$scope.orgname = orgname;
|
$scope.orgname = orgname;
|
||||||
$scope.membersLoading = true;
|
$scope.membersLoading = true;
|
||||||
$scope.membersFound = null;
|
$scope.membersFound = null;
|
||||||
|
$scope.invoiceLoading = true;
|
||||||
|
|
||||||
|
$scope.loadInvoices = function() {
|
||||||
|
if ($scope.invoices) { return; }
|
||||||
|
$scope.invoiceLoading = true;
|
||||||
|
|
||||||
|
var getInvoices = Restangular.one(getRestUrl('organization', orgname, 'invoices'));
|
||||||
|
getInvoices.get().then(function(resp) {
|
||||||
|
$scope.invoiceExpanded = {};
|
||||||
|
$scope.invoices = resp.invoices;
|
||||||
|
$scope.invoiceLoading = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.toggleInvoice = function(id) {
|
||||||
|
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
|
||||||
|
};
|
||||||
|
|
||||||
$scope.loadMembers = function() {
|
$scope.loadMembers = function() {
|
||||||
if ($scope.membersFound) { return; }
|
if ($scope.membersFound) { return; }
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<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 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="#members" ng-click="loadMembers()">Members</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="#billing" ng-click="loadInvoices()">Billing</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -26,6 +27,58 @@
|
||||||
<div class="plan-manager" organization="orgname"></div>
|
<div class="plan-manager" organization="orgname"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Billing tab -->
|
||||||
|
<div id="billing" class="tab-pane">
|
||||||
|
<div ng-show="invoiceLoading">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="!invoiceLoading && !invoices">
|
||||||
|
No invoices have been created
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="!invoiceLoading && invoices">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<th>Billing Date/Time</th>
|
||||||
|
<th>Amount Due</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="invoice" ng-repeat="invoice in invoices">
|
||||||
|
<tr class="invoice-title" ng-click="toggleInvoice(invoice.id)">
|
||||||
|
<td><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
|
||||||
|
<td><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
|
||||||
|
<td>
|
||||||
|
<span class="invoice-status">
|
||||||
|
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
|
||||||
|
<span class="danger" ng-show="!invoice.paid && invoice.attempted">Payment failed - Will retry soon</span>
|
||||||
|
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
|
||||||
|
<td colspan="3">
|
||||||
|
<dl class="dl-normal">
|
||||||
|
<dt>Billing Period</dt>
|
||||||
|
<dd>
|
||||||
|
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
|
||||||
|
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
|
||||||
|
</dd>
|
||||||
|
<dt>Plan</dt>
|
||||||
|
<dd>
|
||||||
|
<span>{{ plan_map[invoice.plan].title }}</span>
|
||||||
|
<span>{{ plan_map[invoice.plan].price / 100 }}</span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Members tab -->
|
<!-- Members tab -->
|
||||||
<div id="members" class="tab-pane">
|
<div id="members" class="tab-pane">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="membersLoading"></i>
|
<i class="fa fa-spinner fa-spin fa-3x" ng-show="membersLoading"></i>
|
||||||
|
|
|
@ -8,6 +8,7 @@ from apscheduler.scheduler import Scheduler
|
||||||
|
|
||||||
from data.queue import image_diff_queue
|
from data.queue import image_diff_queue
|
||||||
from data.database import db as db_connection
|
from data.database import db as db_connection
|
||||||
|
from data.model import DataModelException
|
||||||
from endpoints.registry import process_image_changes
|
from endpoints.registry import process_image_changes
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,8 +30,18 @@ def process_work_items():
|
||||||
logger.debug('Queue gave us some work: %s' % item.body)
|
logger.debug('Queue gave us some work: %s' % item.body)
|
||||||
|
|
||||||
request = json.loads(item.body)
|
request = json.loads(item.body)
|
||||||
process_image_changes(request['namespace'], request['repository'],
|
try:
|
||||||
request['image_id'])
|
image_id = request['image_id']
|
||||||
|
namespace = request['namespace']
|
||||||
|
repository = request['repository']
|
||||||
|
|
||||||
|
process_image_changes(namespace, repository, image_id)
|
||||||
|
except DataModelException:
|
||||||
|
# This exception is unrecoverable, and the item should continue and be
|
||||||
|
# marked as complete.
|
||||||
|
msg = ('Image does not exist in database \'%s\' for repo \'%s/\'%s\'' %
|
||||||
|
(image_id, namespace, repository))
|
||||||
|
logger.warning(msg)
|
||||||
|
|
||||||
image_diff_queue.complete(item)
|
image_diff_queue.complete(item)
|
||||||
|
|
||||||
|
|
|
@ -219,9 +219,11 @@ def process_work_items(pool):
|
||||||
local_item = item
|
local_item = item
|
||||||
def complete_callback(completed):
|
def complete_callback(completed):
|
||||||
if completed:
|
if completed:
|
||||||
|
logger.debug('Queue item completed successfully, will be removed.')
|
||||||
dockerfile_build_queue.complete(local_item)
|
dockerfile_build_queue.complete(local_item)
|
||||||
else:
|
else:
|
||||||
# We have a retryable error, add the job back to the queue
|
# We have a retryable error, add the job back to the queue
|
||||||
|
logger.debug('Queue item incomplete, will be retryed.')
|
||||||
dockerfile_build_queue.incomplete(local_item)
|
dockerfile_build_queue.incomplete(local_item)
|
||||||
|
|
||||||
return complete_callback
|
return complete_callback
|
||||||
|
|
Reference in a new issue