From d7592fd1332d518292958a374a60dfa574a8ba7a Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 13 Nov 2013 14:41:20 -0500 Subject: [PATCH 1/3] Update the diffs worker to not fail if the repository is removed before diffs are computed. --- data/model.py | 4 +++- workers/diffsworker.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/data/model.py b/data/model.py index cae167acf..87568d169 100644 --- a/data/model.py +++ b/data/model.py @@ -662,7 +662,9 @@ def get_image_by_id(namespace_name, repository_name, docker_image_id): Image.docker_image_id == docker_image_id)) 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] diff --git a/workers/diffsworker.py b/workers/diffsworker.py index ce81b96f0..cacf48856 100644 --- a/workers/diffsworker.py +++ b/workers/diffsworker.py @@ -8,6 +8,7 @@ from apscheduler.scheduler import Scheduler from data.queue import image_diff_queue from data.database import db as db_connection +from data.model import DataModelException 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) request = json.loads(item.body) - process_image_changes(request['namespace'], request['repository'], - request['image_id']) + try: + 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) From 0f3d942b5e82d619bdc71fa14a58893b71bf1ec1 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Wed, 13 Nov 2013 17:29:26 -0500 Subject: [PATCH 2/3] Re-add the missing method for getting repository builds. Add more logging to try to see why some builds are not getting removed from the queue. --- data/model.py | 8 ++++++++ workers/dockerfilebuild.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/data/model.py b/data/model.py index 87568d169..cfbb9340c 100644 --- a/data/model.py +++ b/data/model.py @@ -912,6 +912,14 @@ def load_token_data(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, include_inactive=True): joined = RepositoryBuild.select().join(Repository) diff --git a/workers/dockerfilebuild.py b/workers/dockerfilebuild.py index b78cb8b0c..a03342ea0 100644 --- a/workers/dockerfilebuild.py +++ b/workers/dockerfilebuild.py @@ -219,9 +219,11 @@ def process_work_items(pool): local_item = item def complete_callback(completed): if completed: + logger.debug('Queue item completed successfully, will be removed.') dockerfile_build_queue.complete(local_item) else: # 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) return complete_callback From e649e669e19d808216ea891f484f082f007f3a82 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 13 Nov 2013 17:47:45 -0500 Subject: [PATCH 3/3] Check in a basic invoice view for organizations --- endpoints/api.py | 32 ++++++++++++++++++++ static/css/quay.css | 48 ++++++++++++++++++++++++++++++ static/js/controllers.js | 21 ++++++++++++++ static/partials/org-admin.html | 53 ++++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+) diff --git a/endpoints/api.py b/endpoints/api.py index e4fe31765..8f93bd0d8 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1252,6 +1252,38 @@ def subscribe(user, plan, token, accepted_plans): return resp +@app.route('/api/organization//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//plan', methods=['PUT']) @api_login_required def subscribe_org_api(orgname): diff --git a/static/css/quay.css b/static/css/quay.css index 59c1638d8..72d6f41d5 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1638,6 +1638,54 @@ p.editable:hover i { 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 { margin-bottom: 20px; } diff --git a/static/js/controllers.js b/static/js/controllers.js index 456c1ebe6..e74c048ac 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1187,6 +1187,10 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService // Load the list of plans. PlanService.getPlans(function(plans) { $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; @@ -1194,6 +1198,23 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService $scope.orgname = orgname; $scope.membersLoading = true; $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() { if ($scope.membersFound) { return; } diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index c9811e82a..bba531439 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -15,6 +15,7 @@ @@ -26,6 +27,58 @@
+ +
+
+ +
+ +
+ No invoices have been created +
+ +
+ + + + + + + + + + + + + + + + + + +
Billing Date/TimeAmount DueStatus
{{ invoice.date * 1000 | date:'medium' }}{{ invoice.amount_due / 100 }} + + Paid - Thank you! + Payment failed - Will retry soon + Payment pending + +
+
+
Billing Period
+
+ {{ invoice.period_start * 1000 | date:'mediumDate' }} - + {{ invoice.period_end * 1000 | date:'mediumDate' }} +
+
Plan
+
+ {{ plan_map[invoice.plan].title }} + {{ plan_map[invoice.plan].price / 100 }} +
+
+
+
+
+