diff --git a/app/config/test.default.py b/app/config/test.default.py index 81e4fb6dab6b91716bc1df3bafec7e718ebc9dd4..6c28d8e4df4b5ff7db794912e6eb5f31a0837742 100644 --- a/app/config/test.default.py +++ b/app/config/test.default.py @@ -23,7 +23,7 @@ APP_CACHE_EXPIRY = 60 * 60 # One hour. # A client with the project_post_salesforce role. SALESFORCE_KEY = '$SALESFORCE_KEY' -SALESFORCE_SECRET = '$SALESFORCE_SECRET' +SALESFORCE_REFERRER = '$SALESFORCE_REFERRER' # Auth GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' diff --git a/app/controllers/document.py b/app/controllers/document.py new file mode 100644 index 0000000000000000000000000000000000000000..1c87f0bbc55aa70f59168108ecfa00880d35378b --- /dev/null +++ b/app/controllers/document.py @@ -0,0 +1,19 @@ +"""Controllers for managing documents.""" +from app.controllers.base import RestController +from app.models.document import DocumentSlot +from app.forms.document import DocumentSlotForm + + +class DocumentSlotController(RestController): + """A controller for managing the m2m relationship between documents and + projects. + """ + Model = DocumentSlot + constant_fields = ['project_id', 'document_uuid', 'role'] + filters = { + 'project_id': lambda d: DocumentSlot.project_id == d['project_id'] + } + + def get_form(self, filter_data): + """Return the document slot form.""" + return DocumentSlotForm diff --git a/app/forms/document.py b/app/forms/document.py new file mode 100644 index 0000000000000000000000000000000000000000..5223348262d6574ebf20c174b1401feef60e78f5 --- /dev/null +++ b/app/forms/document.py @@ -0,0 +1,13 @@ +"""Forms for handling associated documents.""" +import wtforms as wtf +from app.forms import fields +from app.forms.base import Form + + +class DocumentSlotForm(Form): + """A form for the m2m relationship between documents and projects.""" + project_id = wtf.IntegerField(validators=[wtf.validators.Required()]) + document_uuid = fields.UUID(validators=[wtf.validators.Required()]) + role = wtf.StringField(validators=[ + wtf.validators.Length(max=255), + wtf.validators.Required()]) diff --git a/app/models/document.py b/app/models/document.py new file mode 100644 index 0000000000000000000000000000000000000000..34d1f14c3cd5a30e0df8f045a9d4e146bde3499a --- /dev/null +++ b/app/models/document.py @@ -0,0 +1,16 @@ +"""Models for handling associated documents.""" +from app.lib.database import db +from app.models.base import Model, Tracked +from app.models.columns import GUID + + +class DocumentSlot(Model, Tracked, db.Model): + """A m2m relationship between the project model and documents (provided by + the document service). + """ + project_id = db.Column( + db.Integer, db.ForeignKey('project.id'), nullable=False) + document_uuid = db.Column(GUID(), nullable=False) + + # How the document fits into the project. + role = db.Column(db.Unicode(length=255), nullable=False) diff --git a/app/tests/environment.py b/app/tests/environment.py index 892eccf56dfac5a1a32cd1544bf51aa647613bb5..16748963a63101724b82d72dd833e066ecf05150 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -16,6 +16,7 @@ from app.models.project import Project from app.models.client import Client from app.models.contact import Contact, ContactMethod, ProjectContact from app.models.note import Note +from app.models.document import DocumentSlot class Environment(object): @@ -115,3 +116,11 @@ class Environment(object): title='Test', content='This is a test note.', poster_name='test')) + + @property + def document_slot(self): + """A document slot.""" + return self.add(DocumentSlot( + project_id=self.project.id, + document_uuid=uuid.uuid4(), + role='test')) diff --git a/app/tests/test_document.py b/app/tests/test_document.py new file mode 100644 index 0000000000000000000000000000000000000000..c40d8a0d7b311bb4f5a7d21e5791ee6a06dc4125 --- /dev/null +++ b/app/tests/test_document.py @@ -0,0 +1,132 @@ +"""Unit tests for working with documents.""" +import uuid +from sqlalchemy import and_ +from app.lib.database import db +from app.tests.base import RestTestCase +from app.models.document import DocumentSlot + + +class TestDocumentSlot(RestTestCase): + """Tests the /project/document/ endpoints.""" + url = '/project/document/' + Model = DocumentSlot + + def test_index(self): + """Tests /project/document/ GET.""" + model = self.env.document_slot + self._test_index() + + def test_index_project_id(self): + """Tests /project/document/?project_id=... GET.""" + # Add a model that will be in the response. + project = self.env.project + model = self.env.add(DocumentSlot( + project_id=project.id, + document_uuid=uuid.uuid4(), + role='test')) + # Add a model that will not be in the response. + _ = self.env.document_slot + + response_data = self._test_index( + filter_data={'project_id': project.id}) + self.assertEqual(len(response_data), 1) + data = response_data[0] + self.assertIn('id', data) + self.assertEqual(data['id'], model.id) + + def test_get(self): + """Tests /project/document/ GET.""" + model = self.env.document_slot + self._test_get(model.id) + + def test_post(self): + """Tests /project/document/ POST.""" + data = { + 'project_id': self.env.project.id, + 'document_uuid': str(uuid.uuid4()), + 'role': 'test'} + response_data = self._test_post(data) + self.assertTrue( + db.session.query(DocumentSlot)\ + .filter(and_(*[ + getattr(DocumentSlot, k) == v for k, v in data.items() + ]))\ + .first()) + + def test_post_no_project_id(self): + """Tests /project/document/ POST with no project id. It should 400.""" + data = { + 'document_uuid': str(uuid.uuid4()), + 'role': 'test'} + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_bad_project_id(self): + """Tests /project/document/ POST with a bad project id. It should 400. + """ + data = { + 'project_id': 1, + 'document_uuid': str(uuid.uuid4()), + 'role': 'test'} + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_no_document_uuid(self): + """Tests /project/document/ POST with no document uuid. It should 400. + """ + data = { + 'project_id': self.env.project.id, + 'role': 'test'} + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_bad_document_uuid(self): + """Tests /project/document/ POST with a bad document uuid. It should + 400. + """ + data = { + 'project_id': self.env.project.id, + 'document_uuid': 'foo', + 'role': 'test'} + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_post_no_role(self): + """Tests /project/document/ POST with no role. It should 400.""" + data = { + 'project_id': self.env.project.id, + 'document_uuid': str(uuid.uuid4())} + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + + def test_put_project_id(self): + """Tests /project/document/ PUT with a project id. It should 400. + """ + model = self.env.document_slot + 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, 400) + + def test_put_document_uuid(self): + """Tests /project/document/ PUT with a document uuid. It should + 400. + """ + model = self.env.document_slot + data = model.get_dictionary() + data.update({'document_uuid': str(uuid.uuid4())}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 400) + + def test_put_role(self): + """Tests /project/document/ PUT with a role. It should 400.""" + model = self.env.document_slot + data = model.get_dictionary() + data.update({'role': 'foo'}) + response = self.put(self.url + str(model.id), data=data) + self.assertEqual(response.status_code, 400) + + def test_delete(self): + """Tests /project/document/ DELETE.""" + model = self.env.document_slot + self._test_delete(model.id) diff --git a/app/tests/test_project.py b/app/tests/test_project.py index fbe0a6207f753a6b33cae3196b0a4e9e02d71698..c9afe3714ca3990fd4a221cc358f736223b0cfbf 100644 --- a/app/tests/test_project.py +++ b/app/tests/test_project.py @@ -204,8 +204,7 @@ class TestSalesForceProject(UnprotectedRestTestCase): return { services.config['headers']['app_key']: \ self.app.config['SALESFORCE_KEY'], - services.config['headers']['app_secret']: \ - self.app.config['SALESFORCE_SECRET']} + 'referer': self.app.config['SALESFORCE_REFERRER']} def test_post_sf_client(self): """Tests /project/?form=salesforce POST with a new client.""" diff --git a/app/views/__init__.py b/app/views/__init__.py index 63e29d50505166870135cfa0d8acb986f66a78e9..c0f64eda2ed39c9e7c0be6a9ba263427512b8128 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 project, client, contact, place, note +from app.views import project, client, contact, place, note, document def register(app): @@ -20,3 +20,5 @@ def register(app): place.AddressView.register(app) note.NoteView.register(app) + + document.DocumentSlotView.register(app) diff --git a/app/views/document.py b/app/views/document.py new file mode 100644 index 0000000000000000000000000000000000000000..5eb8be6f7aa00e388bd89ad3e9eef25851260e10 --- /dev/null +++ b/app/views/document.py @@ -0,0 +1,12 @@ +"""Views for working with documents.""" +from app.views.base import RestView +from app.controllers.document import DocumentSlotController + + +class DocumentSlotView(RestView): + """A view for the m2m relationship between document and project.""" + route_base = '/project/document/' + + def get_controller(self): + """Return an instance of the document slot controller.""" + return DocumentSlotController()