diff --git a/docs/api.rst b/docs/api.rst index c7b5166..8b559dc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -488,6 +488,77 @@ stacky/report/ ... ] +stacky/reports/search/ +========================= + +.. http:get:: http://example.com/stacky/reports/search + + Returns reports that match the search criteria in descending order of id. + + The contents of the report varies by the specific report, but + all are in row/column format with Row 0 being a special *metadata* row. + The actual row/columns of the report start at Row 1 onwards. + + **Example request**: + + .. sourcecode:: http + + GET /stacky/reports/search/ HTTP/1.1 + Host: example.com + Accept: application/json + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/json + + [ + [ + "Id", + "Start", + "End", + "Created", + "Name", + "Version" + ], + + [ + 4253, + "2013-11-21 00:00:00", + "2013-11-22 00:00:00", + "2013-11-22 01:44:55", + "public outbound bandwidth", + 1 + ], + [ + 4252, + "2014-01-18 00:00:00", + "2013-11-22 00:00:00", + "2013-11-22 01:44:55", + "image events audit", + 1 + ], + [ + 4248, + "2013-11-21 00:00:00", + "2013-11-22 00:00:00", + "2013-11-22 01:44:55", + "Error detail report", + 1 + ], + + ... + ] + + :query id: integer report id + :query name: string report name(can include spaces) + :query period_start: start of period, which the report pertains to, in the following format: YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] + :query period_end: end of period, which the report pertains to, in the following format: YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] + :query created: the day, when the report was created, in the following format: YYYY-MM-DD + stacky/show/ ====================== diff --git a/stacktach/stacky_server.py b/stacktach/stacky_server.py index d87bf3c..f5690d0 100644 --- a/stacktach/stacky_server.py +++ b/stacktach/stacky_server.py @@ -628,53 +628,67 @@ class BadRequestException(Exception): pass -def _parse_created(request_filters): +def _parse_created(created): try: - created_datetime = datetime.datetime.strptime( - request_filters['created'], '%Y-%m-%d %H:%M:%S') + created_datetime = datetime.datetime.strptime(created, '%Y-%m-%d') return dt.dt_to_decimal(created_datetime) except ValueError: raise BadRequestException( - "'%s' value has an invalid format. It must be in " - "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format." % - request_filters['created']) + "'%s' value has an invalid format. It must be in YYYY-MM-DD format." + % created) -def _create_query_filters_from_request(request_filters, model): - allowed_fields = [field.name for field in models.get_model_fields(model)] - invalid_fields = [] +def _parse_id(id): + try: + return int(id) + except ValueError: + raise BadRequestException( + "'%s' value has an invalid format. It must be in integer " + "format." % id) + + +def _parse_fields_and_create_query_filters(request_filters): query_filters = {} for field, value in request_filters.iteritems(): - if field in allowed_fields: - query_filters[field + '__exact'] = value + if field == 'created': + decimal_created = _parse_created(value) + query_filters['created__gt'] = decimal_created + query_filters['created__lt'] = decimal_created + SECS_PER_DAY + elif field == 'id': + id = _parse_id(value) + query_filters['id__exact'] = id else: - invalid_fields.append(field) - - if invalid_fields: - raise BadRequestException( - "The requested fields do not exist for the corresponding " - "object: %s. Note: The field names of database " - "are case-sensitive." % - ', '.join(sorted(invalid_fields))) + query_filters[field + '__exact'] = value return query_filters -def _get_query_filters(request, model): +def _check_if_fields_searchable(request_filters): + allowed_fields = ['id', 'name', 'created', 'period_start', 'period_end'] + invalid_fields = [field for field in request_filters.keys() + if field not in allowed_fields] + if invalid_fields: + raise BadRequestException( + "The requested fields either do not exist for the corresponding " + "object or are not searchable: %s. Note: The field names of " + "database are case-sensitive." % + ', '.join(sorted(invalid_fields))) + + +def _create_query_filters(request): request_filters = deepcopy(request.GET) - if 'created' in request_filters: - request_filters['created'] = _parse_created(request_filters) request_filters.pop('limit', None) request_filters.pop('offset', None) - return _create_query_filters_from_request(request_filters, model) + _check_if_fields_searchable(request_filters) + return _parse_fields_and_create_query_filters(request_filters) def do_jsonreports_search(request): try: model = models.JsonReport - filters = _get_query_filters(request, model) + filters = _create_query_filters(request) reports = model_search(request, model.objects, filters, order_by='-id') results = [['Id', 'Start', 'End', 'Created', 'Name', 'Version']] diff --git a/tests/unit/test_stacky_server.py b/tests/unit/test_stacky_server.py index 23aea92..8f5d781 100644 --- a/tests/unit/test_stacky_server.py +++ b/tests/unit/test_stacky_server.py @@ -1528,25 +1528,22 @@ class JsonReportsSearchAPI(StacktachBaseTestCase): def tearDown(self): self.mox.UnsetStubs() - def test_jsonreports_search_order_by_period_start(self): + def test_jsonreports_search_order_by_id(self): request = self.mox.CreateMockAnything() request.GET = { 'id': 1, 'name': 'nova_usage_audit', 'period_start': '2014-01-01 00:00:00', 'period_end': '2014-01-02 00:00:00', - 'created': '2014-01-01 09:40:00', - 'version': 4, - 'json': 'json' + 'created': '2014-01-01', } filters = { 'id__exact': 1, 'period_start__exact': '2014-01-01 00:00:00', 'name__exact': 'nova_usage_audit', 'period_end__exact': '2014-01-02 00:00:00', - 'created__exact': decimal.Decimal('1388569200'), - 'version__exact': 4, - 'json__exact': 'json' + 'created__lt': decimal.Decimal('1388620800'), + 'created__gt': decimal.Decimal('1388534400'), } self.mox.StubOutWithMock(stacky_server, 'model_search') stacky_server.model_search(request, self.model, filters, @@ -1555,10 +1552,11 @@ class JsonReportsSearchAPI(StacktachBaseTestCase): self.mox.ReplayAll() actual_result = stacky_server.do_jsonreports_search(request).content - expected_result = \ - [['Id', 'Start', 'End', 'Created', 'Name', 'Version'], - ['5975', '2014-01-18 00:00:00', '2014-01-19 00:00:00', - '2014-01-01 09:40:00', 'nova usage audit', 4]] + expected_result = [ + ['Id', 'Start', 'End', 'Created', 'Name', 'Version'], + ['5975', '2014-01-18 00:00:00', '2014-01-19 00:00:00', + '2014-01-01 09:40:00', 'nova usage audit', 4] + ] self.assertEquals(ast.literal_eval(actual_result), expected_result) self.mox.VerifyAll() @@ -1590,32 +1588,33 @@ class JsonReportsSearchAPI(StacktachBaseTestCase): self.assertEquals(ast.literal_eval(actual_result), expected_result) self.mox.VerifyAll() - def test_jsonreports_search_with_invalid_field_names_400(self): + def test_jsonreports_search_with_invalid_fields(self): request = self.mox.CreateMockAnything() request.GET = {'invalid_column_1': 'value_1', 'invalid_column_2': 'value_2', + 'version': 4, + 'json': 'json', 'period_start': '2014-01-01 00:00:00'} self.mox.ReplayAll() actual_result = stacky_server.do_jsonreports_search(request).content - expected_result = \ - [ + expected_result = [ ["Error", "Message"], - ["Bad Request", "The requested fields do not exist for the " - "corresponding object: invalid_column_1, invalid_column_2. Note: " - "The field names of database are case-sensitive."] + ["Bad Request", "The requested fields either do not exist for the " + "corresponding object or are not searchable: invalid_column_1, " + "invalid_column_2, json, version. Note: The field names of " + "database are case-sensitive."] ] self.assertEqual(ast.literal_eval(actual_result), expected_result) self.mox.VerifyAll() - def test_jsonreports_search_with_invalid_format_of_field_values_400(self): + def test_jsonreports_search_with_invalid_period_start(self): request = self.mox.CreateMockAnything() request.GET = {'period_start': '1234'} self.mox.ReplayAll() actual_result = stacky_server.do_jsonreports_search(request).content - expected_result = \ - [ + expected_result = [ ["Error", "Message"], ["Bad Request", "'1234' value has an invalid format. It must be in " "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."] @@ -1623,25 +1622,49 @@ class JsonReportsSearchAPI(StacktachBaseTestCase): self.assertEqual(ast.literal_eval(actual_result), expected_result) self.mox.VerifyAll() - def test_jsonreports_search_by_created(self): + def test_jsonreports_search_with_invalid_period_end(self): request = self.mox.CreateMockAnything() - request.GET = { - 'created': '2014-01-01 09:40:20'} - filters = { - 'created__exact': 1388569220} - self.mox.StubOutWithMock(stacky_server, 'model_search') - stacky_server.model_search(request, self.model, filters, - order_by='-id').AndReturn( - [self.model_search_result]) + request.GET = {'period_end': '1234'} self.mox.ReplayAll() actual_result = stacky_server.do_jsonreports_search(request).content - expected_result = \ - [['Id', 'Start', 'End', 'Created', 'Name', 'Version'], - ['5975', '2014-01-18 00:00:00', '2014-01-19 00:00:00', - '2014-01-01 09:40:00', 'nova usage audit', 4]] + expected_result = [ + ["Error", "Message"], + ["Bad Request", "'1234' value has an invalid format. It must be in " + "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."] + ] + self.assertEqual(ast.literal_eval(actual_result), expected_result) + self.mox.VerifyAll() - self.assertEquals(ast.literal_eval(actual_result), expected_result) + def test_jsonreports_search_with_invalid_id(self): + request = self.mox.CreateMockAnything() + request.GET = {'id': 'abcd'} + self.mox.ReplayAll() + + actual_result = stacky_server.do_jsonreports_search(request).content + expected_result = [ + ["Error", "Message"], + ["Bad Request", "'abcd' value has an invalid format. It must be in " + "integer format."] + ] + self.assertEqual(ast.literal_eval(actual_result), expected_result) + self.mox.VerifyAll() + + def test_jsonreports_search_with_invalid_created_format(self): + request = self.mox.CreateMockAnything() + request.GET = { + 'created': '2014-01-01 00:00:00' + } + self.mox.ReplayAll() + + actual_result = stacky_server.do_jsonreports_search(request).content + expected_result = [ + ["Error", "Message"], + ["Bad Request", "'2014-01-01 00:00:00' value has an invalid format." + " It must be in YYYY-MM-DD format."] + ] + + self.assertEqual(ast.literal_eval(actual_result), expected_result) self.mox.VerifyAll() def test_jsonreports_search_by_invalid_created_400(self): @@ -1655,7 +1678,7 @@ class JsonReportsSearchAPI(StacktachBaseTestCase): [ ["Error", "Message"], ["Bad Request", "'1234' value has an invalid format. It must be in " - "YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] format."] + "YYYY-MM-DD format."] ] self.assertEquals(ast.literal_eval(actual_result), expected_result) self.mox.VerifyAll()