Merge branch 'master' into gitfix

This commit is contained in:
Joseph Schorr 2015-06-16 15:18:24 -04:00
commit 91c829bd14
18 changed files with 466 additions and 34 deletions

View file

@ -1,5 +1,12 @@
# vim: ft=nginx
set_real_ip_from 0.0.0.0/0;
real_ip_recursive on;
log_format lb_pp '$remote_addr ($proxy_protocol_addr) '
'- $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"'
types_hash_max_size 2048;
include /usr/local/nginx/conf/mime.types.default;

View file

@ -26,18 +26,19 @@ http {
# This header must be set only for HTTPS
add_header Strict-Transport-Security "max-age=63072000; preload";
}
server {
include proxy-protocol.conf;
include server-base.conf;
listen 8443 default proxy_protocol;
ssl on;
# This header must be set only for HTTPS
add_header Strict-Transport-Security "max-age=63072000; preload";
real_ip_header proxy_protocol;
access_log /dev/stdout lb_pp;
}
}

View file

@ -1,8 +0,0 @@
# vim: ft=nginx
set_real_ip_from 0.0.0.0/0;
real_ip_header proxy_protocol;
log_format elb_pp '$proxy_protocol_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /dev/stdout elb_pp;

View file

@ -2928,4 +2928,3 @@ def revert_tag(repository, tag_name, docker_image_id):
return create_or_update_tag(repository.namespace_user.username, repository.name,
tag_name, docker_image_id, reversion=True)

View file

@ -13,6 +13,8 @@ from data import model
from data.billing import PLANS
import features
import uuid
import json
def carderror_response(e):
return {'carderror': e.message}, 402
@ -96,6 +98,48 @@ def get_invoices(customer_id):
}
def get_invoice_fields(user):
try:
cus = billing.Customer.retrieve(user.stripe_id)
except stripe.APIConnectionError:
abort(503, message='Cannot contact Stripe')
if not 'metadata' in cus:
cus.metadata = {}
return json.loads(cus.metadata.get('invoice_fields') or '[]'), cus
def create_billing_invoice_field(user, title, value):
new_field = {
'uuid': str(uuid.uuid4()).split('-')[0],
'title': title,
'value': value
}
invoice_fields, cus = get_invoice_fields(user)
invoice_fields.append(new_field)
if not 'metadata' in cus:
cus.metadata = {}
cus.metadata['invoice_fields'] = json.dumps(invoice_fields)
cus.save()
return new_field
def delete_billing_invoice_field(user, field_uuid):
invoice_fields, cus = get_invoice_fields(user)
invoice_fields = [field for field in invoice_fields if not field['uuid'] == field_uuid]
if not 'metadata' in cus:
cus.metadata = {}
cus.metadata['invoice_fields'] = json.dumps(invoice_fields)
cus.save()
return True
@resource('/v1/plans/')
@show_if(features.BILLING)
class ListPlans(ApiResource):
@ -367,3 +411,159 @@ class OrgnaizationInvoiceList(ApiResource):
return get_invoices(organization.stripe_id)
raise Unauthorized()
@resource('/v1/user/invoice/fields')
@internal_only
@show_if(features.BILLING)
class UserInvoiceFieldList(ApiResource):
""" Resource for listing and creating a user's custom invoice fields. """
schemas = {
'InvoiceField': {
'id': 'InvoiceField',
'type': 'object',
'description': 'Description of an invoice field',
'required': [
'title', 'value'
],
'properties': {
'title': {
'type': 'string',
'description': 'The title of the field being added',
},
'value': {
'type': 'string',
'description': 'The value of the field being added',
},
},
},
}
@require_user_admin
@nickname('listUserInvoiceFields')
def get(self):
""" List the invoice fields for the current user. """
user = get_authenticated_user()
if not user.stripe_id:
raise NotFound()
return {'fields': get_invoice_fields(user)[0]}
@require_user_admin
@nickname('createUserInvoiceField')
@validate_json_request('InvoiceField')
def post(self):
""" Creates a new invoice field. """
user = get_authenticated_user()
if not user.stripe_id:
raise NotFound()
data = request.get_json()
created_field = create_billing_invoice_field(user, data['title'], data['value'])
return created_field
@resource('/v1/user/invoice/field/<field_uuid>')
@internal_only
@show_if(features.BILLING)
class UserInvoiceField(ApiResource):
""" Resource for deleting a user's custom invoice fields. """
@require_user_admin
@nickname('deleteUserInvoiceField')
def delete(self, field_uuid):
""" Deletes the invoice field for the current user. """
user = get_authenticated_user()
if not user.stripe_id:
raise NotFound()
result = delete_billing_invoice_field(user, field_uuid)
if not result:
abort(404)
return 'Okay', 201
@resource('/v1/organization/<orgname>/invoice/fields')
@path_param('orgname', 'The name of the organization')
@related_user_resource(UserInvoiceFieldList)
@internal_only
@show_if(features.BILLING)
class OrganizationInvoiceFieldList(ApiResource):
""" Resource for listing and creating an organization's custom invoice fields. """
schemas = {
'InvoiceField': {
'id': 'InvoiceField',
'type': 'object',
'description': 'Description of an invoice field',
'required': [
'title', 'value'
],
'properties': {
'title': {
'type': 'string',
'description': 'The title of the field being added',
},
'value': {
'type': 'string',
'description': 'The value of the field being added',
},
},
},
}
@require_scope(scopes.ORG_ADMIN)
@nickname('listOrgInvoiceFields')
def get(self, orgname):
""" List the invoice fields for the organization. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
if not organization.stripe_id:
raise NotFound()
return {'fields': get_invoice_fields(organization)[0]}
abort(403)
@require_scope(scopes.ORG_ADMIN)
@nickname('createOrgInvoiceField')
@validate_json_request('InvoiceField')
def post(self, orgname):
""" Creates a new invoice field. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
if not organization.stripe_id:
raise NotFound()
data = request.get_json()
created_field = create_billing_invoice_field(organization, data['title'], data['value'])
return created_field
abort(403)
@resource('/v1/organization/<orgname>/invoice/field/<field_uuid>')
@path_param('orgname', 'The name of the organization')
@related_user_resource(UserInvoiceField)
@internal_only
@show_if(features.BILLING)
class OrganizationInvoiceField(ApiResource):
""" Resource for deleting an organization's custom invoice fields. """
@require_scope(scopes.ORG_ADMIN)
@nickname('deleteOrgInvoiceField')
def delete(self, orgname, field_uuid):
""" Deletes the invoice field for the current user. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
organization = model.get_organization(orgname)
if not organization.stripe_id:
raise NotFound()
result = delete_billing_invoice_field(organization, field_uuid)
if not result:
abort(404)
return 'Okay', 201
abort(403)

View file

@ -1,3 +1,6 @@
.billing-invoices-element .fields-menu {
float: right;
}
.billing-invoices-element .invoice-title {
padding: 6px;
@ -22,4 +25,20 @@
.billing-invoices-element .fa-download {
color: #aaa;
}
.billing-invoices-element .fa-trash-o {
float: right;
margin-top: -3px;
margin-right: -14px !important;
font-size: 14px;
padding: 2px;
padding-left: 6px;
padding-right: 6px;
border-radius: 4px;
}
.billing-invoices-element .invoice-field {
padding-top: 6px;
padding-bottom: 6px;
}

View file

@ -9,6 +9,30 @@
</div>
<div ng-show="!loading && invoices.length">
<div class="dropdown fields-menu" data-title="Custom Invoice Fields" bs-tooltip>
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown">
<i class="fa fa-bars"></i>
<span class="caret"></span>
</button>
<ul class="dropdown-menu pull-right" role="menu">
<li role="presentation" ng-repeat="invoiceField in invoiceFields">
<a class="invoice-field" role="menuitem" tabindex="-1" href="javascript:void(0)" >
{{ invoiceField.title }}: {{ invoiceField.value }}
<i class="fa fa-trash-o btn btn-danger" ng-click="askDeleteField(invoiceField)"></i>
</a>
</li>
<li role="presentation" class="disabled" ng-if="!invoiceFields.length">
<a role="menuitem" tabindex="-1" href="javascript:void(0)">No Custom Fields Defined</a>
</li>
<li role="presentation" class="divider"></li>
<li role="presentation">
<a role="menuitem" tabindex="-1" href="javascript:void(0)" ng-click="showCreateField()">
<i class="fa fa-plus"></i>Add Custom Invoice Field
</a>
</li>
</ul>
</div>
<table class="co-table">
<thead>
<td>Billing Date/Time</td>
@ -39,4 +63,23 @@
</table>
</div>
<!-- Delete Tag Confirm -->
<div class="cor-confirm-dialog"
dialog-context="createFieldInfo"
dialog-action="createCustomField(info.title, info.value, callback)"
dialog-title="Create Custom Field"
dialog-action-title="Create Field">
<form>
<div class="form-group">
<label for="titleInput">Enter Field Title</label>
<input id="titleInput" type="text" class="form-control"ng-model="createFieldInfo.title" placeholder="Field Title">
</div>
<div class="form-group">
<label for="valueInput">Enter Field Value</label>
<input id="valueInput" type="text" class="form-control" ng-model="createFieldInfo.value" placeholder="Field Value">
</div>
</form>
</div>
</div>
</div>

View file

@ -3,12 +3,12 @@
<span id="logs-range" class="mini">
<span class="date-line">
<span class="date-line-caption">From</span>
<input type="text" class="logs-date-picker input-sm" name="start" ng-model="logStartDate" data-max-date="{{ logEndDate }}" data-container="body" bs-datepicker/>
<input type="text" class="logs-date-picker input-sm" name="start" ng-model="options.logStartDate" data-max-date="{{ options.logEndDate }}" data-container="body" bs-datepicker/>
</span>
<span class="date-line">
<span class="date-line-caption add-on">to</span>
<input type="text" class="logs-date-picker input-sm" name="end" ng-model="logEndDate" data-min-date="{{ logStartDate }}" bs-datepicker/>
<input type="text" class="logs-date-picker input-sm" name="end" ng-model="options.logEndDate" data-min-date="{{ options.logStartDate }}" bs-datepicker/>
</span>
</span>
<span class="hidden-xs right">

View file

@ -189,7 +189,8 @@
repository="repository"
trigger="currentSetupTrigger"
canceled="cancelSetupTrigger(trigger)"
counter="showTriggerSetupCounter"></div>
counter="showTriggerSetupCounter"
trigger-runner="askRunTrigger(trigger)"></div>
<!-- Manual trigger dialog -->
<div class="manual-trigger-build-dialog"

View file

@ -122,7 +122,10 @@
ng-click="activate()"
ng-show="currentView == 'analyzed'">Create Trigger</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ currentView == 'postActivation' ? 'Done' : 'Cancel' }}</button>
<button type="button" class="btn btn-success" ng-click="runTriggerNow()"
ng-if="currentView == 'postActivation'">Run Trigger Now</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ currentView == 'postActivation' ? 'Done' : 'Cancel' }}</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->

View file

@ -15,6 +15,8 @@ angular.module('quay').directive('billingInvoices', function () {
},
controller: function($scope, $element, $sce, ApiService) {
$scope.loading = false;
$scope.showCreateField = null;
$scope.invoiceFields = [];
var update = function() {
var hasValidUser = !!$scope.user;
@ -34,11 +36,59 @@ angular.module('quay').directive('billingInvoices', function () {
$scope.invoices = [];
$scope.loading = false;
});
ApiService.listInvoiceFields($scope.organization).then(function(resp) {
$scope.invoiceFields = resp.fields || [];
}, function() {
$scope.invoiceFields = [];
});
};
$scope.$watch('organization', update);
$scope.$watch('user', update);
$scope.$watch('makevisible', update);
$scope.showCreateField = function() {
$scope.createFieldInfo = {
'title': '',
'value': ''
};
};
$scope.askDeleteField = function(field) {
bootbox.confirm('Are you sure you want to delete field ' + field.title + '?', function(r) {
if (r) {
var params = {
'field_uuid': field.uuid
};
ApiService.deleteInvoiceField($scope.organization, null, params).then(function(resp) {
$scope.invoiceFields = $.grep($scope.invoiceFields, function(current) {
return current.uuid != field.uuid
});
}, ApiService.errorDisplay('Could not delete custom field'));
}
});
};
$scope.createCustomField = function(title, value, callback) {
var data = {
'title': title,
'value': value
};
if (!title || !value) {
callback(false);
bootbox.alert('Missing title or value');
return;
}
ApiService.createInvoiceField($scope.organization, data).then(function(resp) {
$scope.invoiceFields.push(resp);
callback(true);
}, ApiService.errorDisplay('Could not create custom field'));
};
}
};

View file

@ -24,9 +24,11 @@ angular.module('quay').directive('logsView', function () {
$scope.chartVisible = true;
$scope.logsPath = '';
$scope.options = {};
var datetime = new Date();
$scope.logStartDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate() - 7);
$scope.logEndDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate());
$scope.options.logStartDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate() - 7);
$scope.options.logEndDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate());
var defaultPermSuffix = function(metadata) {
if (metadata.activating_username) {
@ -261,9 +263,10 @@ angular.module('quay').directive('logsView', function () {
return;
}
var twoWeeksAgo = getOffsetDate($scope.logEndDate, -14);
if ($scope.logStartDate > $scope.logEndDate || $scope.logStartDate < twoWeeksAgo) {
$scope.logStartDate = twoWeeksAgo;
var twoWeeksAgo = getOffsetDate($scope.options.logEndDate, -14);
if ($scope.options.logStartDate > $scope.options.logEndDate ||
$scope.options.logStartDate < twoWeeksAgo) {
$scope.options.logStartDate = twoWeeksAgo;
}
$scope.loading = true;
@ -282,8 +285,8 @@ angular.module('quay').directive('logsView', function () {
url = UtilService.getRestUrl('superuser', 'logs')
}
url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate));
url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate));
url += '?starttime=' + encodeURIComponent(getDateString($scope.options.logStartDate));
url += '&endtime=' + encodeURIComponent(getDateString($scope.options.logEndDate));
if ($scope.performer) {
url += '&performer=' + encodeURIComponent($scope.performer.name);
@ -300,7 +303,7 @@ angular.module('quay').directive('logsView', function () {
});
}
$scope.chart.draw('bar-chart', resp.logs, $scope.logStartDate, $scope.logEndDate);
$scope.chart.draw('bar-chart', resp.logs, $scope.options.logStartDate, $scope.options.logEndDate);
$scope.kindsAllowed = null;
$scope.logs = resp.logs;
$scope.loading = false;
@ -329,8 +332,9 @@ angular.module('quay').directive('logsView', function () {
$scope.$watch('repository', update);
$scope.$watch('makevisible', update);
$scope.$watch('performer', update);
$scope.$watch('logStartDate', update);
$scope.$watch('logEndDate', update);
$scope.$watch('options.logStartDate', update);
$scope.$watch('options.logEndDate', update);
}
};

View file

@ -12,7 +12,8 @@ angular.module('quay').directive('setupTriggerDialog', function () {
'trigger': '=trigger',
'counter': '=counter',
'canceled': '&canceled',
'activated': '&activated'
'activated': '&activated',
'triggerRunner': '&triggerRunner'
},
controller: function($scope, $element, ApiService, UserService, TriggerService) {
var modalSetup = false;
@ -55,6 +56,11 @@ angular.module('quay').directive('setupTriggerDialog', function () {
$('#setupTriggerModal').modal('hide');
};
$scope.runTriggerNow = function() {
$('#setupTriggerModal').modal('hide');
$scope.triggerRunner({'trigger': $scope.trigger});
};
$scope.checkAnalyze = function(isValid) {
$scope.currentView = 'analyzing';
$scope.pullInfo = {

Binary file not shown.

View file

@ -19,7 +19,6 @@ from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, Reposi
from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot,
RegenerateOrgRobot, RegenerateUserRobot, UserRobotPermissions,
OrgRobotPermissions)
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
BuildTriggerList, BuildTriggerAnalyze, BuildTriggerFieldValues)
@ -33,7 +32,9 @@ from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
from endpoints.api.billing import (UserInvoiceList, UserCard, UserPlan, ListPlans,
OrgnaizationInvoiceList, OrganizationCard, OrganizationPlan)
OrgnaizationInvoiceList, OrganizationCard, OrganizationPlan,
UserInvoiceFieldList, UserInvoiceField,
OrganizationInvoiceFieldList, OrganizationInvoiceField)
from endpoints.api.discovery import DiscoveryResource
from endpoints.api.organization import (OrganizationList, OrganizationMember,
OrgPrivateRepositories, OrgnaizationMemberList,
@ -43,7 +44,6 @@ from endpoints.api.organization import (OrganizationList, OrganizationMember,
from endpoints.api.repository import RepositoryList, RepositoryVisibility, Repository
from endpoints.api.permission import (RepositoryUserPermission, RepositoryTeamPermission,
RepositoryTeamPermissionList, RepositoryUserPermissionList)
from endpoints.api.superuser import (SuperUserLogs, SuperUserList, SuperUserManagement,
SuperUserSendRecoveryEmail, UsageInformation,
SuperUserOrganizationManagement, SuperUserOrganizationList)
@ -4058,6 +4058,104 @@ class TestSuperUserManagement(ApiTestCase):
self._run_test('DELETE', 204, 'devtable', None)
class TestUserInvoiceFieldList(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(UserInvoiceFieldList)
def test_get_anonymous(self):
self._run_test('GET', 401, None, None)
def test_get_freshuser(self):
self._run_test('GET', 404, 'freshuser', None)
def test_get_reader(self):
self._run_test('GET', 404, 'reader', None)
def test_get_devtable(self):
self._run_test('GET', 200, 'devtable', None)
def test_post_anonymous(self):
self._run_test('POST', 401, None, None)
def test_post_freshuser(self):
self._run_test('POST', 404, 'freshuser', dict(title='foo', value='bar'))
def test_post_reader(self):
self._run_test('POST', 404, 'reader', dict(title='foo', value='bar'))
def test_post_devtable(self):
self._run_test('POST', 200, 'devtable', dict(title='foo', value='bar'))
class TestUserInvoiceField(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(UserInvoiceField, field_uuid='1234')
def test_get_anonymous(self):
self._run_test('DELETE', 401, None, None)
def test_get_freshuser(self):
self._run_test('DELETE', 404, 'freshuser', None)
def test_get_reader(self):
self._run_test('DELETE', 404, 'reader', None)
def test_get_devtable(self):
self._run_test('DELETE', 201, 'devtable', None)
class TestOrganizationInvoiceFieldList(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(OrganizationInvoiceFieldList, orgname='buynlarge')
def test_get_anonymous(self):
self._run_test('GET', 403, None, None)
def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', None)
def test_get_reader(self):
self._run_test('GET', 403, 'reader', None)
def test_get_devtable(self):
self._run_test('GET', 200, 'devtable', None)
def test_post_anonymous(self):
self._run_test('POST', 403, None, dict(title='foo', value='bar'))
def test_post_freshuser(self):
self._run_test('POST', 403, 'freshuser', dict(title='foo', value='bar'))
def test_post_reader(self):
self._run_test('POST', 403, 'reader', dict(title='foo', value='bar'))
def test_post_devtable(self):
self._run_test('POST', 200, 'devtable', dict(title='foo', value='bar'))
class TestOrganizationInvoiceField(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(OrganizationInvoiceField, orgname='buynlarge', field_uuid='1234')
def test_get_anonymous(self):
self._run_test('DELETE', 403, None, None)
def test_get_freshuser(self):
self._run_test('DELETE', 403, 'freshuser', None)
def test_get_reader(self):
self._run_test('DELETE', 403, 'reader', None)
def test_get_devtable(self):
self._run_test('DELETE', 201, 'devtable', None)
if __name__ == '__main__':
unittest.main()

View file

@ -25,6 +25,8 @@ def renderInvoiceToPdf(invoice, user):
def renderInvoiceToHtml(invoice, user):
""" Renders a nice HTML display for the given invoice. """
from endpoints.api.billing import get_invoice_fields
def get_price(price):
if not price:
return '$0'
@ -44,7 +46,8 @@ def renderInvoiceToHtml(invoice, user):
'invoice': invoice,
'invoice_date': format_date(invoice.date),
'getPrice': get_price,
'getRange': get_range
'getRange': get_range,
'custom_fields': get_invoice_fields(user)[0],
}
template = env.get_template('invoice.tmpl')

View file

@ -19,6 +19,12 @@
<table>
<tr><td>Date:</td><td>{{ invoice_date }}</td></tr>
<tr><td>Invoice #:</td><td style="font-size: 10px">{{ invoice.id }}</td></tr>
{% for custom_field in custom_fields %}
<tr>
<td>*{{ custom_field['title'] }}:</td>
<td style="font-size: 10px">{{ custom_field['value'] }}</td>
</tr>
{% endfor %}
</table>
</td>
</tr>
@ -38,8 +44,8 @@
<td style="padding: 4px; min-width: 150px;">{{ getPrice(line.amount) }}</td>
</tr>
{%- endfor -%}
<tr>
<td></td>
<td valign="right">
@ -54,7 +60,7 @@
</tr>
</tbody>
</table>
<div style="margin: 6px; padding: 6px; width: 100%; max-width: 640px; border-top: 2px solid #eee; text-align: center; font-size: 14px; -webkit-text-adjust: none; font-weight: bold;">
We thank you for your continued business!
</div>