diff --git a/app/__init__.py b/app/__init__.py index 455328709e74397ca2b836ed237acc1918aacc82..cbd49e1aa4393d1386a5194c114e38bee5aef288 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -6,22 +6,22 @@ def create_app(config): app = Flask(__name__) app.config.from_pyfile(config) - from app.lib import database + from .lib import database database.register(app) - from app.lib import red + from .lib import red red.register(app) - from app import views + from . import views views.register(app) - from app.lib import exceptions + from .lib import exceptions exceptions.register(app) - from app.lib import service + from .lib import service service.register(app) - from app.lib import session + from .lib import session session.register(app) with app.app_context(): diff --git a/app/controllers/base.py b/app/controllers/base.py index 6b6c143a890052ab90c2c4f0d2a7444e5338928a..9168871c498ca68863934b39fbbae8a83527b580 100644 --- a/app/controllers/base.py +++ b/app/controllers/base.py @@ -2,7 +2,8 @@ from sqlalchemy.exc import IntegrityError from werkzeug.datastructures import MultiDict from werkzeug.exceptions import NotFound, BadRequest from flask import current_app -from app.lib.database import db, commit + +from ..lib.database import db, commit class RestController(object): @@ -143,3 +144,38 @@ class RestController(object): db.session.delete(model) commit() return {} + +class SalesforceObjectController(RestController): + """ Base object controller for interacting with objects also represented in salesforce. + """ + def retrieveModel(self, data): + """Retrieve the base model using salesforce id, if it exists. Otherwise return + None. + """ + try: + model = db.session.query(self.Model)\ + .filter( + self.Model.sales_force_id == data['sales_force_id'])\ + .first() + except KeyError: + raise BadRequest( + 'When posting an object from salesforce, please ' + + 'include its salesforce id.') + return model + + def post(self, data, filter_data): + """ Post a new model. + + If the post is from salesforce, insert if salesforce id + is new and update if it already exist. If post is not from + salesforce treat as regular Restful POST request + """ + if filter_data.get('form') != 'salesforce': + return super(SalesforceObjectController, self).post(data, filter_data) + # Create/modify the object. + model = self.retrieveModel(data) + if model: + data.update({'id': model.id}) + return self.put(model.id, data, filter_data) + + return super(SalesforceObjectController, self).post(data, filter_data) diff --git a/app/controllers/client.py b/app/controllers/client.py index e7a081b2e962b6190428f404c7ee4664b223bde6..52edcfcf2ddb5dd49ee82f83a75af2db4efc9bfd 100644 --- a/app/controllers/client.py +++ b/app/controllers/client.py @@ -1,13 +1,12 @@ """Controllers for managing clients.""" -from werkzeug.exceptions import BadRequest - -from app.controllers.base import RestController +from app.lib.database import db +from app.controllers.base import SalesforceObjectController from app.models.project import Project from app.models.client import Client from app.forms.client import ClientForm -class ClientController(RestController): +class ClientController(SalesforceObjectController): """The client controller.""" Model = Client constant_fields = ['sales_force_id'] diff --git a/app/controllers/contact.py b/app/controllers/contact.py index 504524077635376880585120956248c9891ae7e7..25164ce48b999dc491e97701e20c716887197a53 100644 --- a/app/controllers/contact.py +++ b/app/controllers/contact.py @@ -1,11 +1,15 @@ """Controllers for managing contacts.""" +from sqlalchemy import and_ from werkzeug.exceptions import BadRequest -from app.controllers.base import RestController + +from app.lib.database import db +from app.controllers.base import RestController, SalesforceObjectController from app.models.contact import Contact, ContactMethod, ProjectContact +from app.models.project import Project from app.forms.contact import ContactForm, ContactMethodForm, ProjectContactForm -class ContactController(RestController): +class ContactController(SalesforceObjectController): """The contact controller.""" Model = Contact constant_fields = ['sales_force_id'] @@ -24,6 +28,42 @@ class ContactController(RestController): """Return the contact form.""" return ContactForm + def post(self, data, filter_data): + """ Post a new model. + + If the post is from salesforce use, insert if salesforce id + is new and update if it already exist. If post is not from + salesforce treat as regular Restful POST request + """ + if filter_data.get('form') == 'salesforce': + # Create/modify the contact with embedded contact methods. + try: + contact_methods_data = data['contact_methods'] + except KeyError: + raise BadRequest( + 'When posting a contact from salesforce embed contact ' + + 'methods') + + model = super(ContactController,self).post(data, filter_data) + + # Prevent salesforce from creating duplicate contact methods. + for contact_method_data in contact_methods_data: + contact_method_data.update({'contact_id': model.id}) + contact_method_filters = [ + getattr(ContactMethod, k) == v for k, v in contact_method_data.items()] + # Get a matching contact method or post a new one + contact_method = ( + db.session.query(ContactMethod)\ + .filter(and_(*contact_method_filters))\ + .first() or + ContactMethodController().post(contact_method_data, {}) + ) + + else: + model = super(ContactController, self).post(data, filter_data) + + return model + class ContactMethodController(RestController): """The contact method controller.""" @@ -53,3 +93,48 @@ class ProjectContactController(RestController): def get_form(self, filter_data): """Return the project contact form.""" return ProjectContactForm + + def post(self, data, filter_data): + """ Post a new model. + + If the post is from salesforce use, insert if salesforce id + is new and update if it already exist. If post is not from + salesforce treat as regular Restful POST request + """ + if filter_data.get('form') == 'salesforce': + # Create/modify the project contact with embeded contact and + # contact methods. + + try: + contact_data = data['contact'] + contact_methods_data = data['contact_methods'] + project_sales_force_id = data['project_sales_force_id'] + except KeyError: + raise BadRequest( + 'When posting a project contact from salesforce embed ' + + 'project saleforce id, contacts, and contact methods') + + contact_data.update({'contact_methods': contact_methods_data}) + contact = ContactController().post(contact_data,{'form': filter_data.get('form')}) + + data.update({'contact_id': contact.id}) + project = db.session.query(Project)\ + .filter(Project.sales_force_id == project_sales_force_id)\ + .first() + if not project: + raise BadRequest( + 'Opportunity out of sync. When posting a project contact from salesforce project ' + + 'the Opportunity has to be tracked by the project service.') + data.update({'project_id': project.id}) + model = db.session.query(self.Model)\ + .filter(and_( + self.Model.contact_id == contact.id , + self.Model.project_id == project.id))\ + .first() + + if not model: + model = super(ProjectContactController, self).post(data, filter_data) + else: + model = super(ProjectContactController, self).post(data, filter_data) + + return model diff --git a/app/controllers/note.py b/app/controllers/note.py index 829ed51986c29269cb80a868a578d628c045b282..e1e4b3f42823248313f8726203a2c74f69de3128 100644 --- a/app/controllers/note.py +++ b/app/controllers/note.py @@ -1,15 +1,49 @@ """Controllers for making small notes on a project.""" -from app.controllers.base import RestController +from app.lib.database import db +from app.controllers.base import SalesforceObjectController from app.models.note import Note +from app.models.project import Project from app.forms.note import NoteForm +from werkzeug.exceptions import BadRequest -class NoteController(RestController): +class NoteController(SalesforceObjectController): """The note controller.""" Model = Note constant_fields = [ - 'sales_force_id', 'project_id', 'posted', 'poster_uuid', 'poster_name'] + 'sales_force_id', 'project_id', 'posted', 'poster_uuid'] def get_form(self, filter_data): """Return the note form.""" return NoteForm + + def post(self, data, filter_data): + """ Post a new model. + + If the post is from salesforce, insert if salesforce id + is new and update if it already exist. If post is not from + salesforce treat as regular Restful POST request + """ + if filter_data.get('form') != 'salesforce': + return super(NoteController, self).post(data, filter_data) + else: + # Create/modify the object. + try: + project = db.session.query(Project)\ + .filter( + Project.sales_force_id == + data['project_sales_force_id'])\ + .first() + except KeyError: + raise BadRequest( + 'When posting a note from salesforce, please ' + + 'include its associated project salesforce id.') + + data.update({'project_id': project.id}) + model = self.retrieveModel(data) + + if not model: + return super(NoteController, self).post(data, filter_data) + + data.update({'id': model.id}) + return self.put(model.id, data, filter_data) diff --git a/app/controllers/place.py b/app/controllers/place.py index b870f7cfbdcfec4534f3a50ceb48379e015296b9..d701aa326a521e1f84a764be2ef0e8c32b0e403b 100644 --- a/app/controllers/place.py +++ b/app/controllers/place.py @@ -1,5 +1,6 @@ from werkzeug.exceptions import BadRequest +from app.lib.database import db from app.controllers.base import RestController from app.forms.place import AddressForm, PlaceForm from app.models.project import Project @@ -15,6 +16,52 @@ class PlaceController(RestController): """Return the Place form.""" return PlaceForm + def post(self, data, filter_data): + """ Post a new model. + + When the post is from salesforce, insert the object if salesforce id + is new and update if it already exist. If post is not from + salesforce treat as regular Restful POST request. + + Place always has an embedded address when coming from salesforce + so we save the address object and then the location object. + """ + if filter_data.get('form') == 'salesforce': + try: + address_data = data['address'] + except KeyError: + raise BadRequest( + 'Salesforce place requires an embedded address please.') + + # Create/modify the place. + try: + model = db.session.query(self.Model)\ + .filter( + self.Model.sales_force_id == data['sales_force_id'])\ + .first() + except KeyError: + raise BadRequest( + 'When post a place from salesforce, please ' + + 'include its salesforce id.') + + address = model.address if model else None + if address: + address_data.update({'id': address.id}) + address = AddressController().put(address.id, address_data, {}) + else: + address = AddressController().post(address_data, {}) + data.update({'address_id': address.id}) + + if model: + data.update({'id': model.id}) + model = self.put(model.id, data, filter_data) + else: + model = super(PlaceController, self).post(data, filter_data) + else: + model = super(PlaceController, self).post(data, filter_data) + + return model + class AddressController(RestController): """The Address controller.""" diff --git a/app/controllers/project.py b/app/controllers/project.py index e79477c733096ef221256742bcd150baac323fe7..679b02cb5680060af18404fb41ffdb8573c019b7 100644 --- a/app/controllers/project.py +++ b/app/controllers/project.py @@ -22,7 +22,7 @@ from app.forms.project import ProjectForm class ProjectController(RestController): """The project controller.""" Model = Project - constant_fields = ['sales_force_id', 'client_id', 'place_id'] + constant_fields = ['sales_force_id'] filters = { 'q': lambda d: and_(*[ Project.name.ilike('%{}%'.format(term)) @@ -40,7 +40,7 @@ class ProjectController(RestController): self.commit() def post(self, data, filter_data): - """Post a new model. + """Post a new model or updates a model(only from salesforce). This logs a model state change after saving a new model. It includes support for salesforce, which dumps all submodels (e.g @@ -53,8 +53,8 @@ class ProjectController(RestController): may be created even if the request 400s due to a problem with the project itself. """ - # A project has to have a client and a place, so we create these models - # before posting the project when performing a bulk upload. + # A project has to have a client and a place, so we create/change these models + # before posting the project when performing an embedded upload. if filter_data.get('form') == 'salesforce': try: client_data = data['client'] @@ -75,6 +75,7 @@ class ProjectController(RestController): raise BadRequest( 'When embedding a client in a salesforce project, please ' + 'include its salesforce id.') + if client: client_data.update({'id': client.id}) client = ClientController().put(client.id, client_data, {}) @@ -82,7 +83,7 @@ class ProjectController(RestController): client = ClientController().post(client_data, {}) data.update({'client_id': client.id}) - # Create/modify the place and address. + # Create/modify the Place and Address(if necessary) try: place = db.session.query(Place)\ .filter( @@ -99,6 +100,7 @@ class ProjectController(RestController): address = AddressController().put(address.id, address_data, {}) else: address = AddressController().post(address_data, {}) + place_data.update({'address_id': address.id}) if place: @@ -106,85 +108,26 @@ class ProjectController(RestController): place = PlaceController().put(place.id, place_data, {}) else: place = PlaceController().post(place_data, {}) + data.update({'place_id': place.id}) - model = super(ProjectController, self).post(data, filter_data) - - if filter_data.get('form') == 'salesforce': + # Create/modify the project try: - try: - contacts_data = data['contacts'] - contact_methods_data = data['contact_methods'] - notes_data = data['notes'] - except KeyError: - raise BadRequest( - 'SalesForce projects require embedded contacts.') - - for contact_data in contacts_data: - # Create the contact if it does not exist. - contact = db.session.query(Contact)\ - .filter(Contact.sales_force_id == \ - contact_data['sales_force_id'])\ - .first() - if contact: - contact_data.update({'id':contact.id}) - contact = ContactController().put( - contact.id, contact_data, {}) - else: - contact = ContactController().post(contact_data, {}) - - # Associate the contact with the project. - project_contact = db.session.query(ProjectContact)\ - .filter(and_( - ProjectContact.project_id == model.id, - ProjectContact.contact_id == contact.id))\ - .first() - if not project_contact: - ProjectContactController().post( - {'project_id': model.id, 'contact_id': contact.id}, - {}) - - for contact_method_data in contact_methods_data: - # Grab the contact by salesforce id. - contact = db.session.query(Contact)\ - .join(ProjectContact, - ProjectContact.contact_id == Contact.id)\ - .join(Project, Project.id == ProjectContact.project_id)\ - .filter(and_( - Project.id == model.id, - Contact.sales_force_id == \ - contact_method_data['contact_sales_force_id']))\ - .first() - if not contact: - raise BadRequest( - 'Contact method references a contact that is not ' + - 'associated with the project.') - - contact_method = db.session.query(ContactMethod)\ - .filter(and_( - ContactMethod.contact_id == contact.id, - ContactMethod.method == \ - contact_method_data['method'], - ContactMethod.value == \ - contact_method_data['value']))\ - .first() - if not contact_method: - contact_method_data.update({'contact_id': contact.id}) - contact_method = ContactMethodController().post( - contact_method_data, - {}) - - for note_data in notes_data: - # Create the note. We know it doesn't exist previously - # because the project didn't exist previously. - note_data.update({'project_id': model.id}) - note = NoteController().post(note_data, {}) - - except BadRequest as e: - # Do not keep the project if contacts failed to upload. - db.session.delete(model) - self.commit() - raise e + model = db.session.query(self.Model)\ + .filter( + self.Model.sales_force_id == data['sales_force_id'])\ + .first() + except KeyError: + raise BadRequest( + 'When posting a project from salesforce, please ' + + 'include its salesforce id.') + if model: + data.update({'id': model.id}) + model = self.put(model.id, data, filter_data) + else: + model = super(ProjectController, self).post(data, filter_data) + else: + model = super(ProjectController, self).post(data, filter_data) self.log_state_change(model) return model diff --git a/app/forms/base.py b/app/forms/base.py index 34ed4f9289b5f360c55a66378ccd2ecd8650665a..8118e9321ae410765aeadb51cf5ef8f14f3065c4 100644 --- a/app/forms/base.py +++ b/app/forms/base.py @@ -1,7 +1,29 @@ """Base classes for working with forms.""" import wtforms as wtf +from ..lib import geography +from . import validators class Form(wtf.Form): """A base class for forms based on the base model mixin.""" id = wtf.IntegerField() + + +class AddressForm(object): + """A form for validating address information.""" + street_address = wtf.StringField( + validators=[wtf.validators.Required(), wtf.validators.Length(max=255)]) + city = wtf.StringField( + validators=[wtf.validators.Required(), wtf.validators.Length(max=255)]) + county = wtf.StringField( + validators=[wtf.validators.Length(max=255)]) + state = wtf.StringField( + validators=[wtf.validators.AnyOf(geography.states)]) + country = wtf.StringField( + validators=[wtf.validators.AnyOf(geography.countries)]) + postal_code = wtf.StringField(validators=[validators.zip_]) + + +class LocationForm(object): + """A form for validating locations.""" + point = wtf.StringField(validators=[validators.point]) diff --git a/app/forms/fields.py b/app/forms/fields.py index be879ff04fc423d1e23f38d19576addd68494d72..f911d45422aa67fbaab906fc9d960fbaba8f5e4e 100644 --- a/app/forms/fields.py +++ b/app/forms/fields.py @@ -4,6 +4,18 @@ import arrow import wtforms as wtf +class String(wtf.StringField): + """A string field implementation that supports null values.""" + def process_formdata(self, valuelist): + if valuelist and valuelist[0] is not None: + self.data = valuelist[0] + else: + self.data = None + + def _value(self): + return str(self.data) if self.data is not None else None + + class Arrow(wtf.Field): """A field that accepts any input that arrow finds an acceptable datetime. """ diff --git a/app/forms/note.py b/app/forms/note.py index b092c6466499d874bb0a64aaaa7bcd561a01bcb3..6dc02ab2d1986ec0c08116fd24ff14559eeb8898 100644 --- a/app/forms/note.py +++ b/app/forms/note.py @@ -6,13 +6,15 @@ from app.forms.base import Form class NoteForm(Form): """A form for validating notes.""" - sales_force_id = wtf.StringField( - validators=[wtf.validators.Length(max=255)]) - project_id = wtf.IntegerField(validators=[wtf.validators.DataRequired()]) + sales_force_id = wtf.StringField(validators=[ + wtf.validators.Length(max=255)]) + project_id = wtf.IntegerField(validators=[wtf.validators.Required()]) - posted = fields.Arrow() - title = wtf.StringField(validators=[wtf.validators.Length(max=255)]) - content = wtf.StringField() + posted = fields.Arrow(validators=[wtf.validators.Required()]) + title = wtf.StringField(validators=[ + wtf.validators.Required(), + wtf.validators.Length(max=255)]) + content = wtf.StringField(validators=[wtf.validators.Required()]) poster_uuid = fields.UUID() poster_name = wtf.StringField(validators=[ diff --git a/app/forms/validators.py b/app/forms/validators.py index 0d9123428459e3268365c8ee7ceaa98da3c475bf..a831f363ef3690f46f02da3bef5a3f84da7af224 100644 --- a/app/forms/validators.py +++ b/app/forms/validators.py @@ -61,8 +61,20 @@ class UUID(object): raise wtf.ValidationError(self.message) +class AnyOf(wtf.validators.AnyOf): + """A custom AnyOf validator that accepts null values.""" + def __call__(self, form, field): + """Call the parent validator if the field data is not None.""" + if field.data is not None: + super(AnyOf, self).__call__(form, field) + + # A phone number validator. phone = wtf.validators.Regexp(r'^[0-9]{10,}$') # A zip code validator. zip_ = wtf.validators.Regexp(r'^\d{5}(-\d{4})?$') + + +# A postGIS point validator. For example, POINT(-70 40). +point = wtf.validators.Regexp(r'POINT\(\-?\d{1,3}(\.\d+)? \-?\d{1,3}(\.\d+)?\)') diff --git a/app/models/base.py b/app/models/base.py index 04a4eaf7ef19c8e03f50825223281f7073074caf..8381060c7d044d4d4d8e2b77f321e16b2492f44d 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -1,10 +1,16 @@ import arrow import decimal import uuid + from sqlalchemy.sql import func from sqlalchemy.ext.declarative import declared_attr -from app.lib.database import db -from app.models import columns + +from geoalchemy2 import Geometry +from geoalchemy2.elements import WKBElement, WKTElement + +from ..lib.database import db +from ..lib import geography +from . import columns class Model(object): @@ -41,6 +47,8 @@ class Model(object): # If we cast decimals to floats, we end up with rounding # errors. Better to render them out to strings. value = str(value) + elif isinstance(value, (WKBElement, WKTElement)): + value = db.session.scalar(value.ST_AsText()) d[key] = value return d @@ -55,3 +63,31 @@ class Tracked(object): class SalesForce(object): """A mixin that includes a SalesForce id.""" sales_force_id = db.Column(db.Unicode(255), index=True) + + +class Address(object): + """An address (with street number, city, state, zip, ...). This does not + necessarily correlate with a physical location as defined in the + Location base model. + """ + street_address = db.Column(db.Unicode(255), nullable=False) + city = db.Column(db.Unicode(255), nullable=False) + county = db.Column(db.Unicode(255)) + state = db.Column( + db.Enum(*geography.states, name='geo_states'), + nullable=False) + country = db.Column( + db.Enum(*geography.countries, name='geo_countries'), + nullable=False) + postal_code = db.Column(db.Unicode(10)) + + +class Location(object): + """A physical location (with latitude, longitude, and elevation).""" + point = db.Column(Geometry('POINT'), nullable=False) + + +class External(object): + """An external-facing model. These have a UUID key.""" + key = db.Column( + columns.GUID(), index=True, nullable=False, default=uuid.uuid4) diff --git a/app/permissions/application.py b/app/permissions/application.py index ed798dd9f208b74b81c28e8fcc9cc690b92f55df..698ab5d3e6ddb3dea6f9bd85e1ce33a4b4212a9f 100644 --- a/app/permissions/application.py +++ b/app/permissions/application.py @@ -1,9 +1,10 @@ """Permissions for working with the app service.""" import requests from werkzeug.exceptions import Unauthorized -from app.permissions.base import Permission -from app.lib.red import redis -from app.lib.service import services + +from ..lib.red import redis +from ..lib.service import services +from .base import Permission class AppNeed(Permission): @@ -108,3 +109,21 @@ class RoleNeed(Permission): return False data = data['data'] return len(data) > 0 + +class FormNeed(Permission): + """Checks that the current requests has the correct form query value. """ + @property + def error(self): + """Return an error based on the form.""" + return Unauthorized( + 'Please authenticate with an application using the {} form.'\ + .format(self.form)) + + def __init__(self, form): + """Initialize with a form.""" + self.form = form + + def is_met(self): + from flask import request + form = request.args.get('form') + return form == self.form diff --git a/app/permissions/auth.py b/app/permissions/auth.py index fd2d4d389175294e8d7b0e15b732ed6f253f7328..1a6bb249f29ee596b32b1372e4096ef54f969423 100644 --- a/app/permissions/auth.py +++ b/app/permissions/auth.py @@ -1,10 +1,10 @@ """Permissions to check authentication.""" from werkzeug.exceptions import Unauthorized -from app.lib.red import redis -from app.lib.service import services -from app.permissions.base import Permission -from app.permissions.application import app_need +from ..lib.red import redis +from ..lib.service import services +from .base import Permission +from .application import app_need, FormNeed, RoleNeed class AuthNeed(Permission): @@ -68,3 +68,8 @@ auth_need = AuthNeed() standard_login_need = app_need & auth_need + +salesforce_need = app_need & ( + (FormNeed('salesforce') & RoleNeed('salesforce')) | + (~FormNeed('salesforce') & auth_need) +) diff --git a/app/tests/base.py b/app/tests/base.py index 7419eb8847be11d07759bfaa095da9467b063d7b..ae3cc063f13acb621c500bc4a625544884c9b28e 100644 --- a/app/tests/base.py +++ b/app/tests/base.py @@ -267,3 +267,13 @@ class RestTestCase(AppProtectedTestCase): self.app.config['HEADER_AUTH_KEY']: self.fake_app.key, self.app.config['HEADER_AUTH_TOKEN']: self.fake_app.token}) return headers + +class SalesforceTestCase(UnprotectedRestTestCase): + """ Base test case for requests from salesforce.""" + @property + def global_headers(self): + """Add app headers to authenticate a salesforce client.""" + return { + services.config['headers']['app_key']: \ + self.app.config['SALESFORCE_KEY'], + 'referer': self.app.config['SALESFORCE_REFERRER']} diff --git a/app/tests/environment.py b/app/tests/environment.py index ce4b306d0eaceccf51dbf30b4beb71bdc06c1ee6..73c95aaf23e8d379e3478aa838b195b2c8afd39b 100644 --- a/app/tests/environment.py +++ b/app/tests/environment.py @@ -33,6 +33,13 @@ class Environment(object): def client(self): return self.add(Client(sales_force_id='test', name='test')) + @property + def otherclient(self): + """This client is only used as an orphaned client that gets + reparented with a project on a project update with its salesforce id. + """ + return self.add(Client(sales_force_id='othertest', name='test')) + @property def address(self): return self.add(Address( @@ -101,6 +108,7 @@ class Environment(object): """A note with a poster uuid.""" return self.add(Note( project_id=self.project.id, + posted=arrow.get().isoformat(), title='Test', content='This is a test note.', poster_uuid=str(uuid.uuid4()))) diff --git a/app/tests/test_client.py b/app/tests/test_client.py index 8cd245154b56f1450a59887c411da07eb3cf52ca..08fd6eadf0a3231d38df34a0580f23f79ed58ee3 100644 --- a/app/tests/test_client.py +++ b/app/tests/test_client.py @@ -1,7 +1,7 @@ """Unit tests for clients.""" from sqlalchemy import and_ from app.lib.database import db -from app.tests.base import RestTestCase +from app.tests.base import RestTestCase, SalesforceTestCase from app.models.client import Client @@ -80,3 +80,40 @@ class TestClient(RestTestCase): model = self.env.client response = self.delete(self.url + str(model.id)) self.assertEqual(response.status_code, 405) + +class TestClientSalesforce(SalesforceTestCase): + """Tests the /client/ endpoints when hit from saleforce.""" + url = '/client/' + Model = Client + + def test_update(self): + """Tests /client/?form=salesforce POST update.""" + model = self.env.client + data = { + 'sales_force_id':model.sales_force_id, + 'name': model.name} + data.update({'name': 'John Doe'}) + self._test_post(data, {'form': 'salesforce'}) + + def test_update_missing_sf_id(self): + """Tests /client/?form=salesforce POST update without sf id. This + should 400.""" + model = self.env.client + data = {'name': model.name} + data.update({'name': 'John Doe'}) + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_name(self): + """Tests /client/?form=salesforce POST update without name. This + should 400.""" + model = self.env.client + data = {'sales_force_id':model.sales_force_id} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) diff --git a/app/tests/test_contact.py b/app/tests/test_contact.py index 76af05683e20777a102d082dd517f0f9f7e93c25..4ec3d1445efb27f7159ecf23f191b4cb47881d74 100644 --- a/app/tests/test_contact.py +++ b/app/tests/test_contact.py @@ -1,7 +1,9 @@ """Unit tests for working with contacts.""" from sqlalchemy import and_ + +from app.lib.service import services from app.lib.database import db -from app.tests.base import RestTestCase +from app.tests.base import RestTestCase, SalesforceTestCase from app.models.project import Project from app.models.contact import Contact, ContactMethod, ProjectContact @@ -276,7 +278,8 @@ class TestContactMethod(RestTestCase): # 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. + """Tests /contact/method/ PUT with a new contact_id. It should + 400. """ model = self.env.email_contact_method data = model.get_dictionary() @@ -285,7 +288,8 @@ class TestContactMethod(RestTestCase): self.assertEqual(response.status_code, 400) def test_put_method(self): - """Tests /contact/method/ PUT with a new method. It should 400.""" + """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'}) @@ -329,7 +333,8 @@ class TestProjectContact(RestTestCase): .first()) def test_post_bad_contact(self): - """Tests /contact/project/ POST with a bad contact id. It should 400.""" + """Tests /contact/project/ POST with a bad contact id. It should 400. + """ data = { 'project_id': self.env.project.id, 'contact_id': 1 @@ -338,7 +343,8 @@ class TestProjectContact(RestTestCase): self.assertEqual(response.status_code, 400) def test_post_bad_project(self): - """Tests /contact/project/ POST with a bad project id. It should 400.""" + """Tests /contact/project/ POST with a bad project id. It should 400. + """ data = { 'project_id': 1, 'contact_id': self.env.contact.id @@ -358,3 +364,236 @@ class TestProjectContact(RestTestCase): """Tests /contact/project/ DELETE.""" model = self.env.project_contact self._test_delete(model.id) + +class TestSalesForceContact(SalesforceTestCase): + """Tests the /note/?form=salesforce POST endpoint.""" + url = '/contact/' + Model = Contact + + def test_update_contact_method(self): + """Tests /contact/?form=salesforce POST with existing object.""" + model = self.env.contact + + data = { + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'contact_methods':[{ + 'method': 'sms', + 'value': '88845555555'}] + } + self._test_post(data, {'form': 'salesforce'}) + + def test_update_name(self): + """Tests /contact/?form=salesforce POST with existing object.""" + model = self.env.contact + + data = { + 'sales_force_id': model.sales_force_id, + 'name': 'John Doe', + 'contact_methods':[{ + 'method': 'sms', + 'value': '88855555555' + }] + } + self._test_post(data, {'form': 'salesforce'}) + + def test_update_missing_name(self): + """Tests /contact/?form=salesforce POST missing name. It should 400. + """ + model = self.env.contact + + data = { + 'sales_force_id': model.sales_force_id, + 'contact_methods':[{ + 'method': 'sms', + 'value': '88855555555' + }] + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_sf_id(self): + """Tests /contact/?form=salesforce POST missing salesforce id. It + should 400.""" + model = self.env.contact + + data = { + 'name': 'John Doe', + 'contact_methods':[{ + 'method': 'sms', + 'value': '88855555555' + }] + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_contact_methods(self): + """Tests /contact/?form=salesforce POST missing contact methods. It + should 400""" + model = self.env.contact + + data = { + 'sales_force_id': model.sales_force_id, + 'name': 'John Doe' + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + +class TestSalesForceProjectContact(SalesforceTestCase): + """Tests the /contact/project/?form=salesforce POST endpoint.""" + url = '/contact/project/' + Model = ProjectContact + + def test_create(self): + """Tests /contact/project/?form=salesforce POST with new contact.""" + project = self.env.project + data = { + 'contact': { + 'name': 'test', + 'sales_force_id': 'testId' + }, + 'project_sales_force_id': project.sales_force_id, + 'contact_methods':[{ + 'method': 'sms', + 'value': '88855555555' + }] + } + self._test_post(data=data, filter_data={'form': 'salesforce'}) + + def test_create_no_contact_methods(self): + """Tests /contact/project?form=salesforce POST new project contact + without contact methods. It should 400.""" + project = self.env.project + data = { + 'contact': { + 'name' : 'test', + 'sales_force_id': 'testId' + }, + 'project_sales_force_id': project.sales_force_id + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_no_contact(self): + """Tests /contact/project?form=salesforce POST new project contact + without contact. It should 400.""" + project = self.env.project + data = { + 'project_sales_force_id': project.sales_force_id, + 'contact_methods':[{ + 'method': 'sms', + 'value': '88855555555' + }] + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + + def test_update(self): + """Tests /contact/project/?form=salesforce POST with existing project + contact.""" + model = self.env.project_contact + contact = db.session.query(Contact)\ + .filter(Contact.id == model.contact_id)\ + .first() + + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + + contact_methods = db.session.query(ContactMethod)\ + .filter(ContactMethod.contact_id == contact.id)\ + .all() + + contact_method_data = [{'method': c.method, 'value': c.value} for c in contact_methods] + + + data = { + 'contact': contact.get_dictionary(), + 'project_sales_force_id': project.sales_force_id, + 'contact_methods': contact_method_data} + + self._test_post(data=data, filter_data={'form': 'salesforce'}) + + def test_update_no_contact_methods(self): + """Tests /contact/project?form=salesforce POST updating project + contact without contact methods. It should 400.""" + model = self.env.project_contact + contact = db.session.query(Contact)\ + .filter(Contact.id == model.contact_id)\ + .first() + contact_data = contact.get_dictionary() + contact_data.pop('id') + contact_data.pop('created') + contact_data.pop('updated') + + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + + data = { + 'contact': contact_data, + 'project_sales_force_id': project.sales_force_id} + + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_no_contact(self): + """Tests /contact/project?form=salesforce POST updating project + contact without contact. It should 400.""" + model = self.env.project_contact + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + data = { + 'project': {"sales_force_id": project.sales_force_id}, + 'contact_methods':[{ + 'method': 'sms', + 'value': '88855555555' + }] + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_no_project_sales_force_id(self): + """Tests /contact/project/?form=salesforce POST with existing project + contact without project_sales_force_id. It should 400.""" + model = self.env.project_contact + contact = db.session.query(Contact)\ + .filter(Contact.id == model.contact_id)\ + .first() + + contact_methods = db.session.query(ContactMethod)\ + .filter(ContactMethod.contact_id == contact.id)\ + .all() + contact_method_data = [{'method': c.method, 'value': c.value} for c in contact_methods] + data = { + 'contact': contact.get_dictionary(), + 'contact_methods': contact_method_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) diff --git a/app/tests/test_note.py b/app/tests/test_note.py index e15d689e4717f600de3f30d8bcbd30e3cb316e32..3a90c93e6255f42a976719f3347bd52a86d2b61b 100644 --- a/app/tests/test_note.py +++ b/app/tests/test_note.py @@ -2,9 +2,12 @@ import arrow import uuid from sqlalchemy import and_ + +from app.lib.service import services from app.lib.database import db -from app.tests.base import RestTestCase +from app.tests.base import RestTestCase, SalesforceTestCase from app.models.note import Note +from app.models.project import Project class TestNote(RestTestCase): @@ -26,6 +29,7 @@ class TestNote(RestTestCase): """Tests /note/ POST with a poster uuid.""" data = { 'project_id': self.env.project.id, + 'posted': arrow.get().isoformat(), 'title': 'Test', 'content': 'This is a test note.', 'poster_uuid': str(uuid.uuid4())} @@ -51,6 +55,7 @@ class TestNote(RestTestCase): def test_post_no_project_id(self): """Tests /note/ POST with no project id. It should 400.""" data = { + 'posted': arrow.get().isoformat(), 'title': 'Test', 'content': 'This is a test note.', 'user_uuid': str(uuid.uuid4())} @@ -61,6 +66,7 @@ class TestNote(RestTestCase): """Tests /note/ POST with a bad project id. It should 400.""" data = { 'project_id': 1, + 'posted': arrow.get().isoformat(), 'title': 'Test', 'content': 'This is a test note.', 'user_uuid': str(uuid.uuid4())} @@ -71,6 +77,7 @@ class TestNote(RestTestCase): """Tests /note/ POST with no poster uuid or name. It should 400.""" data = { 'project_id': self.env.project.id, + 'posted': arrow.get().isoformat(), 'title': 'Test', 'content': 'This is a test note.'} response = self.post(self.url, data=data) @@ -80,12 +87,23 @@ class TestNote(RestTestCase): """Tests /note/ POST with a bad poster uuid. It should 400.""" data = { 'project_id': self.env.project.id, + 'posted': arrow.get().isoformat(), 'title': 'Test', 'content': 'This is a test note.', 'poster_uuid': 'test'} response = self.post(self.url, data=data) self.assertEqual(response.status_code, 400) + def test_post_no_posted(self): + """Test /note/ POST with no posted datetime. It should 400.""" + data = { + 'project_id': self.env.project.id, + 'title': 'Test', + 'content': 'This is a test note.', + 'post_uuid': str(uuid.uuid4())} + response = self.post(self.url, data=data) + self.assertEqual(response.status_code, 400) + def test_post_bad_posted(self): """Tests /note/ POST with a bad posted datetime. It should 400.""" data = { @@ -146,16 +164,252 @@ class TestNote(RestTestCase): response = self.put(self.url + str(model.id), data=data) self.assertEqual(response.status_code, 400) - def test_put_poster_name(self): - """Tests /note/ PUT with a poster name. It should 400.""" - model = self.env.note_with_name - new_data = {'poster_name': 'foo'} - data = model.get_dictionary() - data.update(new_data) - response = self.put(self.url + str(model.id), data=data) - self.assertEqual(response.status_code, 400) - def test_delete(self): """Tests /note/ DELETE.""" model = self.env.note self._test_delete(model.id) + + +class TestSalesForceNote(SalesforceTestCase): + """Tests the /note/?form=salesforce POST endpoint.""" + url = '/note/' + Model = Note + + def test_create(self): + """Tests /note/?form=salesforce POST create with a note.""" + project = self.env.project + + data = { + 'sales_force_id': 'test', + 'project_sales_force_id': project.sales_force_id, + 'posted': arrow.get().isoformat(), + 'title': 'Test', + 'content': 'This is a test note.', + 'poster_name': 'test'} + self._test_post(data, {'form': 'salesforce'}) + + def test_update(self): + """Tests /note/?form=salesforce POST update with a note.""" + model = self.env.note_with_name + data = { + 'sales_force_id': model.sales_force_id, + 'posted': model.posted.isoformat(), + 'title': model.title, + 'content': 'This is an update of test note.', + 'poster_name': model.poster_name} + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + data.update({ + 'project_sales_force_id': project.sales_force_id}) + self._test_post(data, {'form': 'salesforce'}) + + def test_create_missing_title(self): + """Tests /note/?form=salesforce POST create without note title. This should 400.""" + project = self.env.project + + data = { + 'sales_force_id': 'test', + 'project_sales_force_id': project.sales_force_id, + 'posted': arrow.get().isoformat(), + 'content': 'This is a test note.', + 'poster_name': 'test'} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_content(self): + """Tests /note/?form=salesforce POST create without content. This should 400.""" + project = self.env.project + + data = { + 'sales_force_id': 'test', + 'project_sales_force_id': project.sales_force_id, + 'posted': arrow.get().isoformat(), + 'title': "Test", + 'poster_name': 'test'} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_poster_name(self): + """Tests /note/?form=salesforce POST create without poster_name. This should 400.""" + project = self.env.project + + data = { + 'sales_force_id': 'test', + 'project_sales_force_id': project.sales_force_id, + 'posted': arrow.get().isoformat(), + 'title': 'Test', + 'content': 'This is a test note.'} + + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_sf_id(self): + """Tests /note/?form=salesforce POST create without sales_force_id. This should 400.""" + project = self.env.project + + data = { + 'project_sales_force_id': project.sales_force_id, + 'posted': arrow.get().isoformat(), + 'title': 'Test', + 'content': 'This is a test note.', + 'poster_name': 'test'} + + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_project_sf_id(self): + """Tests /note/?form=salesforce POST create without project_sales_force_id. This should 400.""" + project = self.env.project + + data = { + 'sales_force_id': 'test', + 'posted': arrow.get().isoformat(), + 'title': 'Test', + 'content': 'This is a test note.', + 'poster_name': 'test'} + + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_posted(self): + """Tests /note/?form=salesforce POST create without posted. This should 400.""" + project = self.env.project + + data = { + 'sales_force_id': 'test', + 'project_sales_force_id': project.sales_force_id, + 'title': 'Test', + 'content': 'This is a test note.', + 'poster_name': 'test'} + + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_title(self): + """Tests /note/?form=salesforce POST update without note title. This should 400.""" + model = self.env.note_with_name + data = { + 'sales_force_id': model.sales_force_id, + 'posted': model.posted.isoformat(), + 'content': 'This is an update of test note.', + 'poster_name': model.poster_name} + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + data.update({ + 'project_sales_force_id': project.sales_force_id}) + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_content(self): + """Tests /note/?form=salesforce POST update without content. This should 400.""" + model = self.env.note_with_name + data = { + 'sales_force_id': model.sales_force_id, + 'posted': model.posted.isoformat(), + 'title': model.title, + 'poster_name': model.poster_name} + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + data.update({ + 'project_sales_force_id': project.sales_force_id}) + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_poster_name(self): + """Tests /note/?form=salesforce POST update without poster_name. This should 400.""" + model = self.env.note_with_name + data = { + 'sales_force_id': model.sales_force_id, + 'posted': model.posted.isoformat(), + 'title': model.title, + 'content': 'This is an update of test note.'} + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + data.update({ + 'project_sales_force_id': project.sales_force_id}) + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_sf_id(self): + """Tests /note/?form=salesforce POST update without sales_force_id. This should 400.""" + model = self.env.note_with_name + data = { + 'posted': model.posted.isoformat(), + 'title': model.title, + 'content': 'This is an update of test note.', + 'poster_name': model.poster_name} + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + data.update({ + 'project_sales_force_id': project.sales_force_id}) + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_project_sf_id(self): + """Tests /note/?form=salesforce POST update without project_sales_force_id. This should 400.""" + model = self.env.note_with_name + data = { + 'sales_force_id': model.sales_force_id, + 'posted': model.posted.isoformat(), + 'title': model.title, + 'content': 'This is an update of test note.', + 'poster_name': model.poster_name} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_posted(self): + """Tests /note/?form=salesforce POST update without posted. This should 400.""" + model = self.env.note_with_name + data = { + 'sales_force_id': model.sales_force_id, + 'title': model.title, + 'content': 'This is an update of test note.', + 'poster_name': model.poster_name} + project = db.session.query(Project)\ + .filter(Project.id == model.project_id)\ + .first() + data.update({ + 'project_sales_force_id': project.sales_force_id}) + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) diff --git a/app/tests/test_place.py b/app/tests/test_place.py index a2c12d40fdfebde7691832ee621e5f6735388199..f068957f6fafebf6b4095ec821ee9381243acbb6 100644 --- a/app/tests/test_place.py +++ b/app/tests/test_place.py @@ -1,7 +1,9 @@ """Unit tests for working with Places and Addresses.""" from sqlalchemy import and_ + +from app.lib.service import services from app.lib.database import db -from app.tests.base import RestTestCase +from app.tests.base import RestTestCase, SalesforceTestCase from app.models.project import Project from app.models.place import Place, Address @@ -239,3 +241,347 @@ class TestAddress(RestTestCase): model = self.env.address response = self.delete(self.url + str(model.id)) self.assertEqual(response.status_code, 405) + +class TestSalesForcePlace(SalesforceTestCase): + """Tests the /place/?form=salesforce POST endpoint.""" + url = '/place/' + Model = Place + + def test_create(self): + """Tests /place/?form=salesforce POST create with an address.""" + address_data ={ + 'street_address':'15 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': 'test', + 'name': 'test place', + 'address': address_data + } + self._test_post(data, {'form': 'salesforce'}) + + def test_create_missing_street_address(self): + """Tests /place/?form=salesforce POST create missing street address. + It should 400.""" + address_data ={ + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': 'test', + 'name': 'test place', + 'address': address_data + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_city(self): + """Tests /place/?form=salesforce POST create missing city. It should + 400.""" + address_data ={ + 'street_address':'15 MetroTech Center', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': 'test', + 'name': 'test place', + 'address': address_data + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_state(self): + """Tests /place/?form=salesforce POST create missing state. It should + 400.""" + address_data ={ + 'street_address':'15 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': 'test', + 'name': 'test place', + 'address': address_data + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_country(self): + """Tests /place/?form=salesforce POST create missing country. It + should 400.""" + address_data ={ + 'street_address':'15 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'postal_code':'11210'} + data = { + 'sales_force_id': 'test', + 'name': 'test place', + 'address': address_data + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_postal_code(self): + """Tests /place/?form=salesforce POST create missing postal code. It + should 400.""" + address_data ={ + 'street_address':'15 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America'} + data = { + 'sales_force_id': 'test', + 'name': 'test place', + 'address': address_data + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_sf_id(self): + """Tests /place/?form=salesforce POST create missing salesfore id. It + should 400.""" + address_data ={ + 'street_address':'15 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'name': 'test place', + 'address': address_data + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_name(self): + """Tests /place/?form=salesforce POST create with missing name. It + should 400.""" + address_data ={ + 'street_address':'15 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': 'test', + 'address': address_data + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_create_missing_address(self): + """ Tests /place/?form=salesforce POST create missing an address. It + should 400.""" + data = { + 'sales_force_id': 'test', + 'name': 'test place', + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update(self): + """Tests /place/?form=salesforce POST update with an address.""" + model = self.env.place + address_data = { + 'street_address':'2 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'address': address_data} + self._test_post(data, {'form': 'salesforce'}) + + def test_update_missing_street_address(self): + """Tests /place/?form=salesforce POST update missing street + address. It should 400.""" + model = self.env.place + address_data = { + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_city(self): + """Tests /place/?form=salesforce POST update missing city. It should + 400.""" + model = self.env.place + address_data = { + 'street_address':'2 MetroTech Center', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_state(self): + """Tests /place/?form=salesforce POST update missing state. It should + 400.""" + model = self.env.place + address_data = { + 'street_address':'2 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_country(self): + """Tests /place/?form=salesforce POST update missing country. It + should 400.""" + model = self.env.place + address_data = { + 'street_address':'2 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'postal_code':'11210'} + data = { + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_postal_code(self): + """Tests /place/?form=salesforce POST update missing postal code + address. It should 400.""" + model = self.env.place + address_data = { + 'street_address':'2 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America'} + data = { + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_sf_id(self): + """Tests /place/?form=salesforce POST update missing salesfoce id. It + should 400.""" + model = self.env.place + address_data = { + 'street_address':'2 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'name': model.name, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_name(self): + """Tests /place/?form=salesforce POST update missing name. It should + 400.""" + model = self.env.place + address_data = { + 'street_address':'2 MetroTech Center', + 'city':'Brooklyn', + 'county':'Kings', + 'state':'NY', + 'country':'United States of America', + 'postal_code':'11210'} + data = { + 'sales_force_id': model.sales_force_id, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_missing_address(self): + """ Tests /place/?form=salesforce without an address. It should 400. + """ + model = self.env.place + data = { + 'sales_force_id': model.sales_force_id, + 'name': 'test change place' + } + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) diff --git a/app/tests/test_project.py b/app/tests/test_project.py index e4462cb024cea41e0dc0ae9158e2668d04826a65..434b7b03868ad75aaca82837354d13a671b28a1e 100644 --- a/app/tests/test_project.py +++ b/app/tests/test_project.py @@ -5,7 +5,7 @@ from sqlalchemy import and_ from app.lib.service import services from app.lib.database import db -from app.tests.base import RestTestCase, UnprotectedRestTestCase +from app.tests.base import RestTestCase, SalesforceTestCase from app.models.client import Client from app.models.contact import Contact, ContactMethod, ProjectContact from app.models.place import Place, Address @@ -178,20 +178,18 @@ class TestProject(RestTestCase): self.assertEqual(response.status_code, 400) def test_put_client_id(self): - """Tests /project/ PUT with a client id. It should 400.""" + """Tests /project/ PUT with a client id.""" 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) + self._test_put(model.id, data) def test_put_place_id(self): - """Tests /project/ PUT with a place id. It should 400.""" + """Tests /project/ PUT with a place id.""" model = self.env.project data = model.get_dictionary() data.update({'place_id': self.env.place.id}) - response = self.put(self.url + str(model.id), data=data) - self.assertEqual(response.status_code, 400) + self._test_put(model.id, data) def test_delete(self): """Tests /project/ DELETE. It should 405.""" @@ -199,40 +197,29 @@ class TestProject(RestTestCase): response = self.delete(self.url + str(model.id)) self.assertEqual(response.status_code, 405) - -class TestSalesForceProjectAuth(UnprotectedRestTestCase): - """Tests authentication for the /project/?form=salesforce POST endpoint.""" - url = '/project/' - Model = Project - - -class TestSalesForceProject(UnprotectedRestTestCase): +class TestSalesForceProject(SalesforceTestCase): """Tests the /project/?form=salesforce POST endpoint.""" url = '/project/' Model = Project - @property - def global_headers(self): - """Add app headers to authenticate a salesforce client.""" - return { - services.config['headers']['app_key']: \ - self.app.config['SALESFORCE_KEY'], - 'referer': self.app.config['SALESFORCE_REFERRER']} - - def test_post_sf_client(self): - """Tests /project/?form=salesforce POST with a new client.""" + def test_create(self): + """Tests /project/?form=salesforce POST.""" client_data = {'sales_force_id': 'test', 'name': 'test'} - place = self.env.place + place_data = {'sales_force_id': 'test', 'name': 'test'} + address_data = { + 'street_address': '111 N Fake St', + 'city': 'Raleigh', + 'county': 'Wake', + 'state': 'NC', + 'country': 'United States of America', + 'postal_code': '27615'} data = { 'sales_force_id': 'test', 'name': 'test', 'state': 'pending', 'client': client_data, - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [], - 'contact_methods': [], - 'notes': []} + 'place': place_data, + 'address': address_data} response_data = self._test_post(data, {'form': 'salesforce'}) # Check that a client was posted with the new data. self.assertTrue( @@ -244,426 +231,449 @@ class TestSalesForceProject(UnprotectedRestTestCase): Client.sales_force_id == client_data['sales_force_id']))\ .first()) - def test_post_sf_change_client(self): - """Tests /project/?form=salesforce POST with a modified client.""" - client = self.env.client - client_data = client.get_dictionary() - client_data.pop('id') - client_data.update({'name': 'foo'}) - place = self.env.place + def test_create_missing_sf_id(self): + """Tests /project/?form=salesforce POST missing salesforce id. It + should 400.""" + client_data = {'sales_force_id': 'test', 'name': 'test'} + place_data = {'sales_force_id': 'test', 'name': 'test'} + address_data = { + 'street_address': '111 N Fake St', + 'city': 'Raleigh', + 'county': 'Wake', + 'state': 'NC', + 'country': 'United States of America', + 'postal_code': '27615'} data = { - 'sales_force_id': 'test', 'name': 'test', 'state': 'pending', 'client': client_data, - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [], - 'contact_methods': [], - 'notes': []} - response_data = self._test_post(data, {'form': 'salesforce'}) - # Check that the client was modified as expected. - self.assertTrue( - db.session.query(Client)\ - .join(Project, Project.client_id == Client.id) - .filter(and_( - Project.id == response_data['id'], - Client.id == client.id, - Client.name == client_data['name']))\ - .first()) - - def test_post_sf_no_client(self): - """Tests /project/?form=salesforce POST with no client. This should - 400. - """ - place = self.env.place - data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [], - 'contact_methods': [], - 'notes': []} + 'place': place_data, + 'address': address_data} response = self.post( self.url, data=data, query_string={'form': 'salesforce'}) self.assertEqual(response.status_code, 400) - # There are additional ways that posting with a client could fail, but - # those are already tested in the /client/ endpoint, so there's no need - # to test them here. - - def test_post_sf_new_place(self): - """Tests /project/?form=salesforce POST with a new place and address. - """ + def test_create_missing_client(self): + """Tests /project/?form=salesforce POST create with no client. This + should 400.""" + client_data = {'sales_force_id': 'test', 'name': 'test'} place_data = {'sales_force_id': 'test', 'name': 'test'} address_data = { - 'street_address': '15 MetroTech Center', - 'city': 'Brooklyn', - 'county': 'Kings', - 'state': 'NY', + 'street_address': '111 N Fake St', + 'city': 'Raleigh', + 'county': 'Wake', + 'state': 'NC', 'country': 'United States of America', - 'postal_code': '11210'} + 'postal_code': '27615'} data = { 'sales_force_id': 'test', 'name': 'test', 'state': 'pending', - 'client': self.env.client.get_dictionary(), 'place': place_data, - 'address': address_data, - 'contacts': [], - 'contact_methods': [], - 'notes': []} - response_data = self._test_post(data, {'form': 'salesforce'}) - - filters = [Project.id == response_data['id']] - filters += [ - getattr(Place, k) == place_data.get(k) - for k in place_data.keys()] - filters += [ - getattr(Address, k) == address_data.get(k) - for k in address_data.keys()] - - self.assertTrue( - db.session.query(Address)\ - .join(Place, Place.address_id == Address.id)\ - .join(Project, Project.place_id == Place.id)\ - .filter(and_(*filters))\ - .first()) - - def test_post_sf_change_place(self): - """Tests /project/?form=salesforce POST with a modified place and - address. - """ - place = self.env.place - place_data = place.get_dictionary() - place_data.pop('id') - new_place_data = {'name': 'foo'} - place_data.update(new_place_data) - address_data = place.address.get_dictionary() - address_data.pop('id') + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) - new_address_data = { + def test_create_missing_place(self): + """Tests /project/?form=salesforce POST create with no place. It + should 400.""" + client_data = {'sales_force_id': 'test', 'name': 'test'} + address_data = { 'street_address': '111 N Fake St', 'city': 'Raleigh', 'county': 'Wake', 'state': 'NC', + 'country': 'United States of America', 'postal_code': '27615'} - address_data.update(new_address_data) data = { 'sales_force_id': 'test', 'name': 'test', 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place_data, - 'address': address_data, - 'contacts': [], - 'contact_methods': [], - 'notes': []} - response_data = self._test_post(data, {'form': 'salesforce'}) - - filters = [ - Project.id == response_data['id'], - Place.id == place.id, - Address.id == place.address.id] - filters += [ - getattr(Place, k) == place_data.get(k) - for k in new_place_data.keys()] - filters += [ - getattr(Address, k) == address_data.get(k) - for k in new_address_data.keys()] - - self.assertTrue( - db.session.query(Address)\ - .join(Place, Place.address_id == Address.id)\ - .join(Project, Project.place_id == Place.id)\ - .filter(and_(*filters))\ - .first()) + 'client': client_data, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) - def test_post_sf_no_place(self): - """Tests /project/?form=salesforce POST with no place. It should 400. - """ - address_data = { - 'street_address': '15 MetroTech Center', - 'city': 'Brooklyn', - 'county': 'Kings', - 'state': 'NY', - 'country': 'United States of America', - 'postal_code': '11210'} + def test_create_missing_address(self): + """Tests /project/?form=salesforce POST with no address. It should + 400.""" + client_data = {'sales_force_id': 'test', 'name': 'test'} + place_data = {'sales_force_id': 'test', 'name': 'test'} data = { 'sales_force_id': 'test', 'name': 'test', 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'address': address_data, - 'contacts': [], - 'contact_methods': [], - 'notes': []} + 'client': client_data, + 'place': place_data} response = self.post( self.url, data=data, query_string={'form': 'salesforce'}) self.assertEqual(response.status_code, 400) - def test_post_sf_no_address(self): - """Tests /project/?form=salesforce POST with no address. It should 400. - """ + def test_create_missing_project_state(self): + """Tests /project/?form=salesforce POST missing project state. It + should 400.""" + client_data = {'sales_force_id': 'test', 'name': 'test'} place_data = {'sales_force_id': 'test', 'name': 'test'} + address_data = { + 'street_address': '111 N Fake St', + 'city': 'Raleigh', + 'county': 'Wake', + 'state': 'NC', + 'country': 'United States of America', + 'postal_code': '27615'} data = { 'sales_force_id': 'test', 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), + 'client': client_data, 'place': place_data, - 'contacts': [], - 'contact_methods': [], - 'notes': []} + 'address': address_data} response = self.post( self.url, data=data, query_string={'form': 'salesforce'}) self.assertEqual(response.status_code, 400) - # There are additional ways that posting with a place and address could - # fail, but those are already tested in the /address/ and /place/ - # endpoints, so there's no need to test them here. - - def test_post_sf_new_contacts(self): - """Tests /project/?form=salesforce POST with a new contact.""" - place = self.env.place - contact_data = {'sales_force_id': 'test', 'name': 'test'} + def test_create_missing_project_name(self): + """Tests /project/?form=salesforce POST missing project state. It + should 400.""" + client_data = {'sales_force_id': 'test', 'name': 'test'} + place_data = {'sales_force_id': 'test', 'name': 'test'} + address_data = { + 'street_address': '111 N Fake St', + 'city': 'Raleigh', + 'county': 'Wake', + 'state': 'NC', + 'country': 'United States of America', + 'postal_code': '27615'} data = { 'sales_force_id': 'test', - 'name': 'test', 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [contact_data], - 'contact_methods': [], - 'notes': []} + 'client': client_data, + 'place': place_data, + 'address': address_data} + response = self.post( + self.url, + data=data, + query_string={'form': 'salesforce'}) + self.assertEqual(response.status_code, 400) + + def test_update_change_client(self): + """Tests /project/?form=salesforce POST with a change in orphaned + client. This happens when a client is no longer associated to an + opportunity and reassociated to another opportunity.""" + model = self.env.project + client = db.session.query(Client)\ + .filter(Client.id == model.client_id)\ + .first() + place = db.session.query(Place)\ + .filter(Place.id == model.place_id)\ + .first() + client_data = { + 'sales_force_id': client.sales_force_id, + 'name': client.name} + place_data = { + 'sales_force_id': place.sales_force_id, + 'name': place.name} + address_data = { + 'street_address': place.address.street_address, + 'city': place.address.city, + 'county': place.address.county, + 'state': place.address.state, + 'country': place.address.country, + 'postal_code': place.address.postal_code} + data = { + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'state': model.state, + 'client': client_data, + 'place': place_data, + 'address': address_data} + orphaned_client = self.env.otherclient + new_client_data = { + 'sales_force_id': orphaned_client.sales_force_id, + 'name': orphaned_client.name + } + new_client_data.update({'name': 'new'}) + + data.update({'client': new_client_data}) + response_data = self._test_post(data, {'form': 'salesforce'}) + # Check that the new client was added as expected. self.assertTrue( - db.session.query(Contact)\ - .join(ProjectContact, ProjectContact.contact_id == Contact.id)\ - .join(Project, ProjectContact.project_id == Project.id)\ + db.session.query(Client)\ + .join(Project, Project.client_id == Client.id) .filter(and_( Project.id == response_data['id'], - Contact.sales_force_id == contact_data['sales_force_id'], - Contact.name == contact_data['name']))\ + Client.name == new_client_data['name']))\ .first()) - def test_post_sf_change_contacts(self): - """Tests /project/?form=salesforce POST with a modified contact.""" - place = self.env.place - contact = self.env.contact - contact_data = contact.get_dictionary() - contact_data.pop('id') - contact_data.update({'name': 'foo'}) + def test_update_new_client(self): + """Tests /project/?form=salesforce POST update with a new client.""" + model = self.env.project + client = db.session.query(Client)\ + .filter(Client.id == model.client_id)\ + .first() + place = db.session.query(Place)\ + .filter(Place.id == model.place_id)\ + .first() + client_data = { + 'sales_force_id': client.sales_force_id, + 'name': client.name} + place_data = { + 'sales_force_id': place.sales_force_id, + 'name': place.name} + address_data = { + 'street_address': place.address.street_address, + 'city': place.address.city, + 'county': place.address.county, + 'state': place.address.state, + 'country': place.address.country, + 'postal_code': place.address.postal_code} data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [contact_data], - 'contact_methods': [], - 'notes': []} + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'state': model.state, + 'client': client_data, + 'place': place_data, + 'address': address_data} + # New Client + new_client_data = {'sales_force_id': 'test1', 'name': 'foo'} + data.update({'client': new_client_data}) response_data = self._test_post(data, {'form': 'salesforce'}) + + # Check that the new client was added as expected. self.assertTrue( - db.session.query(Contact)\ - .join(ProjectContact, ProjectContact.contact_id == Contact.id)\ - .join(Project, ProjectContact.project_id == Project.id)\ + db.session.query(Client)\ + .join(Project, Project.client_id == Client.id) .filter(and_( Project.id == response_data['id'], - Contact.id == contact.id, - Contact.sales_force_id == contact_data['sales_force_id'], - Contact.name == contact_data['name']))\ + Client.name == new_client_data['name']))\ .first()) - def test_post_sf_no_contacts(self): - """Tests /project/?form=salesforce POST with no contacts. It should + def test_update_no_client(self): + """Tests /project/?form=salesforce POST update with no client. This should 400. """ - place = self.env.place + model = self.env.project + place = db.session.query(Place)\ + .filter(Place.id == model.place_id)\ + .first() + place_data = { + 'sales_force_id': place.sales_force_id, + 'name': place.name} + address_data = { + 'street_address': place.address.street_address, + 'city': place.address.city, + 'county': place.address.county, + 'state': place.address.state, + 'country': place.address.country, + 'postal_code': place.address.postal_code} data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contact_methods': [], - 'notes': []} + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'state': model.state, + 'place': place_data, + 'address': address_data} response = self.post( self.url, data=data, query_string={'form': 'salesforce'}) self.assertEqual(response.status_code, 400) - self.assertFalse(db.session.query(Project).first()) - # There are additional ways that posting with a contact could fail, but - # those are already tested in the /contact/ and /contact/project/ - # endpoints, so there's no need to test them here. + # There are additional ways that posting with a client could fail, but + # those are already tested in the /client/ endpoint, so there's no need + # to test them here. - def test_post_sf_contact_methods(self): - """Tests /project/?form=salesforce POST with a contact method.""" - place = self.env.place - contact_data = { - 'sales_force_id': 'test', - 'name': 'test'} - contact_method_data = { - 'contact_sales_force_id': contact_data['sales_force_id'], - 'method': 'email', - 'value': 'test@blocpower.org'} + def test_update_new_address(self): + """Tests /project/?form=salesforce POST update with a modified place + and address.""" + model = self.env.project + client = db.session.query(Client)\ + .filter(Client.id == model.client_id)\ + .first() + place = db.session.query(Place)\ + .filter(Place.id == model.place_id)\ + .first() + client_data = { + 'sales_force_id': client.sales_force_id, + 'name': client.name} + place_data = { + 'sales_force_id': place.sales_force_id, + 'name': place.name} data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [contact_data], - 'contact_methods': [contact_method_data], - 'notes': []} + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'state': model.state, + 'client': client_data, + 'place': place_data} + + new_address_data = { + 'street_address': '111 N Fake St', + 'city': 'Raleigh', + 'county': 'Wake', + 'state': 'NC', + 'country': 'United States of America', + 'postal_code': '27615'} + + data.update({ + 'client': client_data, + 'place': place_data, + 'address': new_address_data + }) + response_data = self._test_post(data, {'form': 'salesforce'}) + + filters = [ + Project.id == response_data['id'], + Place.id == place.id, + Address.id == place.address.id] + filters += [ + getattr(Place, k) == place_data.get(k) + for k in place_data.keys()] + filters += [ + getattr(Address, k) == new_address_data.get(k) + for k in new_address_data.keys()] + self.assertTrue( - db.session.query(ContactMethod)\ - .join(Contact, ContactMethod.contact_id == Contact.id)\ - .join(ProjectContact, ProjectContact.contact_id == Contact.id)\ - .join(Project, ProjectContact.project_id == Project.id)\ - .filter(and_( - Project.id == response_data['id'], - ContactMethod.method == contact_method_data['method'], - ContactMethod.value == contact_method_data['value']))\ + db.session.query(Address)\ + .join(Place, Place.address_id == Address.id)\ + .join(Project, Project.place_id == Place.id)\ + .filter(and_(*filters))\ .first()) - def test_post_sf_contact_methods_bad_sf_id(self): - """Tests /project/?form=salesforce POST with a contact method with a - bad contact_sales_force_id. It should 400. + def test_update_new_place(self): + """Tests /project/?form=salesforce POST update with a new place and + address. """ - place = self.env.place - contact_method_data = { - 'contact_sales_force_id': 'test', - 'method': 'email', - 'value': 'test@blocpower.org'} + model = self.env.project + client = db.session.query(Client)\ + .filter(Client.id == model.client_id)\ + .first() + place = db.session.query(Place)\ + .filter(Place.id == model.place_id)\ + .first() + client_data = { + 'sales_force_id': client.sales_force_id, + 'name': client.name} data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [], - 'contact_methods': [contact_method_data], - 'notes': []} - response = self.post( - self.url, - data=data, - query_string={'form': 'salesforce'}) - self.assertEqual(response.status_code, 400) + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'state': model.state, + 'client': client_data} - def test_post_sf_contact_methods_unassociated_sf_id(self): - """Tests /project/?form=salesforce POST with a contact method with a - contact_sales_force_id that is not associated with the project. It - should 400. + new_place_data = {'sales_force_id': 'test1', 'name': 'test1'} + new_address_data = { + 'street_address': '111 N Fake St', + 'city': 'Raleigh', + 'county': 'Wake', + 'state': 'NC', + 'country': 'United States of America', + 'postal_code': '27615'} + data.update({ + 'place' : new_place_data, + 'address': new_address_data}) + + response_data = self._test_post(data, {'form': 'salesforce'}) + filters = [Project.id == response_data['id']] + filters += [ + getattr(Place, k) == new_place_data.get(k) + for k in new_place_data.keys()] + filters += [ + getattr(Address, k) == new_address_data.get(k) + for k in new_address_data.keys()] + self.assertTrue( + db.session.query(Address)\ + .join(Place, Place.address_id == Address.id)\ + .join(Project, Project.place_id == Place.id)\ + .filter(and_(*filters))\ + .first()) + + def test_update_no_place(self): + """Tests /project/?form=salesforce POST update with no place. It should 400. """ - place = self.env.place - contact = self.env.contact - contact_method_data = { - 'contact_sales_force_id': contact.sales_force_id, - 'method': 'email', - 'value': 'test@blocpower.org'} + model = self.env.project + client = db.session.query(Client)\ + .filter(Client.id == model.client_id)\ + .first() + place = db.session.query(Place)\ + .filter(Place.id == model.place_id)\ + .first() + client_data = { + 'sales_force_id': client.sales_force_id, + 'name': client.name} + address_data = { + 'street_address': place.address.street_address, + 'city': place.address.city, + 'county': place.address.county, + 'state': place.address.state, + 'country': place.address.country, + 'postal_code': place.address.postal_code} data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [], - 'contact_methods': [contact_method_data], - 'notes': []} + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'state': model.state, + 'client': client_data, + 'address': address_data} + new_address_data = { + 'street_address': '15 MetroTech Center', + 'city': 'Brooklyn', + 'county': 'Kings', + 'state': 'NY', + 'country': 'United States of America', + 'postal_code': '11210'} + data.update({'address': new_address_data}) response = self.post( self.url, data=data, query_string={'form': 'salesforce'}) self.assertEqual(response.status_code, 400) - def test_post_sf_no_contact_methods(self): - """Tests /project/?form=salesforce POST with no contact methods. It - should 400. + def test_update_no_address(self): + """Tests /project/?form=salesforce POST with no address. It should 400. """ - place = self.env.place + model = self.env.project + client = db.session.query(Client)\ + .filter(Client.id == model.client_id)\ + .first() + place = db.session.query(Place)\ + .filter(Place.id == model.place_id)\ + .first() + client_data = { + 'sales_force_id': client.sales_force_id, + 'name': client.name} + place_data = { + 'sales_force_id': place.sales_force_id, + 'name': place.name} + address_data = { + 'street_address': place.address.street_address, + 'city': place.address.city, + 'county': place.address.county, + 'state': place.address.state, + 'country': place.address.country, + 'postal_code': place.address.postal_code} data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [], - 'notes': []} + 'sales_force_id': model.sales_force_id, + 'name': model.name, + 'state': model.state, + 'client': client_data, + 'place': place_data} + data.update({'name': 'new_name'}) response = self.post( self.url, data=data, query_string={'form': 'salesforce'}) self.assertEqual(response.status_code, 400) - # There are additional ways that posting with a contact method could fail, - # but those are already tested in the /contact/method/ endpoint, so - # there's no need to test them here. - - def test_post_sf_notes(self): - """Tests /project/?form=salesforce POST with a note.""" - place = self.env.place - note_data = { - 'sales_force_id': 'test', - 'posted': arrow.get().isoformat(), - 'title': 'Test', - 'content': 'This is a test note.', - 'poster_name': 'test'} - data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [], - 'contact_methods': [], - 'notes': [note_data]} - response_data = self._test_post(data, {'form': 'salesforce'}) - filters = [Project.id == response_data['id']] - filters += [getattr(Note, k) == v for k, v in note_data.items()] - self.assertTrue( - db.session.query(Note)\ - .join(Project, Project.id == Note.project_id)\ - .filter(and_(*filters))\ - .first()) - - def test_post_sf_no_notes(self): - """Tests /project/?form=salesforce POST with a missing notes field. It - should 400. - """ - place = self.env.place - data = { - 'sales_force_id': 'test', - 'name': 'test', - 'state': 'pending', - 'client': self.env.client.get_dictionary(), - 'place': place.get_dictionary(), - 'address': place.address.get_dictionary(), - 'contacts': [], - 'contact_methods': []} - response = self.post( - self.url, data=data, query_string={'form': 'salesforce'}) - self.assertEqual(response.status_code, 400) - - # There are additional ways that posting with a note could fail, but those - # are already tested in the /note/ endpoint, so there's no need to test - # them here. + # There are additional ways that posting with a place and address could + # fail, but those are already tested in the /address/ and /place/ + # endpoints, so there's no need to test them here. diff --git a/app/views/base.py b/app/views/base.py index 1a175c57e9aaae6e9b01a3da64eeecfb763279b4..f37f3d12d92a2b050aba5724e327ca1b8a295f8d 100644 --- a/app/views/base.py +++ b/app/views/base.py @@ -4,7 +4,7 @@ from flask import jsonify, request, current_app from flask.ext.classy import FlaskView from app.lib.database import db from app.lib.red import redis -from app.permissions.auth import standard_login_need +from app.permissions.auth import standard_login_need, salesforce_need class View(FlaskView): @@ -69,3 +69,29 @@ class RestView(UnprotectedRestView): """A view wrapper for RESTful controllers that _does_ offer API protection. """ decorators = (standard_login_need,) + +class SalesforceObjectView(UnprotectedRestView): + """A view wrapper for Salesforce object that offers API protection""" + def get_controller(self): + """Return a controller instance to use for database interactions.""" + pass + + @standard_login_need + def index(self): + return super(SalesforceObjectView, self).index() + + @standard_login_need + def get(self, id_): + return super(SalesforceObjectView, self).get(id_) + + @salesforce_need + def post(self): + return super(SalesforceObjectView, self).post() + + @standard_login_need + def put(self, id_): + return super(SalesforceObjectView, self).put(id_) + + @standard_login_need + def delete(self, id_): + return super(SalesforceObjectView, self).delete(id_) diff --git a/app/views/client.py b/app/views/client.py index 5536dc8fc1af49703ecd8d6d81a6c0374455197e..8646f4e6e0dc3561099a98bfb183a01bea1b0ab7 100644 --- a/app/views/client.py +++ b/app/views/client.py @@ -1,16 +1,20 @@ """Views for managing clients.""" from werkzeug.exceptions import MethodNotAllowed from flask import request -from app.views.base import RestView + +from app.views.base import SalesforceObjectView from app.controllers.client import ClientController +from app.permissions.auth import auth_need, standard_login_need + -class ClientView(RestView): +class ClientView(SalesforceObjectView): """The client view.""" def get_controller(self): """Return an instance of the client controller.""" return ClientController() + @standard_login_need def delete(self, id_): """Not implemented.""" raise MethodNotAllowed( diff --git a/app/views/contact.py b/app/views/contact.py index 88be9abcd5982aa93883425b0640ad9b28b828f7..d90c8dab6c7756990f940f5fb0cec54c928fc666 100644 --- a/app/views/contact.py +++ b/app/views/contact.py @@ -1,19 +1,21 @@ """Views for managing contacts.""" from werkzeug.exceptions import MethodNotAllowed from flask import request -from app.views.base import RestView + +from app.views.base import RestView, SalesforceObjectView from app.controllers.contact import ( ContactController, ContactMethodController, ProjectContactController) -from app.permissions.base import FormNeed +from app.permissions.base import FormNeed +from app.permissions.application import app_need, RoleNeed +from app.permissions.auth import auth_need, standard_login_need, salesforce_need -class ContactView(RestView): +class ContactView(SalesforceObjectView): """The contact view.""" def get_controller(self): """Return an instance of the contact controller.""" return ContactController() - class ContactMethodView(RestView): """The contact method view.""" route_base = '/contact/method/' @@ -23,7 +25,7 @@ class ContactMethodView(RestView): return ContactMethodController() -class ProjectContactView(RestView): +class ProjectContactView(SalesforceObjectView): """A view for the project-contact m2m relationship.""" route_base = '/contact/project/' @@ -31,6 +33,7 @@ class ProjectContactView(RestView): """Return an instance of the project contact controller.""" return ProjectContactController() + @standard_login_need def put(self, id_): """Not implemented.""" raise MethodNotAllowed( diff --git a/app/views/note.py b/app/views/note.py index 4a76d7b76273ed6a843fed456fdbaa18c5ae61a9..631582fb266bad57c24b1fdbe00318a8884a98a4 100644 --- a/app/views/note.py +++ b/app/views/note.py @@ -1,9 +1,15 @@ """Views for managing notes.""" -from app.views.base import RestView +from werkzeug.exceptions import MethodNotAllowed +from flask import request + +from app.views.base import SalesforceObjectView from app.controllers.note import NoteController +from app.permissions.application import app_need, RoleNeed +from app.permissions.auth import auth_need, standard_login_need, salesforce_need + -class NoteView(RestView): +class NoteView(SalesforceObjectView): """The note view.""" def get_controller(self): """Return an instance of the note controller.""" diff --git a/app/views/place.py b/app/views/place.py index c03bcd5a9660f070a7cfd2e9f4cb23d92e7baed7..60673c489b799120eed218a7d76c849e618e7261 100644 --- a/app/views/place.py +++ b/app/views/place.py @@ -1,21 +1,23 @@ -from flask import request +"""Views for managing place and address.""" from werkzeug.exceptions import MethodNotAllowed +from flask import request from app.controllers.place import AddressController, PlaceController -from app.views.base import RestView +from app.views.base import RestView, SalesforceObjectView + +from app.permissions.application import app_need, RoleNeed +from app.permissions.auth import auth_need, standard_login_need -class PlaceView(RestView): +class PlaceView(SalesforceObjectView): """The Place view.""" def get_controller(self): """Return an instance of the Place controller.""" return PlaceController() + @standard_login_need def delete(self, id_): - """Not implemented. - Args: - id_ - The object ID - """ + """Not implemented""" raise MethodNotAllowed( 'Places cannot be deleted. Cancel the project instead.') diff --git a/app/views/project.py b/app/views/project.py index 3f090c3f06707bfee1ca1f78a03b1e3f0a14fb01..5116fcf94b451763f25af46784298c7ad8c99211 100644 --- a/app/views/project.py +++ b/app/views/project.py @@ -2,47 +2,19 @@ from werkzeug.exceptions import MethodNotAllowed from flask import request -from app.views.base import UnprotectedRestView +from app.views.base import SalesforceObjectView from app.controllers.project import ProjectController from app.permissions.application import app_need, RoleNeed from app.permissions.auth import auth_need, standard_login_need -class ProjectView(UnprotectedRestView): +class ProjectView(SalesforceObjectView): """The project view.""" def get_controller(self): """Return an instance of the project controller.""" return ProjectController() - @standard_login_need - def index(self): - return super(ProjectView, self).index() - - @standard_login_need - def get(self, id_): - return super(ProjectView, self).get(id_) - - def post(self): - """Check for an application and either a user or an application with - the project_post_salesforce role (and the salesforce form) before - posting a project. - """ - form = request.args.get('form') - - need = app_need - if form == 'salesforce': - need &= RoleNeed('project_post_salesforce') - else: - need &= auth_need - - with need: - return super(ProjectView, self).post() - - @standard_login_need - def put(self, id_): - return super(ProjectView, self).put(id_) - def delete(self, id_): """Not implemented""" raise MethodNotAllowed( diff --git a/requirements.txt b/requirements.txt index eb48e1f4cc32151d93f87bca3dbb0c1b65478b89..dfb98eccb50d3c857782738574ae48a6f75ef34f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ Flask==0.10.1 Flask-Classy==0.6.10 Flask-SQLAlchemy==2.1 Flask-Testing==0.4.2 -GeoAlchemy2==0.2.6 +GeoAlchemy2==0.3.0 httplib2==0.9.2 itsdangerous==0.24 Jinja2==2.8 @@ -36,7 +36,7 @@ redis==2.10.5 requests==2.6.2 rsa==3.3 six==1.10.0 -SQLAlchemy==1.0.12 +SQLAlchemy==1.0.13 texttable==0.8.4 wcwidth==0.1.6 websocket-client==0.35.0