From c20e7dbcf70126bf79658da343c34d5d92806dc4 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 20 Dec 2013 22:38:53 -0500 Subject: [PATCH] - Add some more analytics events - Enable business features for personal users on business plans - Fix a bug in the credit card image view --- endpoints/api.py | 49 +++++++++++++---- endpoints/web.py | 33 +++++++---- static/css/quay.css | 20 +++---- static/directives/billing-invoices.html | 53 ++++++++++++++++++ static/directives/billing-options.html | 2 +- static/js/app.js | 73 +++++++++++++++++++++++-- static/js/controllers.js | 45 ++++++++------- static/partials/new-organization.html | 3 +- static/partials/org-admin.html | 55 +------------------ static/partials/user-admin.html | 29 +++++++--- 10 files changed, 241 insertions(+), 121 deletions(-) create mode 100644 static/directives/billing-invoices.html diff --git a/endpoints/api.py b/endpoints/api.py index 256fec8a9..205fabb0b 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1601,9 +1601,31 @@ def subscribe(user, plan, token, require_business_plan): return resp +@app.route('/api/user/invoices', methods=['GET']) +@api_login_required +def user_invoices_api(): + user = current_user.db_user() + if not user.stripe_id: + abort(404) + + return get_invoices(user.stripe_id) + + @app.route('/api/organization//invoices', methods=['GET']) @api_login_required def org_invoices_api(orgname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + organization = model.get_organization(orgname) + if not organization.stripe_id: + abort(404) + + return get_invoices(organization.stripe_id) + + abort(403) + + +def get_invoices(customer_id): def invoice_view(i): return { 'id': i.id, @@ -1619,18 +1641,10 @@ def org_invoices_api(orgname): 'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None } - 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) + invoices = stripe.Invoice.all(customer=customer_id, count=12) + return jsonify({ + 'invoices': [invoice_view(i) for i in invoices.data] + }) @app.route('/api/organization//plan', methods=['PUT']) @@ -1815,6 +1829,17 @@ def org_logs_api(orgname): abort(403) +@app.route('/api/user/logs', methods=['GET']) +@api_login_required +def user_logs_api(): + performer_name = request.args.get('performer', None) + start_time = request.args.get('starttime', None) + end_time = request.args.get('endtime', None) + + return get_logs(current_user.db_user().username, start_time, end_time, + performer_name=performer_name) + + def get_logs(namespace, start_time, end_time, performer_name=None, repository=None): performer = None diff --git a/endpoints/web.py b/endpoints/web.py index 22fb279a1..c11c9ac3e 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -4,7 +4,7 @@ import stripe from flask import (abort, redirect, request, url_for, render_template, make_response, Response) -from flask.ext.login import login_user, UserMixin +from flask.ext.login import login_user, UserMixin, current_user from flask.ext.principal import identity_changed from urlparse import urlparse @@ -134,21 +134,34 @@ def privacy(): @app.route('/receipt', methods=['GET']) def receipt(): + if not current_user.is_authenticated(): + abort(401) + return + id = request.args.get('id') if id: invoice = stripe.Invoice.retrieve(id) if invoice: - org = model.get_user_or_org_by_customer_id(invoice.customer) - if org and org.organization: - admin_org = AdministerOrganizationPermission(org.username) - if admin_org.can(): - file_data = renderInvoiceToPdf(invoice, org) - return Response(file_data, - mimetype="application/pdf", - headers={"Content-Disposition": - "attachment;filename=receipt.pdf"}) + user_or_org = model.get_user_or_org_by_customer_id(invoice.customer) + + if user_or_org: + if user_or_org.organization: + admin_org = AdministerOrganizationPermission(user_or_org.username) + if not admin_org.can(): + abort(404) + return + else: + if not user_or_org.username == current_user.db_user().username: + abort(404) + return + + file_data = renderInvoiceToPdf(invoice, user_or_org) + return Response(file_data, + mimetype="application/pdf", + headers={"Content-Disposition": "attachment;filename=receipt.pdf"}) abort(404) + def common_login(db_user): if login_user(_LoginWrappedDBUser(db_user.username, db_user)): logger.debug('Successfully signed in as: %s' % db_user.username) diff --git a/static/css/quay.css b/static/css/quay.css index 46c972cdd..b0e981909 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1919,28 +1919,28 @@ p.editable:hover i { display: inline-block; } -.org-admin .invoice-title { +.billing-invoices-element .invoice-title { padding: 6px; cursor: pointer; } -.org-admin .invoice-status .success { +.billing-invoices-element .invoice-status .success { color: green; } -.org-admin .invoice-status .pending { +.billing-invoices-element .invoice-status .pending { color: steelblue; } -.org-admin .invoice-status .danger { +.billing-invoices-element .invoice-status .danger { color: red; } -.org-admin .invoice-amount:before { +.billing-invoices-element .invoice-amount:before { content: '$'; } -.org-admin .invoice-details { +.billing-invoices-element .invoice-details { margin-left: 10px; margin-bottom: 10px; @@ -1949,21 +1949,21 @@ p.editable:hover i { border-left: 2px solid #eee !important; } -.org-admin .invoice-details td { +.billing-invoices-element .invoice-details td { border: 0px solid transparent !important; } -.org-admin .invoice-details dl { +.billing-invoices-element .invoice-details dl { margin: 0px; } -.org-admin .invoice-details dd { +.billing-invoices-element .invoice-details dd { margin-left: 10px; padding: 6px; margin-bottom: 10px; } -.org-admin .invoice-title:hover { +.billing-invoices-element .invoice-title:hover { color: steelblue; } diff --git a/static/directives/billing-invoices.html b/static/directives/billing-invoices.html new file mode 100644 index 000000000..fc64b1abf --- /dev/null +++ b/static/directives/billing-invoices.html @@ -0,0 +1,53 @@ +
+
+
+
+ +
+ No invoices have been created +
+ +
+ + + + + + + + + + + + + + + + + + + + +
Billing Date/TimeAmount DueStatus
{{ invoice.date * 1000 | date:'medium' }}{{ invoice.amount_due / 100 }} + + Paid - Thank you! + Payment failed + Payment failed - Will retry soon + Payment pending + + + + + +
+
+
Billing Period
+
+ {{ invoice.period_start * 1000 | date:'mediumDate' }} - + {{ invoice.period_end * 1000 | date:'mediumDate' }} +
+
+
+
+ +
diff --git a/static/directives/billing-options.html b/static/directives/billing-options.html index 3b43e6805..8ae5115d5 100644 --- a/static/directives/billing-options.html +++ b/static/directives/billing-options.html @@ -7,7 +7,7 @@
- + ****-****-****-{{ currentCard.last4 }} diff --git a/static/js/app.js b/static/js/app.js index daf28dc24..c7a45f3bb 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -969,6 +969,59 @@ quayApp.filter('visibleLogFilter', function () { }); +quayApp.directive('billingInvoices', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/billing-invoices.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'organization': '=organization', + 'user': '=user', + 'visible': '=visible' + }, + controller: function($scope, $element, $sce, Restangular) { + $scope.loading = false; + $scope.invoiceExpanded = {}; + + $scope.toggleInvoice = function(id) { + $scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id]; + }; + + var update = function() { + var hasValidUser = !!$scope.user; + var hasValidOrg = !!$scope.organization; + var isValid = hasValidUser || hasValidOrg; + + if (!$scope.visible || !isValid) { + return; + } + + $scope.loading = true; + + var url = getRestUrl('user/invoices'); + if ($scope.organization) { + url = getRestUrl('organization', $scope.organization.name, 'invoices'); + } + + var getInvoices = Restangular.one(url); + getInvoices.get().then(function(resp) { + $scope.invoices = resp.invoices; + $scope.loading = false; + }); + }; + + $scope.$watch('organization', update); + $scope.$watch('user', update); + $scope.$watch('visible', update); + } + }; + + return directiveDefinitionObject; +}); + + quayApp.directive('logsView', function () { var directiveDefinitionObject = { priority: 0, @@ -2112,15 +2165,27 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi // Check if we need to redirect based on a previously chosen plan. PlanService.handleNotedPlan(); - var changeTab = function(activeTab) { + var changeTab = function(activeTab, opt_timeout) { + var checkCount = 0; + $timeout(function() { + if (checkCount > 5) { return; } + checkCount++; + $('a[data-toggle="tab"]').each(function(index) { var tabName = this.getAttribute('data-target').substr(1); - if (tabName == activeTab) { - this.click(); + if (tabName != activeTab) { + return; } + + if (this.clientWidth == 0) { + changeTab(activeTab, 500); + return; + } + + this.click(); }); - }); + }, opt_timeout); }; var resetDefaultTab = function() { diff --git a/static/js/controllers.js b/static/js/controllers.js index 108b00ac0..2054f3e84 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -602,8 +602,22 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us $('.form-change-pw').popover(); + $scope.logsShown = 0; + $scope.invoicesShown = 0; + + $scope.loadLogs = function() { + if (!$scope.hasPaidBusinessPlan) { return; } + $scope.logsShown++; + }; + + $scope.loadInvoices = function() { + if (!$scope.hasPaidBusinessPlan) { return; } + $scope.invoicesShown++; + }; + $scope.planChanged = function(plan) { $scope.hasPaidPlan = plan && plan.price > 0; + $scope.hasPaidBusinessPlan = PlanService.isOrgCompatible(plan) && plan.price > 0; }; $scope.showConvertForm = function() { @@ -981,11 +995,6 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) { var orgname = $routeParams.orgname; - $('.info-icon').popover({ - 'trigger': 'hover', - 'html': true - }); - $scope.TEAM_PATTERN = TEAM_PATTERN; $rootScope.title = 'Loading...'; @@ -1053,6 +1062,11 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) $scope.organization = org; $rootScope.title = orgname; $rootScope.description = 'Viewing organization ' + orgname; + + $('.info-icon').popover({ + 'trigger': 'hover', + 'html': true + }); }); }; @@ -1078,31 +1092,20 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService $scope.membersFound = null; $scope.invoiceLoading = true; $scope.logsShown = 0; + $scope.invoicesShown = 0; $scope.loadLogs = function() { $scope.logsShown++; }; + $scope.loadInvoices = function() { + $scope.invoicesShown++; + }; + $scope.planChanged = function(plan) { $scope.hasPaidPlan = plan && plan.price > 0; }; - $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; } $scope.membersLoading = true; diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html index 3033a20b7..79d29e3f9 100644 --- a/static/partials/new-organization.html +++ b/static/partials/new-organization.html @@ -72,7 +72,8 @@
-
diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index a4843005e..5c506cc6d 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -40,60 +40,7 @@
-
-
-
- -
- No invoices have been created -
- -
- - - - - - - - - - - - - - - - - - - - -
Billing Date/TimeAmount DueStatus
{{ invoice.date * 1000 | date:'medium' }}{{ invoice.amount_due / 100 }} - - Paid - Thank you! - Payment failed - Payment failed - Will retry soon - Payment pending - - - - - -
-
-
Billing Period
-
- {{ invoice.period_start * 1000 | date:'mediumDate' }} - - {{ invoice.period_end * 1000 | date:'mediumDate' }} -
-
Plan
-
- {{ invoice.plan ? plan_map[invoice.plan].title : '(N/A)' }} -
-
-
-
+
diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index 746af084d..1b05ac382 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -27,16 +27,23 @@
-
+
+ +
+
+
+
@@ -69,10 +76,15 @@
-
+
+ +
+
+
+
@@ -86,11 +98,11 @@
-
- Converting a user account into an organization cannot be undone.
Here be many fire-breathing dragons! +
+ Note: Converting a user account into an organization cannot be undone
- +
@@ -113,7 +125,7 @@ ng-model="org.adminUser" required autofocus> - The username and password for an existing account that will become administrator of the organization + The username and password for the account that will become administrator of the organization
@@ -123,7 +135,8 @@
-