From 233edb8facff0684e5b47754fd9968112d712c6e Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 23 May 2016 14:52:08 -0400 Subject: [PATCH 01/18] Add a postGIS point validator. --- app/forms/validators.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/forms/validators.py b/app/forms/validators.py index 9a1889d..005539c 100644 --- a/app/forms/validators.py +++ b/app/forms/validators.py @@ -4,3 +4,7 @@ import wtforms as wtf # A zip code validator. zip_ = wtf.validators.Regexp(r'^\d{5}(-\d{4})?$') + + +# A postGIS point validator. For example, POINT(-70 40). +point = wtf.validators.Regexp(r'POINT\(\-?\d{1,3}(\.\d+)? \-?\d{1,3}(\.\d+)?\)') -- GitLab From 91762b8fe47cb5407bbd7d2c85acac55c3e790d6 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 23 May 2016 14:52:18 -0400 Subject: [PATCH 02/18] Add a location form. --- app/forms/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/forms/base.py b/app/forms/base.py index 1be257d..8118e93 100644 --- a/app/forms/base.py +++ b/app/forms/base.py @@ -22,3 +22,8 @@ class AddressForm(object): country = wtf.StringField( validators=[wtf.validators.AnyOf(geography.countries)]) postal_code = wtf.StringField(validators=[validators.zip_]) + + +class LocationForm(object): + """A form for validating locations.""" + point = wtf.StringField(validators=[validators.point]) -- GitLab From be026e408a71977662962a259de5d80d2ff81c56 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 24 May 2016 15:57:24 -0400 Subject: [PATCH 03/18] Fix spacing in base models. --- app/models/base.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/base.py b/app/models/base.py index fd3b0a1..c693607 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -54,6 +54,7 @@ class Tracked(object): created = db.Column(columns.Arrow, default=func.now()) updated = db.Column(columns.Arrow, onupdate=func.now()) + class Address(object): """An address (with street number, city, state, zip, ...). This does not necessarily correlate with a physical location as defined in the @@ -75,6 +76,7 @@ class Location(object): """A physical location (with latitude, longitude, and elevation).""" point = db.Column(Geometry('POINT'), nullable=False) + class SalesForce(object): """A mixin that includes a SalesForce id.""" sales_force_id = db.Column(db.Unicode(255), index=True) -- GitLab From 7f376c0468e413bad4e16da04e1535c8910b7388 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 24 May 2016 16:10:47 -0400 Subject: [PATCH 04/18] Adapt generic configuration to building service. --- app/config/development.default.py | 2 +- app/config/test.default.py | 37 +------------------------------ 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/app/config/development.default.py b/app/config/development.default.py index 24620cb..2b8d67d 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -1,5 +1,5 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False -SQLALCHEMY_DATABASE_URI = 'sqlite://' +SQLALCHEMY_DATABASE_URI = 'postgresql:///buildingservice' REDIS_URI = 'redis://127.0.0.1:6379/' diff --git a/app/config/test.default.py b/app/config/test.default.py index 6231e62..8916da3 100644 --- a/app/config/test.default.py +++ b/app/config/test.default.py @@ -1,5 +1,5 @@ SQLALCHEMY_TRACK_MODIFICATIONS = False -SQLALCHEMY_DATABASE_URI = 'sqlite://' +SQLALCHEMY_DATABASE_URI = 'postgresql:///buildingservice-testing' REDIS_URI = 'redis://127.0.0.1:6379/' @@ -21,35 +21,6 @@ SERVICE_CONFIG = { # App APP_CACHE_EXPIRY = 60 * 60 # One hour. -# Test applications -TEST_CLIENTS = { - # This application should use a secret to authenticate and should have an - # authentication with the boilerplate service. - 'secret-authenticated': { - 'key': '', - 'secret': '' - }, - # This application should use a referrer to authenticate and should have an - # authentication with the boilerplate service. - 'referrer-authenticated': { - 'key': '', - 'referrer': '' - }, - # This application should use a referrer to authenticate, but should have no - # authentication with the boilerplate service. - 'referrer-unauthenticated': { - 'key': '', - 'referrer': '' - }, - # This application should use a referrer to authenticate, should have an - # authentication with the boilerplate service, and should have a role - # named 'test'. - 'referrer-authenticated-role': { - 'key': '', - 'referrer': '' - } -} - # Auth GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' USER_CACHE_EXPIRY = 60 * 60 # One Hour. @@ -58,12 +29,6 @@ USER_CACHE_EXPIRY = 60 * 60 # One Hour. HEADER_AUTH_KEY = 'x-blocpower-auth-key' HEADER_AUTH_TOKEN = 'x-blocpower-auth-token' -# Google auth information. -GOOGLE_CLIENT_ID = '$GOOGLE_CLIENT_ID' -GOOGLE_CLIENT_SECRET = '$GOOGLE_CLIENT_SECRET' -GOOGLE_REFRESH_TOKEN = '$GOOGLE_REFRESH_TOKEN' -GOOGLE_AUTH_URL = 'https://www.googleapis.com/oauth2/v4/token' - # HACK Fix issue where raising an exception in a test context creates an extra # request. PRESERVE_CONTEXT_ON_EXCEPTION = False -- GitLab From bfa78831848f350e37c9ddcb56a1485c0ea2464b Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 24 May 2016 16:57:00 -0400 Subject: [PATCH 05/18] Add json serialization of WKT/WKB elements. --- app/models/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/base.py b/app/models/base.py index 5b472fc..65bc0b8 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -4,6 +4,7 @@ import uuid from sqlalchemy.sql import func from sqlalchemy.ext.declarative import declared_attr from geoalchemy2 import Geometry +from geoalchemy2.elements import WKBElement, WKTElement from ..lib.database import db from ..lib import geography @@ -44,6 +45,8 @@ class Model(object): # If we cast decimals to floats, we end up with rounding # errors. Better to render them out to strings. value = str(value) + elif isinstance(value, (WKBElement, WKTElement)): + value = value.desc d[key] = value return d -- GitLab From 78fdb911aa4d5e5bd3702a9c558bdac281df6977 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 24 May 2016 16:57:42 -0400 Subject: [PATCH 06/18] Fix the building model. --- app/models/building.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/models/building.py b/app/models/building.py index b7fe50e..fd6b210 100644 --- a/app/models/building.py +++ b/app/models/building.py @@ -1,8 +1,7 @@ """Models for dealing with buildings.""" -from .base import Model, Salesforce, Tracked, Location, Address - +from .base import Model, SalesForce, Tracked, Location, Address from ..lib.database import db -class Building(Model, Tracked, Salesforce, Location, Address): - """ A building. """ - name = db.Column(db.Unicode(255), nullable=False) +class Building(Tracked, SalesForce, Location, Address, Model, db.Model): + """A building.""" + name = db.Column(db.Unicode(255)) -- GitLab From 1355969d63a9cb133848a5ff76700e33c8e6babc Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 24 May 2016 16:57:59 -0400 Subject: [PATCH 07/18] Add a building to the test environment. --- app/tests/environment.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/app/tests/environment.py b/app/tests/environment.py index 5ed6760..48df3cf 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -3,10 +3,8 @@ This provides several example models which can be used as needed in the test cases. """ -import requests - -from app.lib.red import redis -from app.lib.database import db, commit +from ..lib.database import db, commit +from ..models.building import Building class Environment(object): @@ -21,11 +19,15 @@ class Environment(object): return model @property - def google_token(self): - """Gets a valid google access token.""" - return requests.post(self.app.config['GOOGLE_AUTH_URL'], data={ - 'client_id': self.app.config['GOOGLE_CLIENT_ID'], - 'client_secret': self.app.config['GOOGLE_CLIENT_SECRET'], - 'refresh_token': self.app.config['GOOGLE_REFRESH_TOKEN'], - 'grant_type': 'refresh_token' - }).json()['id_token'] + def building(self): + """A building.""" + return self.add(Building( + name='test', + sales_force_id='test', + street_address='123 Test St', + city='Test', + county='Test', + state='NY', + country='United States of America', + postal_code='11111', + point='POINT(-70 40)')) -- GitLab From 5ea052ff12a8f9dbf9b26fea5d8403d93b2ee449 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 10:53:42 -0400 Subject: [PATCH 08/18] Use ST_AsText() to get geoalchemy point text. --- app/models/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/base.py b/app/models/base.py index 65bc0b8..536a506 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -1,8 +1,10 @@ import arrow import decimal import uuid + from sqlalchemy.sql import func from sqlalchemy.ext.declarative import declared_attr + from geoalchemy2 import Geometry from geoalchemy2.elements import WKBElement, WKTElement @@ -46,7 +48,7 @@ class Model(object): # errors. Better to render them out to strings. value = str(value) elif isinstance(value, (WKBElement, WKTElement)): - value = value.desc + value = value.ST_AsText() d[key] = value return d -- GitLab From 75b8cfe70deb8ffd7a6e2bcd593f510d0f99c37b Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 10:59:22 -0400 Subject: [PATCH 09/18] Fix ST_AsText() usage. --- app/models/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/base.py b/app/models/base.py index 536a506..8eef92f 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -48,7 +48,7 @@ class Model(object): # errors. Better to render them out to strings. value = str(value) elif isinstance(value, (WKBElement, WKTElement)): - value = value.ST_AsText() + value = db.session.scalar(value.ST_AsText()) d[key] = value return d -- GitLab From 671c4eed5f574025165af36ab94c28a2804971ff Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 11:20:11 -0400 Subject: [PATCH 10/18] Add salesforce base form. --- app/forms/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/forms/base.py b/app/forms/base.py index 8118e93..0ef9f49 100644 --- a/app/forms/base.py +++ b/app/forms/base.py @@ -27,3 +27,9 @@ class AddressForm(object): class LocationForm(object): """A form for validating locations.""" point = wtf.StringField(validators=[validators.point]) + + +class SalesForceForm(object): + """A form for validating salesforce models.""" + sales_force_id = wtf.StringField( + validators=[wtf.validators.Length(max=255)]) -- GitLab From 23338a399b4559e7600197c7d11569a47c01f61e Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 11:20:17 -0400 Subject: [PATCH 11/18] Add a building form. --- app/forms/building.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/forms/building.py diff --git a/app/forms/building.py b/app/forms/building.py new file mode 100644 index 0000000..1c54944 --- /dev/null +++ b/app/forms/building.py @@ -0,0 +1,8 @@ +"""Forms for validating buildings.""" +import wtforms as wtf +from .base import Form, AddressForm, LocationForm, SalesForceForm + + +class BuildingForm(SalesForceForm, LocationForm, AddressForm, Form): + """A form for validating buildings.""" + name = wtf.StringField(validators=[wtf.validators.Length(max=255)]) -- GitLab From c1e68d0b901cf11aa9821e4a4f3dc8109681b0a3 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 11:20:27 -0400 Subject: [PATCH 12/18] Add a building controller. --- app/controllers/building.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/controllers/building.py diff --git a/app/controllers/building.py b/app/controllers/building.py new file mode 100644 index 0000000..48af604 --- /dev/null +++ b/app/controllers/building.py @@ -0,0 +1,13 @@ +"""Controllers for posting or manipulating a building.""" +from ..models.building import Building +from ..forms.building import BuildingForm +from .base import RestController + + +class BuildingController(RestController): + """A building controller.""" + Model = Building + + def get_form(self, filter_data): + """Return the building form.""" + return BuildingForm -- GitLab From b8af8b86eea01cd6a455f3d191a5c586bebe721f Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 11:20:38 -0400 Subject: [PATCH 13/18] Add a building view. --- app/views/building.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 app/views/building.py diff --git a/app/views/building.py b/app/views/building.py new file mode 100644 index 0000000..ac867ec --- /dev/null +++ b/app/views/building.py @@ -0,0 +1,17 @@ +"""Views for working with buildings.""" +from werkzeug.exceptions import MethodNotAllowed +from ..controllers.building import BuildingController +from .base import RestView + + +class BuildingView(RestView): + """The building view.""" + def get_controller(self): + """Return an instance of the building controller.""" + return BuildingController() + + def put(self, id_): + raise MethodNotAllowed() + + def delete(self, id_): + raise MethodNotAllowed() -- GitLab From 5420e1bb2b0c6e84f56090f539254392b98c0d90 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 11:20:50 -0400 Subject: [PATCH 14/18] Register the building view. --- app/views/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/__init__.py b/app/views/__init__.py index 1dcf967..eefe515 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -1,4 +1,5 @@ """Flask-classy views for the flask application.""" +from . import building def register(app): @@ -7,4 +8,4 @@ def register(app): This can be used as a comprehensive list of all views that are present in the application. """ - pass + building.BuildingView.register(app) -- GitLab From 7d386fce0c77d29fb81e329ed47ac5f1125af53a Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 11:21:06 -0400 Subject: [PATCH 15/18] Add tests for the building view. --- app/tests/test_building.py | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 app/tests/test_building.py diff --git a/app/tests/test_building.py b/app/tests/test_building.py new file mode 100644 index 0000000..00258cd --- /dev/null +++ b/app/tests/test_building.py @@ -0,0 +1,52 @@ +"""Unit tests for building endpoints.""" +from sqlalchemy import and_ +from ..lib.database import db +from ..models.building import Building +from .base import RestTestCase + + +class BuildingTestCase(RestTestCase): + """Tests the /building/ endpoints.""" + url = '/building/' + + def test_index(self): + """Test /building/ GET.""" + _ = self.env.building + self._test_index() + + def test_get(self): + """Test /building/ GET. It should 405.""" + model = self.env.building + self._test_get(model.id) + + def test_post(self): + """Test /building/ POST.""" + data = { + 'name': 'test', + 'sales_force_id': 'test', + 'street_address': '123 Test St', + 'city': 'Test', + 'county': 'Test', + 'state': 'NY', + 'country': 'United States of America', + 'postal_code': '11111', + 'point': 'POINT(-70 40)'} + response_data = self._test_post(data) + filters = [Building.id == response_data['id']] + filters += [getattr(Building, k) == v for k, v in data.items()] + self.assertTrue( + db.session.query(Building).filter(and_(*filters)).first()) + + def test_put(self): + """Test /building/ PUT. It should 405.""" + model = self.env.building + data = model.get_dictionary() + data.update({'name': 'foo'}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 405) + + def test_delete(self): + """Test /building/ DELETE. It should 405.""" + model = self.env.building + response = self.delete(self.url + str(model.id)) + self.assertEqual(response.status_code, 405) -- GitLab From cbdd8158fc1f144f2cbedc94ca19b412390c2162 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 25 May 2016 17:00:46 -0400 Subject: [PATCH 16/18] Add an external model. --- app/models/base.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/models/base.py b/app/models/base.py index 8eef92f..835e50e 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -80,3 +80,9 @@ class Address(object): class Location(object): """A physical location (with latitude, longitude, and elevation).""" point = db.Column(Geometry('POINT'), nullable=False) + + +class External(object): + """An external-facing model. These have a UUID key.""" + key = db.Column( + columns.GUID(), index=True, nullable=False, default=uuid.uuid4) -- GitLab From 1c86b29464c2201b2371fffb31321d77f46a4bbe Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Thu, 26 May 2016 09:59:58 -0400 Subject: [PATCH 17/18] Make buildings external. --- app/models/building.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/building.py b/app/models/building.py index fd6b210..224f23e 100644 --- a/app/models/building.py +++ b/app/models/building.py @@ -1,7 +1,8 @@ """Models for dealing with buildings.""" -from .base import Model, SalesForce, Tracked, Location, Address +from .base import Model, SalesForce, Tracked, Location, Address, External from ..lib.database import db -class Building(Tracked, SalesForce, Location, Address, Model, db.Model): +class Building( + Tracked, External, SalesForce, Location, Address, Model, db.Model): """A building.""" name = db.Column(db.Unicode(255)) -- GitLab From bcadb3c540fa6b7063c431b34c19a32b2728525d Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Thu, 26 May 2016 10:00:38 -0400 Subject: [PATCH 18/18] Fix docstring for /building/ GET test. --- app/tests/test_building.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/test_building.py b/app/tests/test_building.py index 00258cd..687d0f5 100644 --- a/app/tests/test_building.py +++ b/app/tests/test_building.py @@ -15,7 +15,7 @@ class BuildingTestCase(RestTestCase): self._test_index() def test_get(self): - """Test /building/ GET. It should 405.""" + """Test /building/ GET.""" model = self.env.building self._test_get(model.id) -- GitLab