Skip to content

Commit

Permalink
Allows filtering with PostgreSQL network operators
Browse files Browse the repository at this point in the history
  • Loading branch information
jfinkels committed Mar 16, 2016
1 parent 6fd4671 commit 01b1fac
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ python:

install:
- pip install --upgrade pip
- pip install -r requirements.txt
- pip install -r requirements-test.txt
- pip install coveralls

script:
Expand Down
5 changes: 5 additions & 0 deletions flask_restless/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ def _sub_operator(model, argument, fieldname):
'le': lambda f, a: f <= a,
'lte': lambda f, a: f <= a,
'leq': lambda f, a: f <= a,
'<<': lambda f, a: f.op('<<')(a),
'<<=': lambda f, a: f.op('<<=')(a),
'>>': lambda f, a: f.op('>>')(a),
'>>=': lambda f, a: f.op('>>=')(a),
'<>': lambda f, a: f.op('<>')(a),
'ilike': lambda f, a: f.ilike(a),
'like': lambda f, a: f.like(a),
'not_like': lambda f, a: ~f.like(a),
Expand Down
4 changes: 4 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
-r requirements.txt
nose
savalidation

# For testing PostgreSQL specific operations...
testing.postgresql
psycopg2
31 changes: 25 additions & 6 deletions tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,6 @@ def setup(self):
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['TESTING'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
# This is to avoid a warning in earlier versions of Flask-SQLAlchemy.
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
# This is required by `manager.url_for()` in order to construct
# absolute URLs.
app.config['SERVER_NAME'] = 'localhost'
Expand All @@ -301,9 +298,14 @@ def setup(self):
class DatabaseTestBase(FlaskTestBase):
"""Base class for tests that use a SQLAlchemy database.
The :meth:`setup` method does the necessary SQLAlchemy initialization, and
the subclasses should populate the database with models and then create the
database (by calling ``self.Base.metadata.create_all()``).
The :meth:`setup` method does the necessary SQLAlchemy
initialization, and the subclasses should populate the database with
models and then create the database (by calling
``self.Base.metadata.create_all()``).
By default, this class creates a SQLite database; subclasses can
override the :meth:`.database_uri` method to enable configuration of
an alternate database backend.
"""

Expand All @@ -313,8 +315,14 @@ def setup(self):
"""
super(DatabaseTestBase, self).setup()

# initialize SQLAlchemy
app = self.flaskapp

app.config['SQLALCHEMY_DATABASE_URI'] = self.database_uri()
# This is to avoid a warning in earlier versions of Flask-SQLAlchemy.
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

engine = create_engine(app.config['SQLALCHEMY_DATABASE_URI'],
convert_unicode=True)
self.Session = sessionmaker(autocommit=False, autoflush=False,
Expand All @@ -328,6 +336,17 @@ def teardown(self):
self.session.remove()
self.Base.metadata.drop_all()

def database_uri(self):
"""The database connection URI to use for the SQLAlchemy engine.
By default, this returns the URI for the SQLite in-memory
database. Subclasses that wish to use a different SQL backend
should override this method so that it returns the desired URI
string.
"""
return 'sqlite://'


class ManagerTestBase(DatabaseTestBase):
"""Base class for tests that use a SQLAlchemy database and an
Expand Down
190 changes: 190 additions & 0 deletions tests/test_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
from sqlalchemy import Integer
from sqlalchemy import Time
from sqlalchemy import Unicode
from sqlalchemy.dialects.postgresql import INET
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import backref
from sqlalchemy.orm import relationship
from testing.postgresql import PostgresqlFactory as PGFactory

from .helpers import check_sole_error
from .helpers import dumps
Expand All @@ -33,6 +35,23 @@
from .helpers import ManagerTestBase


#: The PostgreSQL class used to create a temporary database for testing.
#:
#: This class should be instantiated in the setup method of test
#: classes, and the :class:`Postgresql.stop` method should be called on
#: teardown.
#:
#: This is an optimization designed to speed up the tests that require
#: PostgreSQL, since it can be extremely slow to initialize a PostgreSQL
#: database before each test method.
PostgreSQL = PGFactory(cache_initialized_db=True)


def teardown():
"""Clears the cache in the :attr:`PostgreSQL` class."""
PostgreSQL.clear_cache()


class SearchTestBase(ManagerTestBase):
"""Provides a search method that simplifies a fetch request with filtering
query parameters.
Expand Down Expand Up @@ -892,7 +911,178 @@ def test_compare_equals_to_null(self):
# TODO check the error message here.


class TestNetworkOperators(SearchTestBase):
"""Unit tests for the network address operators in PostgreSQL.
For more information, see `Network Address Functions and Operators`_
in the PostgreSQL documentation.
.. _Network Address Functions and Operators: http://www.postgresql.org/docs/current/interactive/functions-net.html
"""

def setup(self):
super(TestNetworkOperators, self).setup()

class Network(self.Base):
__tablename__ = 'network'
id = Column(Integer, primary_key=True)
address = Column(INET)

self.Network = Network
self.Base.metadata.create_all()
self.manager.create_api(Network)

def teardown(self):
"""Closes the database and removes the temporary directory in
which it lives.
"""
super(TestNetworkOperators, self).teardown()
self.database.stop()

# We know this method will be called by `setup()` in the superclass,
# so we can set up the temporary database here.
def database_uri(self):
"""Creates a PostgreSQL database and returns its connection URI."""
#: The PostgreSQL database used by the test methods in this class.
#:
#: This attribute stores a
#: :class:`~testing.postgresql.Postgresql` object, which must be
#: stopped in the :meth:`.teardown` method.
self.database = PostgreSQL()

return self.database.url()

def test_is_not_equal(self):
"""Test for the ``<>`` ("is not equal") operator.
For example:
.. sourcecode:: postgresql
inet '192.168.1.5' <> inet '192.168.1.4'
"""
network1 = self.Network(id=1, address='192.168.1.5')
network2 = self.Network(id=2, address='192.168.1.4')
self.session.add_all([network1, network2])
self.session.commit()
filters = [dict(name='address', op='>>=', val='192.168.1.5')]
response = self.search('/api/network', filters)
document = loads(response.data)
networks = document['data']
assert ['1'] == sorted(network['id'] for network in networks)

def test_is_contained_by(self):
"""Test for the ``<<`` ("is contained by") operator.
For example:
.. sourcecode:: postgresql
inet '192.168.1.5' << inet '192.168.1/24'
"""
network1 = self.Network(id=1, address='192.168.1.5')
network2 = self.Network(id=2, address='192.168.2.1')
self.session.add_all([network1, network2])
self.session.commit()
filters = [dict(name='address', op='<<', val='192.168.1/24')]
response = self.search('/api/network', filters)
document = loads(response.data)
networks = document['data']
assert ['1'] == sorted(network['id'] for network in networks)

def test_is_contained_by_or_equals(self):
"""Test for the ``<<=`` ("is contained by or equals") operator.
For example:
.. sourcecode:: postgresql
inet '192.168.1/24' <<= inet '192.168.1/24'
"""
network1 = self.Network(id=1, address='192.168.1/24')
network2 = self.Network(id=2, address='192.168.1.5')
network3 = self.Network(id=3, address='192.168.2.1')
self.session.add_all([network1, network2, network3])
self.session.commit()
filters = [dict(name='address', op='<<=', val='192.168.1/24')]
response = self.search('/api/network', filters)
document = loads(response.data)
networks = document['data']
assert ['1', '2'] == sorted(network['id'] for network in networks)

def test_contains(self):
"""Test for the ``>>`` ("contains") operator.
For example:
.. sourcecode:: postgresql
inet '192.168.1/24' >> inet '192.168.1.5'
"""
network1 = self.Network(id=1, address='192.168.1/24')
network2 = self.Network(id=2, address='192.168.2/24')
self.session.add_all([network1, network2])
self.session.commit()
filters = [dict(name='address', op='>>', val='192.168.1.5')]
response = self.search('/api/network', filters)
document = loads(response.data)
networks = document['data']
assert ['1'] == sorted(network['id'] for network in networks)

def test_contains_or_equals(self):
"""Test for the ``>>=`` ("contains or equals") operator.
For example:
.. sourcecode:: postgresql
inet '192.168.1/24' >>= inet '192.168.1/24'
"""
network1 = self.Network(id=1, address='192.168.1/24')
network2 = self.Network(id=2, address='192.168/16')
network3 = self.Network(id=3, address='192.168.2/24')
self.session.add_all([network1, network2, network3])
self.session.commit()
filters = [dict(name='address', op='>>=', val='192.168.1/24')]
response = self.search('/api/network', filters)
document = loads(response.data)
networks = document['data']
assert ['1', '2'] == sorted(network['id'] for network in networks)

def test_contains_or_is_contained_by(self):
"""Test for the ``&&`` ("contains or is contained by") operator.
For example:
.. sourcecode:: postgresql
inet '192.168.1/24' && inet '192.168.1.80/28'
"""
# network1 contains the queried subnet
network1 = self.Network(id=1, address='192.168.1/24')
# network2 is contained by the queried subnet
network2 = self.Network(id=2, address='192.168.1.81/28')
# network3 is neither
network3 = self.Network(id=3, address='192.168.2.1')
self.session.add_all([network1, network2, network3])
self.session.commit()
filters = [dict(name='address', op='>>=', val='192.168.1.80/28')]
response = self.search('/api/network', filters)
document = loads(response.data)
networks = document['data']
assert ['1', '2'] == sorted(network['id'] for network in networks)


class TestAssociationProxy(SearchTestBase):

"""Test for filtering on association proxies."""

def setup(self):
Expand Down

0 comments on commit 01b1fac

Please sign in to comment.