Proposal of adding sort parameter to list notifications

I'm planning to change pagination style to Horizon standard in monasca-ui.
Horizon standard pagination style will need fewer code than current
implementation and will be easier to maintain.
But this change will need sort parameter to api. Alarm Definitions and
Alarms have already sort_by parameter. So I added sort_by parameter
to Notifications.

Change-Id: Ic7dd98c027476ad19174f52ddd2d4ce6523c50cc
This commit is contained in:
Shinya Kawabata 2016-03-15 20:01:07 +09:00
parent 9e6598c079
commit f10bddc7d9
17 changed files with 212 additions and 29 deletions

View File

@ -1427,6 +1427,8 @@ None.
#### Query Parameters
* offset (string, optional)
* limit (integer, optional)
* sort_by (string, optional) - Comma separated list of fields to sort by, defaults to 'id'. Fields may be followed by 'asc' or 'desc' to set the direction, ex 'address desc'
Allowed fields for sort_by are: 'id', 'name', 'type', 'address', 'created_at', 'updated_at'
#### Request Body
None.

View File

@ -46,5 +46,5 @@ public interface NotificationMethodRepo {
NotificationMethod update(String tenantId, String notificationMethodId, String name,
NotificationMethodType type, String address);
List<NotificationMethod> find(String tenantId, String offset, int limit);
List<NotificationMethod> find(String tenantId, List<String> sortBy, String offset, int limit);
}

View File

@ -34,6 +34,7 @@ import monasca.api.domain.exception.EntityNotFoundException;
import monasca.api.domain.model.notificationmethod.NotificationMethod;
import monasca.api.domain.model.notificationmethod.NotificationMethodRepo;
import monasca.api.domain.model.notificationmethod.NotificationMethodType;
import monasca.api.resource.exception.Exceptions;
import monasca.common.hibernate.db.NotificationMethodDb;
import monasca.common.model.alarm.AlarmNotificationMethodType;
@ -198,7 +199,13 @@ public class NotificationMethodSqlRepoImpl
@Override
@SuppressWarnings("unchecked")
public List<NotificationMethod> find(String tenantId, String offset, int limit) {
public List<NotificationMethod> find(String tenantId, List<String> sortBy, String offset,
int limit) {
if (sortBy != null && !sortBy.isEmpty()) {
throw Exceptions.unprocessableEntity(
"Sort_by is not implemented for the hibernate database type");
}
Session session = null;
List<NotificationMethodDb> resultList;
List<NotificationMethod> notificationList = Lists.newArrayList();

View File

@ -26,6 +26,8 @@ import org.skife.jdbi.v2.Query;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Joiner;
import monasca.api.domain.exception.EntityExistsException;
import monasca.api.domain.exception.EntityNotFoundException;
import monasca.api.domain.model.notificationmethod.NotificationMethod;
@ -40,6 +42,7 @@ import monasca.common.persistence.BeanMapper;
public class NotificationMethodMySqlRepoImpl implements NotificationMethodRepo {
private static final Logger LOG = LoggerFactory
.getLogger(NotificationMethodMySqlRepoImpl.class);
private static final Joiner COMMA_JOINER = Joiner.on(',');
private final DBI db;
private final PersistUtils persistUtils;
@ -103,26 +106,37 @@ public class NotificationMethodMySqlRepoImpl implements NotificationMethodRepo {
}
@Override
public List<NotificationMethod> find(String tenantId, String offset, int limit) {
public List<NotificationMethod> find(String tenantId, List<String> sortBy, String offset,
int limit) {
try (Handle h = db.open()) {
String rawQuery =
" SELECT nm.id, nm.tenant_id, nm.name, nm.type, nm.address, nm.created_at, nm.updated_at "
+ "FROM notification_method as nm "
+ "WHERE tenant_id = :tenantId %1$s order by nm.id asc %2$s";
+ "WHERE tenant_id = :tenantId %1$s %2$s %3$s";
String offsetPart = "";
if (offset != null) {
offsetPart = "and nm.id > :offset";
}
String orderByPart = "";
if (sortBy != null && !sortBy.isEmpty()) {
orderByPart = " order by " + COMMA_JOINER.join(sortBy);
if (!orderByPart.contains("id")) {
orderByPart = orderByPart + ",id";
}
} else {
orderByPart = " order by id ";
}
String limitPart = "";
if (limit > 0) {
limitPart = " limit :limit";
}
String query = String.format(rawQuery, offsetPart, limitPart);
String query = String.format(rawQuery, offsetPart, orderByPart, limitPart);
Query<?> q = h.createQuery(query);

View File

@ -17,6 +17,7 @@ import com.codahale.metrics.annotation.Timed;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
@ -37,6 +38,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import monasca.api.app.command.CreateNotificationMethodCommand;
import monasca.api.app.validation.Validation;
import monasca.api.domain.model.notificationmethod.NotificationMethod;
import monasca.api.domain.model.notificationmethod.NotificationMethodRepo;
import monasca.api.infrastructure.persistence.PersistUtils;
@ -48,6 +50,9 @@ import monasca.api.infrastructure.persistence.PersistUtils;
public class NotificationMethodResource {
private final NotificationMethodRepo repo;
private final PersistUtils persistUtils;
private final static List<String> ALLOWED_SORT_BY = Arrays.asList("id", "name", "type",
"address", "updated_at",
"created_at");
@Inject
public NotificationMethodResource(NotificationMethodRepo repo, PersistUtils persistUtils) {
@ -74,11 +79,15 @@ public class NotificationMethodResource {
@Timed
@Produces(MediaType.APPLICATION_JSON)
public Object list(@Context UriInfo uriInfo, @HeaderParam("X-Tenant-Id") String tenantId,
@QueryParam("sort_by") String sortByStr,
@QueryParam("offset") String offset,
@QueryParam("limit") String limit) throws UnsupportedEncodingException {
List<String> sortByList = Validation.parseAndValidateSortBy(sortByStr, ALLOWED_SORT_BY);
final int paging_limit = this.persistUtils.getLimit(limit);
final List<NotificationMethod> resources = repo.find(tenantId, offset, paging_limit);
final List<NotificationMethod> resources = repo.find(tenantId, sortByList, offset,
paging_limit);
return Links.paginate(paging_limit,
Links.hydrate(resources, uriInfo),
uriInfo);

View File

@ -23,6 +23,8 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.ws.rs.WebApplicationException;
import monasca.api.domain.exception.EntityExistsException;
import monasca.api.domain.exception.EntityNotFoundException;
import monasca.api.domain.model.notificationmethod.NotificationMethod;
@ -103,12 +105,12 @@ public class NotificationMethodSqlRepositoryImplTest {
@Test(groups = "orm")
public void shouldFind() {
List<NotificationMethod> nms1 = repo.find("444", null, 1);
List<NotificationMethod> nms1 = repo.find("444", null, null, 1);
assertEquals(nms1, Arrays.asList(new NotificationMethod("123", "MyEmail", NotificationMethodType.EMAIL, "a@b"), new NotificationMethod("124",
"OtherEmail", NotificationMethodType.EMAIL, "a@b")));
List<NotificationMethod> nms2 = repo.find("444", "123", 1);
List<NotificationMethod> nms2 = repo.find("444", null, "123", 1);
assertEquals(nms2, Collections.singletonList(new NotificationMethod("124", "OtherEmail", NotificationMethodType.EMAIL, "a@b")));
}
@ -153,4 +155,9 @@ public class NotificationMethodSqlRepositoryImplTest {
repo.update("444", "124", "MyEmail", NotificationMethodType.EMAIL, "abc");
}
@Test(groups = "orm", expectedExceptions = WebApplicationException.class)
public void shouldFindThrowException() {
repo.find("444", Arrays.asList("name", "address"), null, 1);
}
}

View File

@ -89,7 +89,7 @@ public class NotificationMethodMySqlRepositoryImplTest {
}
public void shouldFind() {
List<NotificationMethod> nms = repo.find("444", null, 1);
List<NotificationMethod> nms = repo.find("444", null, null, 1);
assertEquals(nms, Arrays.asList(new NotificationMethod("123", "MyEmail",
NotificationMethodType.EMAIL, "a@b"),new NotificationMethod("124", "OtherEmail",

View File

@ -61,7 +61,8 @@ public class NotificationMethodResourceTest extends AbstractMonApiResourceTest {
when(repo.create(eq("abc"), eq("MyPd"), eq(NotificationMethodType.PAGERDUTY), anyString()))
.thenReturn(notificationMethodPagerduty);
when(repo.findById(eq("abc"), eq("123"))).thenReturn(notificationMethod);
when(repo.find(eq("abc"), anyString(), anyInt())).thenReturn(Arrays.asList(notificationMethod));
when(repo.find(eq("abc"), (List<String>) anyList(), anyString(), anyInt()))
.thenReturn(Arrays.asList(notificationMethod));
addResources(new NotificationMethodResource(repo, new PersistUtils()));
}
@ -216,7 +217,7 @@ public class NotificationMethodResourceTest extends AbstractMonApiResourceTest {
List<NotificationMethod> notificationMethods = Arrays.asList(nm);
assertEquals(notificationMethods, Arrays.asList(notificationMethod));
verify(repo).find(eq("abc"), anyString(), anyInt());
verify(repo).find(eq("abc"), (List<String>) anyList(), anyString(), anyInt());
}
public void shouldGet() {
@ -258,7 +259,8 @@ public class NotificationMethodResourceTest extends AbstractMonApiResourceTest {
}
public void should500OnInternalException() {
doThrow(new RuntimeException("")).when(repo).find(anyString(), anyString(), anyInt());
doThrow(new RuntimeException("")).when(repo).find(anyString(), (List<String>) anyList(),
anyString(), anyInt());
try {
client().resource("/v2.0/notification-methods").header("X-Tenant-Id", "abc")

View File

@ -14,13 +14,13 @@
import datetime
from monasca_common.repositories.mysql import mysql_repository
from oslo_log import log
from oslo_utils import uuidutils
from monasca_api.common.repositories import alarm_definitions_repository as adr
from monasca_api.common.repositories import exceptions
from monasca_api.common.repositories.model import sub_alarm_definition
from monasca_common.repositories.mysql import mysql_repository
LOG = log.getLogger(__name__)

View File

@ -15,11 +15,11 @@
from datetime import datetime
from time import time
from monasca_common.repositories.mysql import mysql_repository
from oslo_log import log
from monasca_api.common.repositories import alarms_repository
from monasca_api.common.repositories import exceptions
from monasca_common.repositories.mysql import mysql_repository
LOG = log.getLogger(__name__)

View File

@ -14,12 +14,12 @@
import datetime
from monasca_common.repositories.mysql import mysql_repository
from oslo_log import log
from oslo_utils import uuidutils
from monasca_api.common.repositories import exceptions
from monasca_api.common.repositories import notifications_repository as nr
from monasca_common.repositories.mysql import mysql_repository
LOG = log.getLogger(__name__)
@ -75,7 +75,7 @@ class NotificationsRepository(mysql_repository.MySQLRepository,
return notification_id
@mysql_repository.mysql_try_catch_block
def list_notifications(self, tenant_id, offset, limit):
def list_notifications(self, tenant_id, sort_by, offset, limit):
query = """
select *
@ -88,7 +88,16 @@ class NotificationsRepository(mysql_repository.MySQLRepository,
query += " and id > %s "
parms.append(offset.encode('utf8'))
query += " order by id limit %s "
if sort_by:
query += " order by " + ','.join(sort_by)
if 'id' not in sort_by:
query += ",id "
else:
query += " "
else:
query += " order by id "
query += " limit %s "
parms.append(limit + 1)
rows = self._execute_query(query, parms)

View File

@ -27,7 +27,7 @@ class NotificationsRepository(object):
return
@abc.abstractmethod
def list_notifications(self, tenant_id, offset, limit):
def list_notifications(self, tenant_id, sort_by, offset, limit):
return
@abc.abstractmethod

View File

@ -23,7 +23,7 @@ from monasca_api.common.repositories import notifications_repository as nr
from monasca_api.common.repositories.sqla import models
from monasca_api.common.repositories.sqla import sql_repository
from sqlalchemy import MetaData, update, insert, delete
from sqlalchemy import select, bindparam, func, and_
from sqlalchemy import select, bindparam, func, and_, literal_column
LOG = log.getLogger(__name__)
@ -112,7 +112,7 @@ class NotificationsRepository(sql_repository.SQLRepository,
return notification_id
@sql_repository.sql_try_catch_block
def list_notifications(self, tenant_id, offset, limit):
def list_notifications(self, tenant_id, sort_by, offset, limit):
rows = []
@ -124,12 +124,21 @@ class NotificationsRepository(sql_repository.SQLRepository,
parms = {'b_tenant_id': tenant_id}
if sort_by is not None:
order_columns = [literal_column(col) for col in sort_by]
if 'id' not in sort_by:
order_columns.append(nm.c.id)
else:
order_columns = [nm.c.id]
if offset:
select_nm_query = (select_nm_query
.where(nm.c.id > bindparam('b_offset')))
parms['b_offset'] = offset.encode('utf8')
select_nm_query = select_nm_query.order_by(*order_columns)
select_nm_query = (select_nm_query
.order_by(nm.c.id)
.limit(bindparam('b_limit')))

View File

@ -152,9 +152,9 @@ class TestNotificationMethodRepoDB(testtools.TestCase, fixtures.TestWithFixtures
self.assertEqual(nm['address'], 'a@b')
def test_should_find(self):
nms = self.repo.list_notifications('444', None, 1)
nms = self.repo.list_notifications('444', None, None, 1)
self.assertEqual(nms, self.default_nms)
nms = self.repo.list_notifications('444', 'z', 1)
nms = self.repo.list_notifications('444', None, 'z', 1)
self.assertEqual(nms, [])
def test_update(self):

View File

@ -12,9 +12,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import six.moves.urllib.parse as urlparse
from oslo_log import log
import six.moves.urllib.parse as urlparse
from validate_email import validate_email
import voluptuous

View File

@ -22,6 +22,7 @@ from monasca_api.common.repositories import exceptions
from monasca_api.v2.common.schemas import (
notifications_request_body_schema as schemas_notifications)
from monasca_api.v2.common.schemas import exceptions as schemas_exceptions
from monasca_api.v2.common import validation
from monasca_api.v2.reference import helpers
from monasca_api.v2.reference import resource
@ -121,10 +122,10 @@ class Notifications(notifications_api_v2.NotificationsV2API):
return helpers.add_links_to_resource(response, uri)
@resource.resource_try_catch_block
def _list_notifications(self, tenant_id, uri, offset, limit):
def _list_notifications(self, tenant_id, uri, sort_by, offset, limit):
rows = self._notifications_repo.list_notifications(tenant_id, offset,
limit)
rows = self._notifications_repo.list_notifications(tenant_id, sort_by,
offset, limit)
result = [self._build_notification_result(row,
uri) for row in rows]
@ -173,10 +174,20 @@ class Notifications(notifications_api_v2.NotificationsV2API):
if notification_method_id is None:
helpers.validate_authorization(req, self._default_authorized_roles)
tenant_id = helpers.get_tenant_id(req)
sort_by = helpers.get_query_param(req, 'sort_by', default_val=None)
if sort_by is not None:
if isinstance(sort_by, basestring):
sort_by = sort_by.split(',')
allowed_sort_by = {'id', 'name', 'type', 'address',
'updated_at', 'created_at'}
validation.validate_sort_by(sort_by, allowed_sort_by)
offset = helpers.get_query_param(req, 'offset')
limit = helpers.get_limit(req)
result = self._list_notifications(tenant_id, req.uri, offset,
limit)
result = self._list_notifications(tenant_id, req.uri, sort_by,
offset, limit)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
else:

View File

@ -14,6 +14,8 @@
import time
import six.moves.urllib.parse as urlparse
from monasca_tempest_tests.tests.api import base
from monasca_tempest_tests.tests.api import constants
from monasca_tempest_tests.tests.api import helpers
@ -122,6 +124,118 @@ class TestNotificationMethods(base.BaseMonascaTest):
delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
def test_list_notification_methods_sort_by(self):
notifications = [helpers.create_notification(
name='notification sort by 01',
type='PAGERDUTY',
address='test03@localhost',
), helpers.create_notification(
name='notification sort by 02',
type='WEBHOOK',
address='http://localhost/test01',
), helpers.create_notification(
name='notification sort by 03',
type='EMAIL',
address='test02@localhost',
)]
for notification in notifications:
resp, response_body = self.monasca_client.create_notifications(notification)
notification['id'] = response_body['id']
time.sleep(1)
sort_params1 = ['id', 'name', 'type', 'address']
for sort_by in sort_params1:
notif_sorted_by = sorted(notifications, key=lambda
notification: notification[sort_by])
resp, response_body = self.monasca_client.list_notification_methods(
'?sort_by=' + sort_by)
self.assertEqual(200, resp.status)
for i, element in enumerate(response_body['elements']):
self.assertEqual(notif_sorted_by[i][sort_by], element[sort_by])
resp, response_body = self.monasca_client.list_notification_methods(
'?sort_by=' + sort_by + urlparse.quote(' asc'))
self.assertEqual(200, resp.status)
for i, element in enumerate(response_body['elements']):
self.assertEqual(notif_sorted_by[i][sort_by], element[sort_by])
notif_sorted_by_reverse = sorted(notifications, key=lambda
notification: notification[sort_by], reverse=True)
resp, response_body = self.monasca_client.list_notification_methods(
'?sort_by=' + sort_by + urlparse.quote(' desc'))
self.assertEqual(200, resp.status)
for i, element in enumerate(response_body['elements']):
self.assertEqual(notif_sorted_by_reverse[i][sort_by], element[sort_by])
sort_params2 = ['created_at', 'updated_at']
for sort_by in sort_params2:
resp, response_body = self.monasca_client.list_notification_methods(
'?sort_by=' + sort_by)
self.assertEqual(200, resp.status)
for i, element in enumerate(response_body['elements']):
self.assertEqual(notifications[i]['id'], element['id'])
resp, response_body = self.monasca_client.list_notification_methods(
'?sort_by=' + sort_by + urlparse.quote(' asc'))
self.assertEqual(200, resp.status)
for i, element in enumerate(response_body['elements']):
self.assertEqual(notifications[i]['id'], element['id'])
resp, response_body = self.monasca_client.list_notification_methods(
'?sort_by=' + sort_by + urlparse.quote(' desc'))
self.assertEqual(200, resp.status)
for i, element in enumerate(response_body['elements']):
self.assertEqual(notifications[-i-1]['id'], element['id'])
for notification in notifications:
self.monasca_client.delete_notification_method(notification['id'])
@test.attr(type="gate")
def test_list_notification_methods_multiple_sort_by(self):
notifications = [helpers.create_notification(
name='notification sort by 01',
type='EMAIL',
address='test02@localhost',
), helpers.create_notification(
name='notification sort by 02',
type='PAGERDUTY',
address='test03@localhost',
), helpers.create_notification(
name='notification sort by 03',
type='EMAIL',
address='test04@localhost',
), helpers.create_notification(
name='notification sort by 04',
type='EMAIL',
address='test01@localhost',
)]
for notification in notifications:
resp, response_body = self.monasca_client.create_notifications(notification)
notification['id'] = response_body['id']
resp, response_body = self.monasca_client.list_notification_methods(
'?sort_by=' + urlparse.quote('type asc,address desc,id'))
self.assertEqual(200, resp.status)
expected_order = [2, 0, 3, 1]
for i, element in enumerate(response_body['elements']):
self.assertEqual(notifications[expected_order[i]]['id'], element['id'])
for element in response_body['elements']:
self.monasca_client.delete_notification_method(element['id'])
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_list_notification_methods_invalid_sort_by(self):
query_parms = '?sort_by=random'
self.assertRaises(exceptions.UnprocessableEntity,
self.monasca_client.list_notification_methods,
query_parms)
@test.attr(type="gate")
def test_list_notification_methods_with_offset_limit(self):
name1 = data_utils.rand_name('notification')