From ffadd16b8e35eaa20b7ba68d6a2148f5a64fb5a5 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 12:02:23 -0400 Subject: [PATCH 01/53] Remove unused imports from the project model. --- app/models/project.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/project.py b/app/models/project.py index 74870fb..e644f26 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -1,7 +1,5 @@ """Models directly-relating to the top-level project.""" -from sqlalchemy import event - -from app.lib.database import db, commit +from app.lib.database import db from app.models.base import Model, Tracked -- GitLab From 22e07fcee1cc35a56777bffc3d9a484380d7b7fb Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:49:32 -0400 Subject: [PATCH 02/53] Add missing import to the project controller. --- app/controllers/project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/project.py b/app/controllers/project.py index c1aaf53..f338345 100644 --- a/app/controllers/project.py +++ b/app/controllers/project.py @@ -1,4 +1,5 @@ """Controllers for managing top-level projects.""" +from werkzeug.exceptions import BadRequest from app.lib.database import db from app.controllers.base import RestController from app.models.project import Project, ProjectStateChange -- GitLab From 948fa9c66c8648cf5dcda06dc94752af6b21100d Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:49:52 -0400 Subject: [PATCH 03/53] Add explanatory note to the get_form method of the project controller. --- app/controllers/project.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/project.py b/app/controllers/project.py index f338345..9122c42 100644 --- a/app/controllers/project.py +++ b/app/controllers/project.py @@ -11,6 +11,7 @@ class ProjectController(RestController): Model = Project def get_form(self, filter_data): + """Return a form to either create or edit a project.""" try: return { 'create': CreateProjectForm, -- GitLab From 66f1b29e5e4ea193079409fe1660a572716d0a35 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:50:13 -0400 Subject: [PATCH 04/53] Fix quotes in the project view. --- app/views/project.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/project.py b/app/views/project.py index 20e9939..9ba2ad0 100644 --- a/app/views/project.py +++ b/app/views/project.py @@ -15,12 +15,12 @@ class ProjectView(RestView): def post(self): form = request.args.get('form') - with FormNeed("create", form): + with FormNeed('create', form): return super(ProjectView, self).post() def put(self, id_): form = request.args.get('form') - with FormNeed("update", form): + with FormNeed('update', form): return super(ProjectView, self).put(id_) def delete(self, id_): -- GitLab From ff40e67dfb7acced4bc39815067b263571b440f5 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:50:25 -0400 Subject: [PATCH 05/53] Add a client model. --- app/models/client.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/models/client.py diff --git a/app/models/client.py b/app/models/client.py new file mode 100644 index 0000000..dcd2dad --- /dev/null +++ b/app/models/client.py @@ -0,0 +1,9 @@ +"""Models for tracking client information.""" +from app.lib.database import db +from app.models.base import Model, Tracked + + +class Client(Model, Tracked, db.Model): + """A client.""" + sales_force_id = db.Column(db.Unicode(255), index=True) + name = db.Column(db.Unicode(255), nullable=False) -- GitLab From 02b4bf590ff07a481edf69b198ec2492eff11db7 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:51:12 -0400 Subject: [PATCH 06/53] Add client create/update forms. --- app/forms/client.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/forms/client.py diff --git a/app/forms/client.py b/app/forms/client.py new file mode 100644 index 0000000..55d6c6d --- /dev/null +++ b/app/forms/client.py @@ -0,0 +1,23 @@ +"""Forms for validating clients.""" +import wtforms as wtf +from app.forms.base import Form + + +class CreateClientForm(Form): + """A form for posting new clients.""" + sales_force_id = wtf.StringField( + validators=[wtf.validators.Length(max=255)]) + name = wtf.StringField( + validators=[ + wtf.validators.Required(), + wtf.validators.Length(max=255) + ]) + + +class UpdateClientForm(Form): + """A form for manipulating existing clients.""" + name = wtf.StringField( + validators=[ + wtf.validators.Required(), + wtf.validators.Length(max=255) + ]) -- GitLab From 1d9d92649f1898ae193300901c5b38271bb9cdee Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:51:24 -0400 Subject: [PATCH 07/53] Add client controller. --- app/controllers/client.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/controllers/client.py diff --git a/app/controllers/client.py b/app/controllers/client.py new file mode 100644 index 0000000..583ca26 --- /dev/null +++ b/app/controllers/client.py @@ -0,0 +1,20 @@ +"""Controllers for managing clients.""" +from werkzeug.exceptions import BadRequest +from app.controllers.base import RestController +from app.models.client import Client +from app.forms.client import CreateClientForm, UpdateClientForm + + +class ClientController(RestController): + """The client controller.""" + Model = Client + + def get_form(self, filter_data): + """Return a form to either create or edit a client.""" + try: + return { + 'create': CreateClientForm, + 'update': UpdateClientForm + }[filter_data.get('form')] + except KeyError: + raise BadRequest('Invalid form.') -- GitLab From e3f271f401d6840750e0b46416279fe5e4610807 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:51:42 -0400 Subject: [PATCH 08/53] Remove trailing space in the project test. --- app/tests/test_project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/test_project.py b/app/tests/test_project.py index 4b07d4f..2d15352 100644 --- a/app/tests/test_project.py +++ b/app/tests/test_project.py @@ -93,7 +93,7 @@ class TestProject(RestTestCase): def test_put_state(self): """Tests /project/ PUT with a new state. - + This should create a project state change model. """ model = self.env.project -- GitLab From e05915c6164d3f52327d6617b829a07070128f06 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:51:53 -0400 Subject: [PATCH 09/53] Add a client view. --- app/views/client.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 app/views/client.py diff --git a/app/views/client.py b/app/views/client.py new file mode 100644 index 0000000..06b031c --- /dev/null +++ b/app/views/client.py @@ -0,0 +1,29 @@ +"""Views for managing clients.""" +from werkzeug.exceptions import MethodNotAllowed +from flask import request +from app.views.base import RestView +from app.controllers.client import ClientController +from app.permissions.base import FormNeed + + +class ClientView(RestView): + """The client view.""" + def get_controller(self): + """Return an instance of the client controller.""" + return ClientController() + + def post(self): + """Checks for a create form before posting.""" + form = request.args.get('form') + with FormNeed('create', form): + return super(ClientView, self).post() + + def put(self, id_): + form = request.args.get('form') + with FormNeed('update', form): + return super(ClientView, self).put(id_) + + def delete(self, id_): + """Not implemented.""" + raise MethodNotAllowed( + 'Clients cannot be deleted. Cancel the project instead.') -- GitLab From 35e817766c4cd960581953fff877229f7de4a276 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:52:04 -0400 Subject: [PATCH 10/53] Register the client view. --- app/views/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/__init__.py b/app/views/__init__.py index ee90b57..ab975b2 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -1,5 +1,5 @@ """Flask-classy views for the flask application.""" -from app.views import application, auth, project +from app.views import application, auth, project, client def register(app): @@ -10,6 +10,8 @@ def register(app): """ application.AppAuthView.register(app) + auth.GoogleAuthView.register(app) + project.ProjectView.register(app) - auth.GoogleAuthView.register(app) + client.ClientView.register(app) -- GitLab From f7651070904b5224c5bba27eb7f19f5d65f6f61f Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:52:45 -0400 Subject: [PATCH 11/53] Add a client to the test environment. --- app/tests/environment.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/tests/environment.py b/app/tests/environment.py index 77ca585..b5be42a 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -11,6 +11,7 @@ from app.lib.database import db, commit from app.models.application import Application from app.models.project import Project +from app.models.client import Client class Environment(object): @@ -52,10 +53,6 @@ class Environment(object): redis.client.sadd(application.key, token) return application, token - @property - def project(self): - return self.add(Project(sales_force_id='test', name='test')) - @property def google_token(self): """Gets a valid google access token.""" @@ -65,3 +62,11 @@ class Environment(object): 'refresh_token': self.app.config['GOOGLE_REFRESH_TOKEN'], 'grant_type': 'refresh_token' }).json()['id_token'] + + @property + def project(self): + return self.add(Project(sales_force_id='test', name='test')) + + @property + def client(self): + return self.add(Client(sales_force_id='test', name='test')) -- GitLab From bab2cfb5ffe1dfed64b9460109cc2d47d5f8d8f8 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Mon, 21 Mar 2016 15:52:57 -0400 Subject: [PATCH 12/53] Add a test case for the client endpoint. --- app/tests/test_client.py | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 app/tests/test_client.py diff --git a/app/tests/test_client.py b/app/tests/test_client.py new file mode 100644 index 0000000..cc0ab57 --- /dev/null +++ b/app/tests/test_client.py @@ -0,0 +1,83 @@ +"""Unit tests for clients.""" +from sqlalchemy import and_ +from app.lib.database import db +from app.tests.base import RestTestCase +from app.models.client import Client + + +class TestClient(RestTestCase): + """Tests the /client/ endpoints.""" + url = '/client/' + Model = Client + + def test_index(self): + """Tests /client/ GET.""" + model = self.env.client + self._test_index() + + def test_get(self): + """Tests /client/ GET.""" + model = self.env.client + self._test_get(model.id) + + def test_post(self): + """Tests /client/ POST.""" + data = self._test_post( + { + 'sales_force_id': 'test', + 'name': 'test' + }, { + 'form': 'create' + }) + self.assertTrue( + db.session.query(Client) + .filter(and_( + Client.id == data['id'], + Client.sales_force_id == data['sales_force_id'], + Client.name == data['name']))\ + .first()) + + def test_post_bad_form(self): + """Tests /client/ POST with a bad form. It should 400.""" + response = self.post(self.url, + data={'sales_force_id': 'test', 'name': 'test'}, + query_string={'form': 'update'}) + self.assertEqual(response.status_code, 400) + + def test_post_missing_name(self): + """Tests /client/ POST with a missing name. It should 400.""" + response = self.post(self.url, + data={'sales_force_id': 'test'}, + query_string={'form': 'create'}) + self.assertEqual(response.status_code, 400) + + def test_put(self): + """Tests /client/ PUT.""" + model = self.env.client + data = model.get_dictionary() + data.update({'name': 'foo'}) + response_data = self._test_put(model.id, data, {'form': 'update'}) + self.assertEqual(response_data['name'], 'foo') + self.assertTrue( + db.session.query(Client)\ + .filter(and_( + Client.id == model.id, + Client.name == data['name']))\ + .first()) + + def test_put_bad_form(self): + """Tests /client/ PUT with a bad form. It should 400.""" + model = self.env.client + data = model.get_dictionary() + data.update({'name': 'foo'}) + response = self.put( + self.url + str(model.id), + data=data, + query_string={'form': 'create'}) + self.assertEqual(response.status_code, 400) + + def test_delete(self): + """Tests /client/ DELETE. It should 405.""" + model = self.env.client + response = self.delete(self.url + str(model.id)) + self.assertEqual(response.status_code, 405) -- GitLab From 3ce9f04091d9e2b0ba1f081dd649dd0603a5b4c0 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:00:55 -0400 Subject: [PATCH 13/53] Add sales force model mixin. --- app/models/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/models/base.py b/app/models/base.py index 1a7283b..b63c56e 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -41,3 +41,8 @@ class Tracked(object): """A mixin to include tracking datetime fields.""" created = db.Column(db.DateTime, default=func.now()) updated = db.Column(db.DateTime, onupdate=func.now()) + + +class SalesForce(object): + """A mixin that includes a SalesForce id.""" + sales_force_id = db.Column(db.Unicode(255), index=True) -- GitLab From 135fd41aecbd20b96788143a9956d38431898cfd Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:01:12 -0400 Subject: [PATCH 14/53] Use the sales force model mixin in the project model. --- app/models/project.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/project.py b/app/models/project.py index e644f26..1d9eb5b 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -1,9 +1,9 @@ """Models directly-relating to the top-level project.""" from app.lib.database import db -from app.models.base import Model, Tracked +from app.models.base import Model, Tracked, SalesForce -class Project(Model, Tracked, db.Model): +class Project(Model, Tracked, SalesForce, db.Model): """A project.""" # TODO SQLAlchemy < 1.1 does not support the use of a python enum in Enum # fields, so for now we have to use a list of strings. When 1.1 leaves @@ -34,7 +34,6 @@ class Project(Model, Tracked, db.Model): 'constructed', 'verified', 'paid' ] - sales_force_id = db.Column(db.Unicode(255), index=True) name = db.Column(db.Unicode(255), nullable=False) state = db.Column(db.Enum(*states), default='pending', nullable=False) -- GitLab From a625635bfed8a512b142dcbebb8e5dedca897224 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:01:30 -0400 Subject: [PATCH 15/53] Use the sales force model mixin in the client model. --- app/models/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/client.py b/app/models/client.py index dcd2dad..a473e61 100644 --- a/app/models/client.py +++ b/app/models/client.py @@ -1,9 +1,9 @@ """Models for tracking client information.""" from app.lib.database import db -from app.models.base import Model, Tracked +from app.models.base import Model, Tracked, SalesForce -class Client(Model, Tracked, db.Model): +class Client(Model, Tracked, SalesForce, db.Model): """A client.""" sales_force_id = db.Column(db.Unicode(255), index=True) name = db.Column(db.Unicode(255), nullable=False) -- GitLab From dbb556e88369a55a487b9611b7c64905f16f63c4 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:02:00 -0400 Subject: [PATCH 16/53] Add docstring for the /client/ PUT endpoint. --- app/views/client.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/client.py b/app/views/client.py index 06b031c..0d69c58 100644 --- a/app/views/client.py +++ b/app/views/client.py @@ -19,6 +19,7 @@ class ClientView(RestView): return super(ClientView, self).post() def put(self, id_): + """Checks for an update form before putting.""" form = request.args.get('form') with FormNeed('update', form): return super(ClientView, self).put(id_) -- GitLab From c320ec71ade4e377eae73da1538a69db25b4ad23 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:02:25 -0400 Subject: [PATCH 17/53] Use backslash correctly in the client test case. --- app/tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tests/test_client.py b/app/tests/test_client.py index cc0ab57..0af9978 100644 --- a/app/tests/test_client.py +++ b/app/tests/test_client.py @@ -30,7 +30,7 @@ class TestClient(RestTestCase): 'form': 'create' }) self.assertTrue( - db.session.query(Client) + db.session.query(Client)\ .filter(and_( Client.id == data['id'], Client.sales_force_id == data['sales_force_id'], -- GitLab From c9e37f78185786cf45c6fd0bf1dc392d85b59c5f Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:02:39 -0400 Subject: [PATCH 18/53] Add a contact model. --- app/models/contact.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/models/contact.py diff --git a/app/models/contact.py b/app/models/contact.py new file mode 100644 index 0000000..346b252 --- /dev/null +++ b/app/models/contact.py @@ -0,0 +1,8 @@ +"""Models for dealing with contacts.""" +from app.lib.database import db +from app.models.base import Model, Tracked, SalesForce + + +class Contact(Model, Tracked, SalesForce, db.Model): + """A contact (i.e an entity you can reach out to).""" + name = db.Column(db.Unicode(255), nullable=False) -- GitLab From e0c6762fce48c7df18fe40d074e1318767a17365 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:02:50 -0400 Subject: [PATCH 19/53] Add the contact model to the test environment. --- app/tests/environment.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/tests/environment.py b/app/tests/environment.py index b5be42a..2175beb 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -12,6 +12,7 @@ from app.lib.database import db, commit from app.models.application import Application from app.models.project import Project from app.models.client import Client +from app.models.contact import Contact class Environment(object): @@ -70,3 +71,7 @@ class Environment(object): @property def client(self): return self.add(Client(sales_force_id='test', name='test')) + + @property + def contact(self): + return self.add(Contact(sales_force_id='test', name='test')) -- GitLab From e3e11565eb48c9b6b5c647e6861d54bded895dd7 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:03:14 -0400 Subject: [PATCH 20/53] Add create and update forms for the contact model. --- app/forms/contact.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/forms/contact.py diff --git a/app/forms/contact.py b/app/forms/contact.py new file mode 100644 index 0000000..c89d98b --- /dev/null +++ b/app/forms/contact.py @@ -0,0 +1,23 @@ +"""Forms for dealing with contacts.""" +import wtforms as wtf +from app.forms.base import Form + + +class CreateContactForm(Form): + """A form to create new contacts.""" + sales_force_id = wtf.StringField( + validators=[wtf.validators.Length(max=255)]) + name = wtf.StringField( + validators=[ + wtf.validators.Required(), + wtf.validators.Length(max=255) + ]) + + +class UpdateContactForm(Form): + """A form for manipulating existing contacts.""" + name = wtf.StringField( + validators=[ + wtf.validators.Required(), + wtf.validators.Length(max=255) + ]) -- GitLab From ac5800d5c7bb527d41ee7ef672f446b8d45e19e2 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:03:25 -0400 Subject: [PATCH 21/53] Add a contact controller. --- app/controllers/contact.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 app/controllers/contact.py diff --git a/app/controllers/contact.py b/app/controllers/contact.py new file mode 100644 index 0000000..be51bdf --- /dev/null +++ b/app/controllers/contact.py @@ -0,0 +1,20 @@ +"""Controllers for managing contacts.""" +from werkzeug.exceptions import BadRequest +from app.controllers.base import RestController +from app.models.contact import Contact +from app.forms.contact import CreateContactForm, UpdateContactForm + + +class ContactController(RestController): + """The client controller.""" + Model = Contact + + def get_form(self, filter_data): + """Return a form to either create or edit a contact.""" + try: + return { + 'create': CreateContactForm, + 'update': UpdateContactForm + }[filter_data.get('form')] + except KeyError: + raise BadRequest('Invalid form.') -- GitLab From 192bd979489f9c573bc03f837aee1196b8d121db Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:03:34 -0400 Subject: [PATCH 22/53] Add a contact view. --- app/views/contact.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 app/views/contact.py diff --git a/app/views/contact.py b/app/views/contact.py new file mode 100644 index 0000000..6bbff2a --- /dev/null +++ b/app/views/contact.py @@ -0,0 +1,25 @@ +"""Views for managing contacts.""" +from werkzeug.exceptions import MethodNotAllowed +from flask import request +from app.views.base import RestView +from app.controllers.contact import ContactController +from app.permissions.base import FormNeed + + +class ContactView(RestView): + """The contact view.""" + def get_controller(self): + """Return an instance of the contact controller.""" + return ContactController() + + def post(self): + """Checks for a create form before posting.""" + form = request.args.get('form') + with FormNeed('create', form): + return super(ContactView, self).post() + + def put(self, id_): + """Checks for an update form before putting.""" + form = request.args.get('form') + with FormNeed('update', form): + return super(ContactView, self).put(id_) -- GitLab From 3b618144d715ceacb1025bd3c80f097701c83d5c Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:03:43 -0400 Subject: [PATCH 23/53] Register the contact view. --- app/views/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/views/__init__.py b/app/views/__init__.py index ab975b2..8d3da44 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -1,5 +1,5 @@ """Flask-classy views for the flask application.""" -from app.views import application, auth, project, client +from app.views import application, auth, project, client, contact def register(app): @@ -15,3 +15,5 @@ def register(app): project.ProjectView.register(app) client.ClientView.register(app) + + contact.ContactView.register(app) -- GitLab From f5822d83ce56651abe9b1a05e88fe1d124507930 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 11:03:50 -0400 Subject: [PATCH 24/53] Add a contact test case. --- app/tests/test_contact.py | 81 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 app/tests/test_contact.py diff --git a/app/tests/test_contact.py b/app/tests/test_contact.py new file mode 100644 index 0000000..688c2ac --- /dev/null +++ b/app/tests/test_contact.py @@ -0,0 +1,81 @@ +"""Unit tests for working with contacts.""" +from sqlalchemy import and_ +from app.lib.database import db +from app.tests.base import RestTestCase +from app.models.contact import Contact + + +class TestContact(RestTestCase): + """Tests the /contact/ endpoints.""" + url = '/contact/' + Model = Contact + + def test_index(self): + """Tests /contact/ GET.""" + model = self.env.contact + self._test_index() + + def test_get(self): + """Tests /client/ GET.""" + model = self.env.contact + self._test_get(model.id) + + def test_post(self): + """Tests /contact/ POST.""" + data = { + 'sales_force_id': 'test', + 'name': 'test' + } + response_data = self._test_post(data, {'form': 'create'}) + self.assertTrue( + db.session.query(Contact)\ + .filter(and_( + Contact.id == response_data['id'], + Contact.sales_force_id == data['sales_force_id'], + Contact.name == data['name']))\ + .first()) + + def test_post_bad_form(self): + """Tests /contact/ POST with a bad form. It should 400.""" + response = self.post(self.url, + data={'sales_force_id': 'test', 'name': 'test'}, + query_string={'form': 'update'}) + self.assertEqual(response.status_code, 400) + + def test_post_missing_name(self): + """Tests /contact/ POST with a missing name. It should 400.""" + response = self.post(self.url, + data={'sales_force_id': 'test'}, + query_string={'form': 'create'}) + self.assertEqual(response.status_code, 400) + + def test_put(self): + """Tests /contact/ PUT.""" + model = self.env.contact + data = model.get_dictionary() + data.update({'name': 'foo'}) + response_data = self._test_put(model.id, data, {'form': 'update'}) + self.assertEqual(response_data['name'], 'foo'); + self.assertTrue( + db.session.query(Contact)\ + .filter(and_( + Contact.id == model.id, + Contact.name == data['name'] + ))\ + .first()) + + def test_put_bad_form(self): + """Tests /contact/ PUT with a bad form. It should 400.""" + model = self.env.contact + data = model.get_dictionary() + data.update({'name': 'foo'}) + response = self.put( + self.url + str(model.id), + data=data, + query_string={'form': 'create'}) + self.assertEqual(response.status_code, 400) + + def test_delete(self): + """Tests /contact/ DELETE.""" + model = self.env.contact + self._test_delete(model.id) -- GitLab From 099a5021e7bd933b2b12b791c9337a69831f1153 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:45:29 -0400 Subject: [PATCH 25/53] Add a phone number validator. --- app/forms/validators.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 app/forms/validators.py diff --git a/app/forms/validators.py b/app/forms/validators.py new file mode 100644 index 0000000..a5648f6 --- /dev/null +++ b/app/forms/validators.py @@ -0,0 +1,6 @@ +"""Custom validators.""" +import wtforms as wtf + + +# A phone number validator. +phone = wtf.validators.Regexp(r'^[0-9]{10,}$') -- GitLab From e3db89ffacc954862652e8959e13f4e9cd67cf9f Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:46:53 -0400 Subject: [PATCH 26/53] Add a Map validator. This is useful when the value of one form field affects the validators used against another. For example, `contact_method` would affect the validator used against `contact_value`. --- app/forms/validators.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/forms/validators.py b/app/forms/validators.py index a5648f6..1aed526 100644 --- a/app/forms/validators.py +++ b/app/forms/validators.py @@ -2,5 +2,23 @@ import wtforms as wtf +class Map(object): + """A mapped validator. Maps the value of another field on the form to a + list of validators to apply to this field. + """ + def __init__(self, condition_field_name, map_): + """Initialize the validator with a condition field and a map.""" + self.condition_field_name = condition_field_name + self.map_ = map_ + + def __call__(self, form, field): + """Call the subvalidators from the matching map_ value.""" + condition_field = form._fields.get(self.condition_field_name) + if condition_field: + condition_field = condition_field.data + for validator in self.map_.get(condition_field, []): + validator(form, field) + + # A phone number validator. phone = wtf.validators.Regexp(r'^[0-9]{10,}$') -- GitLab From 6684f015feeeaddd07d857e7ea39d48d349ca8d6 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:47:28 -0400 Subject: [PATCH 27/53] Add support for constant fields to the base controller. --- app/controllers/base.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/controllers/base.py b/app/controllers/base.py index 75036dc..330d4e6 100644 --- a/app/controllers/base.py +++ b/app/controllers/base.py @@ -34,6 +34,9 @@ class RestController(object): # The primary key to use to retrieve the model in get/put/delete requests. key = 'id' + # Field names that put() is not allowed to change. + constant_fields = [] + def commit(self): """Commits the transaction. @@ -105,10 +108,19 @@ class RestController(object): form = self.get_form(filter_data)(formdata=MultiDict(data)) if not str(form.id.data) == str(id_): - raise BadRequest(['The id in the model and the uri do not match.']) + raise BadRequest( + {'id': 'The id in the model and the uri do not match.'}) if not form.validate(): raise BadRequest(form.errors) + for field in self.constant_fields: + if not getattr(model, field) == getattr(form, field).data: + raise BadRequest( + { + '{}'.format(field): \ + 'The {} field cannot be modified.'.format(field) + }) + form.populate_obj(model) db.session.add(model) self.commit() -- GitLab From fd14661817150622f6136636d9d00553c016d009 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:47:52 -0400 Subject: [PATCH 28/53] Add a contact method model. --- app/models/contact.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/models/contact.py b/app/models/contact.py index 346b252..96254ba 100644 --- a/app/models/contact.py +++ b/app/models/contact.py @@ -6,3 +6,22 @@ from app.models.base import Model, Tracked, SalesForce class Contact(Model, Tracked, SalesForce, db.Model): """A contact (i.e an entity you can reach out to).""" name = db.Column(db.Unicode(255), nullable=False) + + +class ContactMethod(Model, Tracked, db.Model): + """A contact method.""" + # TODO SQLAlchemy < 1.1 does not support the use of a python enum in Enum + # fields, so for now we have to use a list of strings. When 1.1 leaves + # beta, this should be ported to the Enum below. + # + #class Methods(unicode, Enum): + # SMS = 'sms' + # Phone = 'phone' + # Fax = 'fax' + # Email = 'email' + methods = ['sms', 'phone', 'fax', 'email'] + + contact_id = db.Column( + db.Integer, db.ForeignKey('contact.id'), nullable=False) + method = db.Column(db.Enum(*methods), nullable=False) + value = db.Column(db.Unicode(255), nullable=False) -- GitLab From f48812bebe308868600c5ee39bd734cee9799ba3 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:48:32 -0400 Subject: [PATCH 29/53] Add a contact method form. --- app/forms/contact.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/forms/contact.py b/app/forms/contact.py index c89d98b..87739f9 100644 --- a/app/forms/contact.py +++ b/app/forms/contact.py @@ -1,6 +1,9 @@ """Forms for dealing with contacts.""" import wtforms as wtf + +from app.forms import validators from app.forms.base import Form +from app.models.contact import ContactMethod class CreateContactForm(Form): @@ -21,3 +24,23 @@ class UpdateContactForm(Form): wtf.validators.Required(), wtf.validators.Length(max=255) ]) + + +class ContactMethodForm(Form): + """A form for validating contact methods.""" + contact_id = wtf.IntegerField(validators=[wtf.validators.Required()]) + method = wtf.StringField( + validators=[ + wtf.validators.Required(), + wtf.validators.AnyOf(ContactMethod.methods) + ]) + value = wtf.StringField( + validators=[ + wtf.validators.Required(), + validators.Map('method', { + 'email': [wtf.validators.Email()], + 'phone': [validators.phone], + 'sms': [validators.phone], + 'fax': [validators.phone] + }) + ]) -- GitLab From 0a11c0e8248e00d8f5cb163122e69427d20543cc Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:48:47 -0400 Subject: [PATCH 30/53] Add a contact method controller. --- app/controllers/contact.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/controllers/contact.py b/app/controllers/contact.py index be51bdf..8f1bb62 100644 --- a/app/controllers/contact.py +++ b/app/controllers/contact.py @@ -1,12 +1,13 @@ """Controllers for managing contacts.""" from werkzeug.exceptions import BadRequest from app.controllers.base import RestController -from app.models.contact import Contact -from app.forms.contact import CreateContactForm, UpdateContactForm +from app.models.contact import Contact, ContactMethod +from app.forms.contact import ( + CreateContactForm, UpdateContactForm, ContactMethodForm) class ContactController(RestController): - """The client controller.""" + """The contact controller.""" Model = Contact def get_form(self, filter_data): @@ -18,3 +19,14 @@ class ContactController(RestController): }[filter_data.get('form')] except KeyError: raise BadRequest('Invalid form.') + + +class ContactMethodController(RestController): + """The contact method controller.""" + Model = ContactMethod + + constant_fields = ['contact_id', 'method'] + + def get_form(self, filter_data): + """Return the contact method form.""" + return ContactMethodForm -- GitLab From 5cc4fea3f35ef9689291df58abe85fc4b9e45c21 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:49:09 -0400 Subject: [PATCH 31/53] Add contact methods to the test environment. --- app/tests/environment.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/app/tests/environment.py b/app/tests/environment.py index 2175beb..cc1f21b 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -12,7 +12,7 @@ from app.lib.database import db, commit from app.models.application import Application from app.models.project import Project from app.models.client import Client -from app.models.contact import Contact +from app.models.contact import Contact, ContactMethod class Environment(object): @@ -75,3 +75,31 @@ class Environment(object): @property def contact(self): return self.add(Contact(sales_force_id='test', name='test')) + + @property + def email_contact_method(self): + return self.add(ContactMethod( + contact_id=self.contact.id, + method='email', + value='test@blocpower.org')) + + @property + def phone_contact_method(self): + return self.add(ContactMethod( + contact_id=self.contact.id, + method='phone', + value='5555555555')) + + @property + def sms_contact_method(self): + return self.add(ContactMethod( + contact_id=self.contact.id, + method='sms', + value='5555555555')) + + @property + def fax_contact_method(self): + return self.add(ContactMethod( + contact_id=self.contact.id, + method='fax', + value='5555555555')) -- GitLab From 5e94f235a61f4c2a4a6f1eecc00e32e22b5c30f7 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:49:32 -0400 Subject: [PATCH 32/53] Add a contact method view. --- app/views/contact.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/views/contact.py b/app/views/contact.py index 6bbff2a..0841b74 100644 --- a/app/views/contact.py +++ b/app/views/contact.py @@ -2,7 +2,7 @@ from werkzeug.exceptions import MethodNotAllowed from flask import request from app.views.base import RestView -from app.controllers.contact import ContactController +from app.controllers.contact import ContactController, ContactMethodController from app.permissions.base import FormNeed @@ -23,3 +23,12 @@ class ContactView(RestView): form = request.args.get('form') with FormNeed('update', form): return super(ContactView, self).put(id_) + + +class ContactMethodView(RestView): + """The contact method view.""" + route_base = '/contact/method/' + + def get_controller(self): + """Return an instance of the contact method controller.""" + return ContactMethodController() -- GitLab From d4a842c3a7bd10714261abaae7a9a7618913162a Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:49:42 -0400 Subject: [PATCH 33/53] Register the contact method view. --- app/views/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/__init__.py b/app/views/__init__.py index 8d3da44..f0b04e2 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -17,3 +17,4 @@ def register(app): client.ClientView.register(app) contact.ContactView.register(app) + contact.ContactMethodView.register(app) -- GitLab From 61134fe36b1dbf6981f7a8855e19201099f7fb80 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:50:00 -0400 Subject: [PATCH 34/53] Add test for the /contact/method/ endpoints. --- app/tests/test_contact.py | 202 +++++++++++++++++++++++++++++++++++++- 1 file changed, 200 insertions(+), 2 deletions(-) diff --git a/app/tests/test_contact.py b/app/tests/test_contact.py index 688c2ac..fdc4964 100644 --- a/app/tests/test_contact.py +++ b/app/tests/test_contact.py @@ -2,7 +2,7 @@ from sqlalchemy import and_ from app.lib.database import db from app.tests.base import RestTestCase -from app.models.contact import Contact +from app.models.contact import Contact, ContactMethod class TestContact(RestTestCase): @@ -16,7 +16,7 @@ class TestContact(RestTestCase): self._test_index() def test_get(self): - """Tests /client/ GET.""" + """Tests /contact/ GET.""" model = self.env.contact self._test_get(model.id) @@ -79,3 +79,201 @@ class TestContact(RestTestCase): """Tests /contact/ DELETE.""" model = self.env.contact self._test_delete(model.id) + + +class TestContact(RestTestCase): + """Tests the /contact/method/ endpoints.""" + url = '/contact/method/' + Model = ContactMethod + + def test_index(self): + """Tests /contact/method/ GET.""" + model = self.env.email_contact_method + self._test_index() + + def test_get(self): + """Tests /contact/method/ GET.""" + model = self.env.email_contact_method + self._test_get(model.id) + + def test_post_email(self): + """Tests /contact/method/ POST with an email.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'email', + 'value': 'test@blocpower.org' + } + response_data = self._test_post(data) + self.assertTrue( + db.session.query(ContactMethod)\ + .filter(and_( + ContactMethod.id == response_data['id'], + ContactMethod.method == data['method'], + ContactMethod.value == data['value']))\ + .first()) + + def test_post_bad_email(self): + """Tests /contact/method/ POST with a bad email.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'email', + 'value': 'foo@bar' + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_phone(self): + """Tests /contact/method/ POST with a phone number.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'phone', + 'value': '55555555555' + } + response_data = self._test_post(data) + self.assertTrue( + db.session.query(ContactMethod)\ + .filter(and_( + ContactMethod.id == response_data['id'], + ContactMethod.method == data['method'], + ContactMethod.value == data['value']))\ + .first()) + + def test_post_bad_phone(self): + """Tests /contact/method/ POST with a bad phone number.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'phone', + 'value': '555555555' # One character too short. + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_sms(self): + """Tests /contact/method/ POST with a sms number.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'sms', + 'value': '55555555555' + } + response_data = self._test_post(data) + self.assertTrue( + db.session.query(ContactMethod)\ + .filter(and_( + ContactMethod.id == response_data['id'], + ContactMethod.method == data['method'], + ContactMethod.value == data['value']))\ + .first()) + + def test_post_bad_sms(self): + """Tests /contact/method/ POST with a bad sms number.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'sms', + 'value': '555555555' # One character too short. + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_fax(self): + """Tests /contact/method/ POST with a fax number.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'fax', + 'value': '55555555555' + } + response_data = self._test_post(data) + self.assertTrue( + db.session.query(ContactMethod)\ + .filter(and_( + ContactMethod.id == response_data['id'], + ContactMethod.method == data['method'], + ContactMethod.value == data['value']))\ + .first()) + + def test_post_bad_fax(self): + """Tests /contact/method/ POST with a bad fax number.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'fax', + 'value': '555555555' # One character too short. + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_bad_method(self): + """Tests /contact/method/ POST with a bad method.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'mail', + 'value': 'test@blocpower.org' + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_bad_contact_id(self): + """Tests /contact/method/ POST with a bad contact id.""" + data = { + 'contact_id': 1, + 'method': 'email', + 'value': 'test@blocpower.org' + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_no_method(self): + """Tests /contact/method/ POST with no method.""" + data = { + 'contact_id': self.env.contact.id, + 'value': 'test@blocpower.org' + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_no_value(self): + """Tests /contact/method/ POST with no value.""" + data = { + 'contact_id': self.env.contact.id, + 'method': 'email' + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_put(self): + """Tests /contact/method/ PUT.""" + model = self.env.email_contact_method + data = model.get_dictionary() + data.update({'value': 'foo@blocpower.org'}) + response_data = self._test_put(model.id, data) + self.assertEqual(response_data['value'], data['value']); + self.assertTrue( + db.session.query(ContactMethod)\ + .filter(and_( + ContactMethod.id == model.id, + ContactMethod.value == data['value']))\ + .first()) + + # Put requests can also fail in the same way as post requests, but, since + # they use the same forms, we only really need to test one. Those + # negative cases are left out for the sake of brevity. + + def test_put_contact_id(self): + """Tests /contact/method/ PUT with a new contact_id. It should 400. + """ + model = self.env.email_contact_method + data = model.get_dictionary() + data.update({'contact_id': self.env.contact.id}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 400) + + def test_put_method(self): + """Tests /contact/method/ PUT with a new method. It should 400.""" + model = self.env.email_contact_method + data = model.get_dictionary() + data.update({'method': 'phone', 'value': '5555555555'}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 400) + + def test_delete(self): + """Tests /contact/method/ DELETE.""" + model = self.env.email_contact_method + self._test_delete(model.id) -- GitLab From 0924cbdd97166761eba7b0eaa76d142769dd8357 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 19:58:58 -0400 Subject: [PATCH 35/53] Use a constant field instead of two different forms to block modifying the sales force id on the project model. --- app/controllers/project.py | 13 +++------ app/forms/project.py | 16 +--------- app/tests/test_project.py | 60 ++++++++++++++------------------------ app/views/project.py | 11 ------- 4 files changed, 27 insertions(+), 73 deletions(-) diff --git a/app/controllers/project.py b/app/controllers/project.py index 9122c42..fee6aa2 100644 --- a/app/controllers/project.py +++ b/app/controllers/project.py @@ -3,22 +3,17 @@ from werkzeug.exceptions import BadRequest from app.lib.database import db from app.controllers.base import RestController from app.models.project import Project, ProjectStateChange -from app.forms.project import CreateProjectForm, UpdateProjectForm +from app.forms.project import ProjectForm class ProjectController(RestController): """The project controller.""" Model = Project + constant_fields = ['sales_force_id'] def get_form(self, filter_data): - """Return a form to either create or edit a project.""" - try: - return { - 'create': CreateProjectForm, - 'update': UpdateProjectForm - }[filter_data.get('form')] - except KeyError: - raise BadRequest('Invalid form.') + """Return the project form.""" + return ProjectForm def log_state_change(self, model): db.session.add(ProjectStateChange( diff --git a/app/forms/project.py b/app/forms/project.py index 82e29c9..c27310a 100644 --- a/app/forms/project.py +++ b/app/forms/project.py @@ -5,7 +5,7 @@ from app.forms.base import Form from app.models.project import Project -class CreateProjectForm(Form): +class ProjectForm(Form): """A form for validating new projects.""" sales_force_id = wtf.StringField( validators=[wtf.validators.Length(max=255)]) @@ -19,17 +19,3 @@ class CreateProjectForm(Form): wtf.validators.Required(), wtf.validators.AnyOf(Project.states) ]) - - -class UpdateProjectForm(Form): - """A form for validating existing projects.""" - name = wtf.StringField( - validators=[ - wtf.validators.Required(), - wtf.validators.Length(max=255) - ]) - state = wtf.StringField( - validators=[ - wtf.validators.Required(), - wtf.validators.AnyOf(Project.states) - ]) diff --git a/app/tests/test_project.py b/app/tests/test_project.py index 2d15352..282fba1 100644 --- a/app/tests/test_project.py +++ b/app/tests/test_project.py @@ -27,14 +27,10 @@ class TestProject(RestTestCase): In addition to posting the model normally, it should create a state change model. """ - data = self._test_post( - { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending' - }, { - 'form': 'create' - }) + data = self._test_post({ + 'sales_force_id': 'test', + 'name': 'test', + 'state': 'pending'}) self.assertTrue( db.session.query(ProjectStateChange)\ .filter(and_( @@ -42,39 +38,22 @@ class TestProject(RestTestCase): ProjectStateChange.state == data['state']))\ .first()) - def test_post_bad_form(self): - """Tests /project/ POST with a bad form.""" - response = self.post(self.url, - data={'sales_force_id': 'test', 'name': 'test', 'state': 'pending'}) - self.assertEqual(response.status_code, 400) - def test_post_missing_name(self): """Tests /project/ POST with a missing name.""" response = self.post(self.url, - data={'sales_force_id': 'test', 'state': 'pending'}, - query_string={'form': 'create'}) + data={'sales_force_id': 'test', 'state': 'pending'}) self.assertEqual(response.status_code, 400) def test_post_missing_state(self): """Tests /project/ POST with a missing state.""" response = self.post(self.url, - data={'sales_force_id': 'test', 'name': 'test'}, - query_string={'form': 'create'}) + data={'sales_force_id': 'test', 'name': 'test'}) self.assertEqual(response.status_code, 400) def test_post_missing_state(self): """Tests /project/ POST with a bad state.""" response = self.post(self.url, - data={'sales_force_id': 'test', 'name': 'test', 'state': 'foo'}, - query_string={'form': 'create'}) - self.assertEqual(response.status_code, 400) - - def test_put_bad_form(self): - """Tests /project/ PUT with a bad form.""" - model = self.env.project - data = model.get_dictionary() - data.update({'name': 'foo'}) - response = self.put(self.url + str(model.id), data=data) + data={'sales_force_id': 'test', 'name': 'test', 'state': 'foo'}) self.assertEqual(response.status_code, 400) def test_put_name(self): @@ -82,14 +61,13 @@ class TestProject(RestTestCase): model = self.env.project data = model.get_dictionary() data.update({'name': 'foo'}) - response_data = self._test_put( - model.id, data, filter_data={'form': 'update'}) - self.assertEqual(response_data['name'], 'foo') + response_data = self._test_put(model.id, data) + self.assertEqual(response_data['name'], data['name']) self.assertEqual( db.session.query(Project)\ .filter(Project.id == model.id)\ .first().name, - 'foo') + data['name']) def test_put_state(self): """Tests /project/ PUT with a new state. @@ -99,14 +77,13 @@ class TestProject(RestTestCase): model = self.env.project data = model.get_dictionary() data.update({'state': 'accepted'}) - response_data = self._test_put( - model.id, data, filter_data={'form': 'update'}) - self.assertEqual(response_data['state'], 'accepted') + response_data = self._test_put(model.id, data) + self.assertEqual(response_data['state'], data['state']) self.assertEqual( db.session.query(Project)\ .filter(Project.id == model.id)\ .first().state, - 'accepted') + data['state']) self.assertTrue( db.session.query(ProjectStateChange)\ .filter(and_( @@ -119,8 +96,15 @@ class TestProject(RestTestCase): model = self.env.project data = model.get_dictionary() data.update({'state': 'foo'}) - response = self.put(self.url + str(model.id), - data=data, query_string={'form': 'update'}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 400) + + def test_put_sales_force_id(self): + """Tests /project/ PUT with a sales force id. It should 400.""" + model = self.env.project + data = model.get_dictionary() + data.update({'sales_force_id': 'foo'}) + response = self.put(self.url + str(model.id), data=data) self.assertEqual(response.status_code, 400) def test_delete(self): diff --git a/app/views/project.py b/app/views/project.py index 9ba2ad0..9764266 100644 --- a/app/views/project.py +++ b/app/views/project.py @@ -4,7 +4,6 @@ from flask import request from app.views.base import RestView from app.controllers.project import ProjectController -from app.permissions.base import FormNeed class ProjectView(RestView): @@ -13,16 +12,6 @@ class ProjectView(RestView): """Return an instance of the project controller.""" return ProjectController() - def post(self): - form = request.args.get('form') - with FormNeed('create', form): - return super(ProjectView, self).post() - - def put(self, id_): - form = request.args.get('form') - with FormNeed('update', form): - return super(ProjectView, self).put(id_) - def delete(self, id_): """Not implemented""" raise MethodNotAllowed( -- GitLab From 261943cd5284fd7c8a1702612fafa89a3f367baf Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Tue, 22 Mar 2016 20:31:40 -0400 Subject: [PATCH 36/53] Use a constant field instead of two different forms to enforce that the sales force id remains constant in the client model. --- app/controllers/client.py | 13 ++++--------- app/forms/client.py | 11 +---------- app/tests/test_client.py | 33 +++++++++------------------------ app/views/client.py | 13 ------------- 4 files changed, 14 insertions(+), 56 deletions(-) diff --git a/app/controllers/client.py b/app/controllers/client.py index 583ca26..e9ab583 100644 --- a/app/controllers/client.py +++ b/app/controllers/client.py @@ -2,19 +2,14 @@ from werkzeug.exceptions import BadRequest from app.controllers.base import RestController from app.models.client import Client -from app.forms.client import CreateClientForm, UpdateClientForm +from app.forms.client import ClientForm class ClientController(RestController): """The client controller.""" Model = Client + constant_fields = ['sales_force_id'] def get_form(self, filter_data): - """Return a form to either create or edit a client.""" - try: - return { - 'create': CreateClientForm, - 'update': UpdateClientForm - }[filter_data.get('form')] - except KeyError: - raise BadRequest('Invalid form.') + """Return the client form.""" + return ClientForm diff --git a/app/forms/client.py b/app/forms/client.py index 55d6c6d..7dc2c4a 100644 --- a/app/forms/client.py +++ b/app/forms/client.py @@ -3,7 +3,7 @@ import wtforms as wtf from app.forms.base import Form -class CreateClientForm(Form): +class ClientForm(Form): """A form for posting new clients.""" sales_force_id = wtf.StringField( validators=[wtf.validators.Length(max=255)]) @@ -12,12 +12,3 @@ class CreateClientForm(Form): wtf.validators.Required(), wtf.validators.Length(max=255) ]) - - -class UpdateClientForm(Form): - """A form for manipulating existing clients.""" - name = wtf.StringField( - validators=[ - wtf.validators.Required(), - wtf.validators.Length(max=255) - ]) diff --git a/app/tests/test_client.py b/app/tests/test_client.py index 0af9978..0dca409 100644 --- a/app/tests/test_client.py +++ b/app/tests/test_client.py @@ -22,13 +22,9 @@ class TestClient(RestTestCase): def test_post(self): """Tests /client/ POST.""" - data = self._test_post( - { - 'sales_force_id': 'test', - 'name': 'test' - }, { - 'form': 'create' - }) + data = self._test_post({ + 'sales_force_id': 'test', + 'name': 'test'}) self.assertTrue( db.session.query(Client)\ .filter(and_( @@ -37,18 +33,10 @@ class TestClient(RestTestCase): Client.name == data['name']))\ .first()) - def test_post_bad_form(self): - """Tests /client/ POST with a bad form. It should 400.""" - response = self.post(self.url, - data={'sales_force_id': 'test', 'name': 'test'}, - query_string={'form': 'update'}) - self.assertEqual(response.status_code, 400) - def test_post_missing_name(self): """Tests /client/ POST with a missing name. It should 400.""" response = self.post(self.url, - data={'sales_force_id': 'test'}, - query_string={'form': 'create'}) + data={'sales_force_id': 'test'}) self.assertEqual(response.status_code, 400) def test_put(self): @@ -56,7 +44,7 @@ class TestClient(RestTestCase): model = self.env.client data = model.get_dictionary() data.update({'name': 'foo'}) - response_data = self._test_put(model.id, data, {'form': 'update'}) + response_data = self._test_put(model.id, data) self.assertEqual(response_data['name'], 'foo') self.assertTrue( db.session.query(Client)\ @@ -65,15 +53,12 @@ class TestClient(RestTestCase): Client.name == data['name']))\ .first()) - def test_put_bad_form(self): - """Tests /client/ PUT with a bad form. It should 400.""" + def test_put_sales_force_id(self): + """Tests /client/ PUT with a sales force id. It should 400.""" model = self.env.client data = model.get_dictionary() - data.update({'name': 'foo'}) - response = self.put( - self.url + str(model.id), - data=data, - query_string={'form': 'create'}) + data.update({'sales_force_id': 'foo'}) + response = self.put(self.url + str(model.id), data=data) self.assertEqual(response.status_code, 400) def test_delete(self): diff --git a/app/views/client.py b/app/views/client.py index 0d69c58..5536dc8 100644 --- a/app/views/client.py +++ b/app/views/client.py @@ -3,7 +3,6 @@ from werkzeug.exceptions import MethodNotAllowed from flask import request from app.views.base import RestView from app.controllers.client import ClientController -from app.permissions.base import FormNeed class ClientView(RestView): @@ -12,18 +11,6 @@ class ClientView(RestView): """Return an instance of the client controller.""" return ClientController() - def post(self): - """Checks for a create form before posting.""" - form = request.args.get('form') - with FormNeed('create', form): - return super(ClientView, self).post() - - def put(self, id_): - """Checks for an update form before putting.""" - form = request.args.get('form') - with FormNeed('update', form): - return super(ClientView, self).put(id_) - def delete(self, id_): """Not implemented.""" raise MethodNotAllowed( -- GitLab From aa1a5422249eb10932e45ee51e36305be859e388 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:36:35 -0400 Subject: [PATCH 37/53] Add a m2m model for the project-contact relationship. --- app/models/contact.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/contact.py b/app/models/contact.py index 96254ba..f9294a0 100644 --- a/app/models/contact.py +++ b/app/models/contact.py @@ -25,3 +25,11 @@ class ContactMethod(Model, Tracked, db.Model): db.Integer, db.ForeignKey('contact.id'), nullable=False) method = db.Column(db.Enum(*methods), nullable=False) value = db.Column(db.Unicode(255), nullable=False) + + +class ProjectContact(Model, Tracked, db.Model): + """A m2m relationship between project and contact.""" + project_id = db.Column( + db.Integer, db.ForeignKey('project.id'), nullable=False) + contact_id = db.Column( + db.Integer, db.ForeignKey('contact.id'), nullable=False) -- GitLab From 8119f5946c754d3f99b1f40013143e92c38c3a8a Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:36:48 -0400 Subject: [PATCH 38/53] Add a form for the project contact model. --- app/forms/contact.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/forms/contact.py b/app/forms/contact.py index 87739f9..fddee0a 100644 --- a/app/forms/contact.py +++ b/app/forms/contact.py @@ -44,3 +44,9 @@ class ContactMethodForm(Form): 'fax': [validators.phone] }) ]) + + +class ProjectContactForm(Form): + """A form for validating m2m relationships between projects and contacts.""" + project_id = wtf.IntegerField(validators=[wtf.validators.Required()]) + contact_id = wtf.IntegerField(validators=[wtf.validators.Required()]) -- GitLab From 4910f0eeb0b0050ef9f111fe634b9a092068aca5 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:37:04 -0400 Subject: [PATCH 39/53] Add an instance of the project-contact m2m model to the test enviornment. --- app/tests/environment.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/tests/environment.py b/app/tests/environment.py index cc1f21b..9911e94 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -12,7 +12,7 @@ from app.lib.database import db, commit from app.models.application import Application from app.models.project import Project from app.models.client import Client -from app.models.contact import Contact, ContactMethod +from app.models.contact import Contact, ContactMethod, ProjectContact class Environment(object): @@ -103,3 +103,9 @@ class Environment(object): contact_id=self.contact.id, method='fax', value='5555555555')) + + @property + def project_contact(self): + return self.add(ProjectContact( + project_id=self.project.id, + contact_id=self.contact.id)) -- GitLab From ca0b38d7807bc216a11483df6806d6d041200fb8 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:37:14 -0400 Subject: [PATCH 40/53] Add a project-contact controller. --- app/controllers/contact.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/app/controllers/contact.py b/app/controllers/contact.py index 8f1bb62..7aec357 100644 --- a/app/controllers/contact.py +++ b/app/controllers/contact.py @@ -1,9 +1,9 @@ """Controllers for managing contacts.""" from werkzeug.exceptions import BadRequest from app.controllers.base import RestController -from app.models.contact import Contact, ContactMethod +from app.models.contact import Contact, ContactMethod, ProjectContact from app.forms.contact import ( - CreateContactForm, UpdateContactForm, ContactMethodForm) + CreateContactForm, UpdateContactForm, ContactMethodForm, ProjectContactForm) class ContactController(RestController): @@ -24,9 +24,17 @@ class ContactController(RestController): class ContactMethodController(RestController): """The contact method controller.""" Model = ContactMethod - constant_fields = ['contact_id', 'method'] def get_form(self, filter_data): """Return the contact method form.""" return ContactMethodForm + + +class ProjectContactController(RestController): + """The controller for the project-contact m2m relationship.""" + Model = ProjectContact + + def get_form(self, filter_data): + """Return the project contact form.""" + return ProjectContactForm -- GitLab From 6adcb9d72dd7e0ba4eaaf3a89bd1dc9eb24cc0bc Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:37:31 -0400 Subject: [PATCH 41/53] Add a view for the project-contact m2m relationship. --- app/views/contact.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/views/contact.py b/app/views/contact.py index 0841b74..bf7378c 100644 --- a/app/views/contact.py +++ b/app/views/contact.py @@ -2,7 +2,8 @@ from werkzeug.exceptions import MethodNotAllowed from flask import request from app.views.base import RestView -from app.controllers.contact import ContactController, ContactMethodController +from app.controllers.contact import ( + ContactController, ContactMethodController, ProjectContactController) from app.permissions.base import FormNeed @@ -32,3 +33,18 @@ class ContactMethodView(RestView): def get_controller(self): """Return an instance of the contact method controller.""" return ContactMethodController() + + +class ProjectContactView(RestView): + """A view for the project-contact m2m relationship.""" + route_base = '/contact/project/' + + def get_controller(self): + """Return an instance of the project contact controller.""" + return ProjectContactController() + + def put(self, id_): + """Not implemented.""" + raise MethodNotAllowed( + 'Do not modify existing m2m relationships. Delete this and ' + + 'create a new one instead.') -- GitLab From b485cd0a6238743df20b8972a3aa658274091765 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:37:47 -0400 Subject: [PATCH 42/53] Register the project contact view. --- app/views/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/__init__.py b/app/views/__init__.py index f0b04e2..3582222 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -18,3 +18,4 @@ def register(app): contact.ContactView.register(app) contact.ContactMethodView.register(app) + contact.ProjectContactView.register(app) -- GitLab From 00bef62694f82a83ae7a91ec63ac4fdbee368976 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:38:09 -0400 Subject: [PATCH 43/53] Add unit tests for the /contact/project/ endpoint. --- app/tests/test_contact.py | 66 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/app/tests/test_contact.py b/app/tests/test_contact.py index fdc4964..c562565 100644 --- a/app/tests/test_contact.py +++ b/app/tests/test_contact.py @@ -2,7 +2,7 @@ from sqlalchemy import and_ from app.lib.database import db from app.tests.base import RestTestCase -from app.models.contact import Contact, ContactMethod +from app.models.contact import Contact, ContactMethod, ProjectContact class TestContact(RestTestCase): @@ -81,7 +81,7 @@ class TestContact(RestTestCase): self._test_delete(model.id) -class TestContact(RestTestCase): +class TestContactMethod(RestTestCase): """Tests the /contact/method/ endpoints.""" url = '/contact/method/' Model = ContactMethod @@ -277,3 +277,65 @@ class TestContact(RestTestCase): """Tests /contact/method/ DELETE.""" model = self.env.email_contact_method self._test_delete(model.id) + + +class TestProjectContact(RestTestCase): + """Tests the /contact/project/ endpoints.""" + url = '/contact/project/' + Model = ProjectContact + + def test_index(self): + """Tests /contact/project/ GET.""" + model = self.env.project_contact + self._test_index() + + def test_get(self): + """Tests /contact/project/ GET.""" + model = self.env.project_contact + self._test_get(model.id) + + def test_post(self): + """Tests /contact/project/ POST.""" + data = { + 'project_id': self.env.project.id, + 'contact_id': self.env.contact.id + } + response_data = self._test_post(data) + self.assertTrue( + db.session.query(ProjectContact)\ + .filter(and_( + ProjectContact.id == response_data['id'], + ProjectContact.project_id == data['project_id'], + ProjectContact.contact_id == data['contact_id']))\ + .first()) + + def test_post_bad_contact(self): + """Tests /contact/project/ POST with a bad contact id. It should 400.""" + data = { + 'project_id': self.env.project.id, + 'contact_id': 1 + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_bad_project(self): + """Tests /contact/project/ POST with a bad project id. It should 400.""" + data = { + 'project_id': 1, + 'contact_id': self.env.contact.id + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_put(self): + """Tests /contact/project/ PUT. It should 405.""" + model = self.env.project_contact + data = model.get_dictionary() + data.update({'project_id': self.env.project.id}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 405) + + def test_delete(self): + """Tests /contact/project/ DELETE.""" + model = self.env.project_contact + self._test_delete(model.id) -- GitLab From 0cf04c7affd1b3bf62c82a0eb3b4a08ccd103a97 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:46:21 -0400 Subject: [PATCH 44/53] Add a client-contact m2m relationship. --- app/models/contact.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/contact.py b/app/models/contact.py index f9294a0..d0d1498 100644 --- a/app/models/contact.py +++ b/app/models/contact.py @@ -33,3 +33,11 @@ class ProjectContact(Model, Tracked, db.Model): db.Integer, db.ForeignKey('project.id'), nullable=False) contact_id = db.Column( db.Integer, db.ForeignKey('contact.id'), nullable=False) + + +class ClientContact(Model, Tracked, db.Model): + """A m2m relationship between client and contact.""" + client_id = db.Column( + db.Integer, db.ForeignKey('client.id'), nullable=False) + contact_id = db.Column( + db.Integer, db.ForeignKey('contact.id'), nullable=False) -- GitLab From 04c8aee49c6c7b226b72190b0d2d04c948b95902 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:46:40 -0400 Subject: [PATCH 45/53] Add the client contact model to the test environment. --- app/tests/environment.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/tests/environment.py b/app/tests/environment.py index 9911e94..d81ee9d 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -12,7 +12,8 @@ from app.lib.database import db, commit from app.models.application import Application from app.models.project import Project from app.models.client import Client -from app.models.contact import Contact, ContactMethod, ProjectContact +from app.models.contact import ( + Contact, ContactMethod, ProjectContact, ClientContact) class Environment(object): @@ -109,3 +110,9 @@ class Environment(object): return self.add(ProjectContact( project_id=self.project.id, contact_id=self.contact.id)) + + @property + def client_contact(self): + return self.add(ClientContact( + client_id=self.client.id, + contact_id=self.contact.id)) -- GitLab From b9de059e2ffdc899788b21152fe1d3c034f91edf Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:46:58 -0400 Subject: [PATCH 46/53] Add a form for the client-contact m2m relationship. --- app/forms/contact.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/forms/contact.py b/app/forms/contact.py index fddee0a..3c7d36a 100644 --- a/app/forms/contact.py +++ b/app/forms/contact.py @@ -50,3 +50,9 @@ class ProjectContactForm(Form): """A form for validating m2m relationships between projects and contacts.""" project_id = wtf.IntegerField(validators=[wtf.validators.Required()]) contact_id = wtf.IntegerField(validators=[wtf.validators.Required()]) + + +class ClientContactForm(Form): + """A form for validating m2m relationships between clients and contacts.""" + client_id = wtf.IntegerField(validators=[wtf.validators.Required()]) + contact_id = wtf.IntegerField(validators=[wtf.validators.Required()]) -- GitLab From b23a417dfa661a46e8c9769aab8d0473a4535315 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:47:13 -0400 Subject: [PATCH 47/53] Add a controller for the client-contact m2m relationship. --- app/controllers/contact.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/controllers/contact.py b/app/controllers/contact.py index 7aec357..bd7b13f 100644 --- a/app/controllers/contact.py +++ b/app/controllers/contact.py @@ -1,9 +1,11 @@ """Controllers for managing contacts.""" from werkzeug.exceptions import BadRequest from app.controllers.base import RestController -from app.models.contact import Contact, ContactMethod, ProjectContact +from app.models.contact import ( + Contact, ContactMethod, ProjectContact, ClientContact) from app.forms.contact import ( - CreateContactForm, UpdateContactForm, ContactMethodForm, ProjectContactForm) + CreateContactForm, UpdateContactForm, ContactMethodForm, ProjectContactForm, + ClientContactForm) class ContactController(RestController): @@ -38,3 +40,12 @@ class ProjectContactController(RestController): def get_form(self, filter_data): """Return the project contact form.""" return ProjectContactForm + + +class ClientContactController(RestController): + """The controller for the client-contact m2m relationship.""" + Model = ClientContact + + def get_form(self, filter_data): + """Return the client contact form.""" + return ClientContactForm -- GitLab From e5803ee174d643dddac793aea3fb64b15f700af8 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:47:26 -0400 Subject: [PATCH 48/53] Add a view for the client-contact m2m relationship. --- app/views/contact.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/app/views/contact.py b/app/views/contact.py index bf7378c..650869e 100644 --- a/app/views/contact.py +++ b/app/views/contact.py @@ -3,7 +3,8 @@ from werkzeug.exceptions import MethodNotAllowed from flask import request from app.views.base import RestView from app.controllers.contact import ( - ContactController, ContactMethodController, ProjectContactController) + ContactController, ContactMethodController, ProjectContactController, + ClientContactController) from app.permissions.base import FormNeed @@ -48,3 +49,18 @@ class ProjectContactView(RestView): raise MethodNotAllowed( 'Do not modify existing m2m relationships. Delete this and ' + 'create a new one instead.') + + +class ClientContactView(RestView): + """A view for the client-contact m2m relationship.""" + route_base = '/contact/client/' + + def get_controller(self): + """Return an instance of the client contact controller.""" + return ClientContactController() + + def put(self, id_): + """Not implemented.""" + raise MethodNotAllowed( + 'Do not modify existing m2m relationships. Delete this and ' + + 'create a new one instead.') -- GitLab From 1322c8f2cfba9bedb1a754db60c642a6dbf2de2f Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:47:36 -0400 Subject: [PATCH 49/53] Register the client-contact view. --- app/views/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/__init__.py b/app/views/__init__.py index 3582222..da6f2e0 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -19,3 +19,4 @@ def register(app): contact.ContactView.register(app) contact.ContactMethodView.register(app) contact.ProjectContactView.register(app) + contact.ClientContactView.register(app) -- GitLab From b5a791d45a7e93140c679d6dac37a42b1df73818 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 11:47:54 -0400 Subject: [PATCH 50/53] Add unit tests for the /contact/client/ endpoints. --- app/tests/test_contact.py | 65 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/app/tests/test_contact.py b/app/tests/test_contact.py index c562565..f98043e 100644 --- a/app/tests/test_contact.py +++ b/app/tests/test_contact.py @@ -2,7 +2,8 @@ from sqlalchemy import and_ from app.lib.database import db from app.tests.base import RestTestCase -from app.models.contact import Contact, ContactMethod, ProjectContact +from app.models.contact import ( + Contact, ContactMethod, ProjectContact, ClientContact) class TestContact(RestTestCase): @@ -339,3 +340,65 @@ class TestProjectContact(RestTestCase): """Tests /contact/project/ DELETE.""" model = self.env.project_contact self._test_delete(model.id) + + +class TestClientContact(RestTestCase): + """Tests the /contact/client/ endpoints.""" + url = '/contact/client/' + Model = ClientContact + + def test_index(self): + """Tests /contact/client/ GET.""" + model = self.env.client_contact + self._test_index() + + def test_get(self): + """Tests /contact/client/ GET.""" + model = self.env.client_contact + self._test_get(model.id) + + def test_post(self): + """Tests /contact/client/ POST.""" + data = { + 'client_id': self.env.client.id, + 'contact_id': self.env.contact.id + } + response_data = self._test_post(data) + self.assertTrue( + db.session.query(ClientContact)\ + .filter(and_( + ClientContact.id == response_data['id'], + ClientContact.client_id == data['client_id'], + ClientContact.contact_id == data['contact_id']))\ + .first()) + + def test_post_bad_contact(self): + """Tests /contact/client/ POST with a bad contact id. It should 400.""" + data = { + 'client_id': self.env.project.id, + 'contact_id': 1 + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_bad_client(self): + """Tests /contact/client/ POST with a bad client id. It should 400.""" + data = { + 'client_id': 1, + 'contact_id': self.env.contact.id + } + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_put(self): + """Tests /contact/client/ PUT. It should 405.""" + model = self.env.client_contact + data = model.get_dictionary() + data.update({'client_id': self.env.client.id}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 405) + + def test_delete(self): + """Tests /contact/client/ DELETE.""" + model = self.env.client_contact + self._test_delete(model.id) -- GitLab From 62a063aec3b10f823e3f02fa3c3b8720a0190bbe Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 12:27:20 -0400 Subject: [PATCH 51/53] Add a client_id field to the project model. --- app/controllers/project.py | 2 +- app/forms/project.py | 1 + app/models/project.py | 2 ++ app/tests/environment.py | 11 +++++--- app/tests/test_project.py | 52 +++++++++++++++++++++++++++++++++----- 5 files changed, 56 insertions(+), 12 deletions(-) diff --git a/app/controllers/project.py b/app/controllers/project.py index fee6aa2..f79edea 100644 --- a/app/controllers/project.py +++ b/app/controllers/project.py @@ -9,7 +9,7 @@ from app.forms.project import ProjectForm class ProjectController(RestController): """The project controller.""" Model = Project - constant_fields = ['sales_force_id'] + constant_fields = ['sales_force_id', 'client_id'] def get_form(self, filter_data): """Return the project form.""" diff --git a/app/forms/project.py b/app/forms/project.py index c27310a..787f064 100644 --- a/app/forms/project.py +++ b/app/forms/project.py @@ -9,6 +9,7 @@ class ProjectForm(Form): """A form for validating new projects.""" sales_force_id = wtf.StringField( validators=[wtf.validators.Length(max=255)]) + client_id = wtf.IntegerField(validators=[wtf.validators.Required()]) name = wtf.StringField( validators=[ wtf.validators.Required(), diff --git a/app/models/project.py b/app/models/project.py index 1d9eb5b..d572634 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -34,6 +34,8 @@ class Project(Model, Tracked, SalesForce, db.Model): 'constructed', 'verified', 'paid' ] + client_id = db.Column( + db.Integer, db.ForeignKey('client.id'), nullable=False) name = db.Column(db.Unicode(255), nullable=False) state = db.Column(db.Enum(*states), default='pending', nullable=False) diff --git a/app/tests/environment.py b/app/tests/environment.py index d81ee9d..34db137 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -65,14 +65,17 @@ class Environment(object): 'grant_type': 'refresh_token' }).json()['id_token'] - @property - def project(self): - return self.add(Project(sales_force_id='test', name='test')) - @property def client(self): return self.add(Client(sales_force_id='test', name='test')) + @property + def project(self): + return self.add(Project( + sales_force_id='test', + client_id=self.client.id, + name='test')) + @property def contact(self): return self.add(Contact(sales_force_id='test', name='test')) diff --git a/app/tests/test_project.py b/app/tests/test_project.py index 282fba1..fbc4b85 100644 --- a/app/tests/test_project.py +++ b/app/tests/test_project.py @@ -27,33 +27,63 @@ class TestProject(RestTestCase): In addition to posting the model normally, it should create a state change model. """ - data = self._test_post({ + data = { 'sales_force_id': 'test', + 'client_id': self.env.client.id, 'name': 'test', - 'state': 'pending'}) + 'state': 'pending'} + response_data = self._test_post(data) + self.assertTrue( + db.session.query(Project)\ + .filter(and_( + Project.id == response_data['id'], + Project.sales_force_id == data['sales_force_id'], + Project.name == data['name'], + Project.state == data['state']))\ + .first()) self.assertTrue( db.session.query(ProjectStateChange)\ .filter(and_( - ProjectStateChange.project_id == data['id'], + ProjectStateChange.project_id == response_data['id'], ProjectStateChange.state == data['state']))\ .first()) def test_post_missing_name(self): """Tests /project/ POST with a missing name.""" response = self.post(self.url, - data={'sales_force_id': 'test', 'state': 'pending'}) + data={ + 'sales_force_id': 'test', + 'client_id': self.env.client.id, + 'state': 'pending'}) self.assertEqual(response.status_code, 400) def test_post_missing_state(self): """Tests /project/ POST with a missing state.""" response = self.post(self.url, - data={'sales_force_id': 'test', 'name': 'test'}) + data={ + 'sales_force_id': 'test', + 'client_id': self.env.client.id, + 'name': 'test'}) self.assertEqual(response.status_code, 400) - def test_post_missing_state(self): + def test_post_bad_state(self): """Tests /project/ POST with a bad state.""" response = self.post(self.url, - data={'sales_force_id': 'test', 'name': 'test', 'state': 'foo'}) + data={ + 'sales_force_id': 'test', + 'client_id': self.env.client.id, + 'name': 'test', + 'state': 'foo'}) + self.assertEqual(response.status_code, 400) + + def test_post_bad_client_id(self): + """Tests /project/ POST with a bad client id.""" + response = self.post(self.url, + data={ + 'sales_force_id': 'test', + 'client_id': 1, + 'name': 'test', + 'state': 'pending'}) self.assertEqual(response.status_code, 400) def test_put_name(self): @@ -107,6 +137,14 @@ class TestProject(RestTestCase): response = self.put(self.url + str(model.id), data=data) self.assertEqual(response.status_code, 400) + def test_put_client_id(self): + """Tests /project/ PUT with a client id. It should 400.""" + model = self.env.project + data = model.get_dictionary() + data.update({'client_id': self.env.client.id}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 400) + def test_delete(self): """Tests /project/ DELETE. It should 405.""" model = self.env.project -- GitLab From bcbbaee93f160e9a71ab2919daec380d204ed514 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 17:20:03 -0400 Subject: [PATCH 52/53] Remove the client contact m2m relationship. --- app/controllers/contact.py | 15 ++------- app/forms/contact.py | 6 ---- app/models/contact.py | 8 ----- app/tests/environment.py | 9 +----- app/tests/test_contact.py | 65 +------------------------------------- app/views/__init__.py | 1 - app/views/contact.py | 18 +---------- 7 files changed, 5 insertions(+), 117 deletions(-) diff --git a/app/controllers/contact.py b/app/controllers/contact.py index bd7b13f..7aec357 100644 --- a/app/controllers/contact.py +++ b/app/controllers/contact.py @@ -1,11 +1,9 @@ """Controllers for managing contacts.""" from werkzeug.exceptions import BadRequest from app.controllers.base import RestController -from app.models.contact import ( - Contact, ContactMethod, ProjectContact, ClientContact) +from app.models.contact import Contact, ContactMethod, ProjectContact from app.forms.contact import ( - CreateContactForm, UpdateContactForm, ContactMethodForm, ProjectContactForm, - ClientContactForm) + CreateContactForm, UpdateContactForm, ContactMethodForm, ProjectContactForm) class ContactController(RestController): @@ -40,12 +38,3 @@ class ProjectContactController(RestController): def get_form(self, filter_data): """Return the project contact form.""" return ProjectContactForm - - -class ClientContactController(RestController): - """The controller for the client-contact m2m relationship.""" - Model = ClientContact - - def get_form(self, filter_data): - """Return the client contact form.""" - return ClientContactForm diff --git a/app/forms/contact.py b/app/forms/contact.py index 3c7d36a..fddee0a 100644 --- a/app/forms/contact.py +++ b/app/forms/contact.py @@ -50,9 +50,3 @@ class ProjectContactForm(Form): """A form for validating m2m relationships between projects and contacts.""" project_id = wtf.IntegerField(validators=[wtf.validators.Required()]) contact_id = wtf.IntegerField(validators=[wtf.validators.Required()]) - - -class ClientContactForm(Form): - """A form for validating m2m relationships between clients and contacts.""" - client_id = wtf.IntegerField(validators=[wtf.validators.Required()]) - contact_id = wtf.IntegerField(validators=[wtf.validators.Required()]) diff --git a/app/models/contact.py b/app/models/contact.py index d0d1498..f9294a0 100644 --- a/app/models/contact.py +++ b/app/models/contact.py @@ -33,11 +33,3 @@ class ProjectContact(Model, Tracked, db.Model): db.Integer, db.ForeignKey('project.id'), nullable=False) contact_id = db.Column( db.Integer, db.ForeignKey('contact.id'), nullable=False) - - -class ClientContact(Model, Tracked, db.Model): - """A m2m relationship between client and contact.""" - client_id = db.Column( - db.Integer, db.ForeignKey('client.id'), nullable=False) - contact_id = db.Column( - db.Integer, db.ForeignKey('contact.id'), nullable=False) diff --git a/app/tests/environment.py b/app/tests/environment.py index 34db137..145b0a4 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -12,8 +12,7 @@ from app.lib.database import db, commit from app.models.application import Application from app.models.project import Project from app.models.client import Client -from app.models.contact import ( - Contact, ContactMethod, ProjectContact, ClientContact) +from app.models.contact import Contact, ContactMethod, ProjectContact class Environment(object): @@ -113,9 +112,3 @@ class Environment(object): return self.add(ProjectContact( project_id=self.project.id, contact_id=self.contact.id)) - - @property - def client_contact(self): - return self.add(ClientContact( - client_id=self.client.id, - contact_id=self.contact.id)) diff --git a/app/tests/test_contact.py b/app/tests/test_contact.py index f98043e..c562565 100644 --- a/app/tests/test_contact.py +++ b/app/tests/test_contact.py @@ -2,8 +2,7 @@ from sqlalchemy import and_ from app.lib.database import db from app.tests.base import RestTestCase -from app.models.contact import ( - Contact, ContactMethod, ProjectContact, ClientContact) +from app.models.contact import Contact, ContactMethod, ProjectContact class TestContact(RestTestCase): @@ -340,65 +339,3 @@ class TestProjectContact(RestTestCase): """Tests /contact/project/ DELETE.""" model = self.env.project_contact self._test_delete(model.id) - - -class TestClientContact(RestTestCase): - """Tests the /contact/client/ endpoints.""" - url = '/contact/client/' - Model = ClientContact - - def test_index(self): - """Tests /contact/client/ GET.""" - model = self.env.client_contact - self._test_index() - - def test_get(self): - """Tests /contact/client/ GET.""" - model = self.env.client_contact - self._test_get(model.id) - - def test_post(self): - """Tests /contact/client/ POST.""" - data = { - 'client_id': self.env.client.id, - 'contact_id': self.env.contact.id - } - response_data = self._test_post(data) - self.assertTrue( - db.session.query(ClientContact)\ - .filter(and_( - ClientContact.id == response_data['id'], - ClientContact.client_id == data['client_id'], - ClientContact.contact_id == data['contact_id']))\ - .first()) - - def test_post_bad_contact(self): - """Tests /contact/client/ POST with a bad contact id. It should 400.""" - data = { - 'client_id': self.env.project.id, - 'contact_id': 1 - } - response = self.post(self.url, data=data) - self.assertEqual(response.status_code, 400) - - def test_post_bad_client(self): - """Tests /contact/client/ POST with a bad client id. It should 400.""" - data = { - 'client_id': 1, - 'contact_id': self.env.contact.id - } - response = self.post(self.url, data=data) - self.assertEqual(response.status_code, 400) - - def test_put(self): - """Tests /contact/client/ PUT. It should 405.""" - model = self.env.client_contact - data = model.get_dictionary() - data.update({'client_id': self.env.client.id}) - response = self.put(self.url + str(model.id), data=data) - self.assertEqual(response.status_code, 405) - - def test_delete(self): - """Tests /contact/client/ DELETE.""" - model = self.env.client_contact - self._test_delete(model.id) diff --git a/app/views/__init__.py b/app/views/__init__.py index da6f2e0..3582222 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -19,4 +19,3 @@ def register(app): contact.ContactView.register(app) contact.ContactMethodView.register(app) contact.ProjectContactView.register(app) - contact.ClientContactView.register(app) diff --git a/app/views/contact.py b/app/views/contact.py index 650869e..bf7378c 100644 --- a/app/views/contact.py +++ b/app/views/contact.py @@ -3,8 +3,7 @@ from werkzeug.exceptions import MethodNotAllowed from flask import request from app.views.base import RestView from app.controllers.contact import ( - ContactController, ContactMethodController, ProjectContactController, - ClientContactController) + ContactController, ContactMethodController, ProjectContactController) from app.permissions.base import FormNeed @@ -49,18 +48,3 @@ class ProjectContactView(RestView): raise MethodNotAllowed( 'Do not modify existing m2m relationships. Delete this and ' + 'create a new one instead.') - - -class ClientContactView(RestView): - """A view for the client-contact m2m relationship.""" - route_base = '/contact/client/' - - def get_controller(self): - """Return an instance of the client contact controller.""" - return ClientContactController() - - def put(self, id_): - """Not implemented.""" - raise MethodNotAllowed( - 'Do not modify existing m2m relationships. Delete this and ' + - 'create a new one instead.') -- GitLab From a9fe4e1b432bb56d329fa3f7efed882378f29c18 Mon Sep 17 00:00:00 2001 From: astex <0astex@gmail.com> Date: Wed, 23 Mar 2016 17:26:26 -0400 Subject: [PATCH 53/53] Use a constant field for sales_force_id instead of two separate forms. --- app/controllers/contact.py | 14 ++++---------- app/forms/contact.py | 13 ++----------- app/forms/project.py | 2 +- app/tests/test_contact.py | 26 +++++++------------------- app/views/contact.py | 12 ------------ 5 files changed, 14 insertions(+), 53 deletions(-) diff --git a/app/controllers/contact.py b/app/controllers/contact.py index 7aec357..9e337d9 100644 --- a/app/controllers/contact.py +++ b/app/controllers/contact.py @@ -2,23 +2,17 @@ from werkzeug.exceptions import BadRequest from app.controllers.base import RestController from app.models.contact import Contact, ContactMethod, ProjectContact -from app.forms.contact import ( - CreateContactForm, UpdateContactForm, ContactMethodForm, ProjectContactForm) +from app.forms.contact import ContactForm, ContactMethodForm, ProjectContactForm class ContactController(RestController): """The contact controller.""" Model = Contact + constant_fields = ['sales_force_id'] def get_form(self, filter_data): - """Return a form to either create or edit a contact.""" - try: - return { - 'create': CreateContactForm, - 'update': UpdateContactForm - }[filter_data.get('form')] - except KeyError: - raise BadRequest('Invalid form.') + """Return the contact form.""" + return ContactForm class ContactMethodController(RestController): diff --git a/app/forms/contact.py b/app/forms/contact.py index fddee0a..2ccc7b9 100644 --- a/app/forms/contact.py +++ b/app/forms/contact.py @@ -6,8 +6,8 @@ from app.forms.base import Form from app.models.contact import ContactMethod -class CreateContactForm(Form): - """A form to create new contacts.""" +class ContactForm(Form): + """A form for validating contacts.""" sales_force_id = wtf.StringField( validators=[wtf.validators.Length(max=255)]) name = wtf.StringField( @@ -17,15 +17,6 @@ class CreateContactForm(Form): ]) -class UpdateContactForm(Form): - """A form for manipulating existing contacts.""" - name = wtf.StringField( - validators=[ - wtf.validators.Required(), - wtf.validators.Length(max=255) - ]) - - class ContactMethodForm(Form): """A form for validating contact methods.""" contact_id = wtf.IntegerField(validators=[wtf.validators.Required()]) diff --git a/app/forms/project.py b/app/forms/project.py index 787f064..e56e9e0 100644 --- a/app/forms/project.py +++ b/app/forms/project.py @@ -6,7 +6,7 @@ from app.models.project import Project class ProjectForm(Form): - """A form for validating new projects.""" + """A form for validating projects.""" sales_force_id = wtf.StringField( validators=[wtf.validators.Length(max=255)]) client_id = wtf.IntegerField(validators=[wtf.validators.Required()]) diff --git a/app/tests/test_contact.py b/app/tests/test_contact.py index c562565..2be613b 100644 --- a/app/tests/test_contact.py +++ b/app/tests/test_contact.py @@ -26,7 +26,7 @@ class TestContact(RestTestCase): 'sales_force_id': 'test', 'name': 'test' } - response_data = self._test_post(data, {'form': 'create'}) + response_data = self._test_post(data) self.assertTrue( db.session.query(Contact)\ .filter(and_( @@ -35,18 +35,9 @@ class TestContact(RestTestCase): Contact.name == data['name']))\ .first()) - def test_post_bad_form(self): - """Tests /contact/ POST with a bad form. It should 400.""" - response = self.post(self.url, - data={'sales_force_id': 'test', 'name': 'test'}, - query_string={'form': 'update'}) - self.assertEqual(response.status_code, 400) - def test_post_missing_name(self): """Tests /contact/ POST with a missing name. It should 400.""" - response = self.post(self.url, - data={'sales_force_id': 'test'}, - query_string={'form': 'create'}) + response = self.post(self.url, data={'sales_force_id': 'test'}) self.assertEqual(response.status_code, 400) def test_put(self): @@ -54,7 +45,7 @@ class TestContact(RestTestCase): model = self.env.contact data = model.get_dictionary() data.update({'name': 'foo'}) - response_data = self._test_put(model.id, data, {'form': 'update'}) + response_data = self._test_put(model.id, data) self.assertEqual(response_data['name'], 'foo'); self.assertTrue( db.session.query(Contact)\ @@ -64,15 +55,12 @@ class TestContact(RestTestCase): ))\ .first()) - def test_put_bad_form(self): - """Tests /contact/ PUT with a bad form. It should 400.""" + def test_put_sales_force_id(self): + """Tests /contact/ PUT with a sales force id. It should 400.""" model = self.env.contact data = model.get_dictionary() - data.update({'name': 'foo'}) - response = self.put( - self.url + str(model.id), - data=data, - query_string={'form': 'create'}) + data.update({'sales_force_id': 'foo'}) + response = self.put(self.url + str(model.id), data=data) self.assertEqual(response.status_code, 400) def test_delete(self): diff --git a/app/views/contact.py b/app/views/contact.py index bf7378c..88be9ab 100644 --- a/app/views/contact.py +++ b/app/views/contact.py @@ -13,18 +13,6 @@ class ContactView(RestView): """Return an instance of the contact controller.""" return ContactController() - def post(self): - """Checks for a create form before posting.""" - form = request.args.get('form') - with FormNeed('create', form): - return super(ContactView, self).post() - - def put(self, id_): - """Checks for an update form before putting.""" - form = request.args.get('form') - with FormNeed('update', form): - return super(ContactView, self).put(id_) - class ContactMethodView(RestView): """The contact method view.""" -- GitLab