diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000000000000000000000000000000000000..71e945081f9e359ddfd8b3fc8ceceee01a08f112 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,407 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins=pylint_flask + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. This option is deprecated +# and it will be removed in Pylint 2.0. +optimize-ast=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=import-star-module-level,coerce-builtin,unicode-builtin,no-absolute-import,input-builtin,reduce-builtin,cmp-builtin,basestring-builtin,cmp-method,setslice-method,unichr-builtin,backtick,old-octal-literal,old-ne-operator,oct-method,map-builtin-not-iterating,raw_input-builtin,apply-builtin,range-builtin-not-iterating,useless-suppression,dict-view-method,filter-builtin-not-iterating,unpacking-in-except,getslice-method,intern-builtin,raising-string,standarderror-builtin,buffer-builtin,coerce-method,old-raise-syntax,file-builtin,dict-iter-method,metaclass-assignment,long-suffix,next-method-called,suppressed-message,long-builtin,delslice-method,execfile-builtin,old-division,nonzero-method,using-cmp-argument,xrange-builtin,hex-method,indexing-exception,print-statement,round-builtin,reload-builtin,parameter-unpacking,zip-builtin-not-iterating + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=optparse + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/Dockerfile b/Dockerfile index a80685cb102e9d5d8a318af37c2ba59f5c601035..5611b7967ebb31336371530ba67d136bd60318cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,10 @@ ENV UWSGIREQ=/var/log/uwsgi.req.log # Custom environment variables. These change from project to project. ARG DOMAIN ENV DOMAIN=${DOMAIN} +ARG NUM_PROCESSES +ENV NUM_PROCESSES=${NUM_PROCESSES} +ARG NUM_THREADS +ENV NUM_THREADS=${NUM_THREADS} # Add the nginx reposoitory. We need 1.8 in order to support adding CORS headers to error responses. RUN apt-get update @@ -54,6 +58,8 @@ RUN \ echo "Copying environment variables from $1 to $2."; \ sed -e "s/\$DOMAIN/$DOMAIN/g" \ -e "s@\$CODEROOT@$CODEROOT@g" \ + -e "s@\$NUM_PROCESSES@$NUM_PROCESSES@g" \ + -e "s@\$NUM_THREADS@$NUM_THREADS@g" \ -e "s@\$NGINXERR@$NGINXERR@g" \ -e "s@\$NGINXREQ@$NGINXREQ@g" \ -e "s@\$UWSGIERR@$UWSGIERR@g" \ diff --git a/app/__init__.py b/app/__init__.py index cbd49e1aa4393d1386a5194c114e38bee5aef288..990886ae845f4fe14207268e9767f6f53d9873fe 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -24,7 +24,4 @@ def create_app(config): from .lib import session session.register(app) - with app.app_context(): - database.db.create_all() - return app diff --git a/app/config/development.default.py b/app/config/development.default.py index 5a20208c82fe48608f7fba419e724a09b1f5416e..8d06722db7893b986851725d81d8eb08e21b89f1 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -1,20 +1,23 @@ +import os + + SQLALCHEMY_TRACK_MODIFICATIONS = False -SQLALCHEMY_DATABASE_URI = 'postgres:///projectservice' +SQLALCHEMY_DATABASE_URI = os.environ['DBURI'] -REDIS_URI = 'redis://127.0.0.1:6379/' +REDIS_URI = os.environ['REDISURI'] DEBUG = True SERVICE_CONFIG = { - 'app_key': '$APP_KEY', - 'app_secret': '$APP_SECRET', + 'app_key': os.environ['APP_KEY'], + 'app_secret': os.environ['APP_SECRET'], 'headers': { 'app_key': 'x-blocpower-app-key', 'app_token': 'x-blocpower-app-token', 'app_secret': 'x-blocpower-app-secret'}, 'urls': { - 'app': 'http://127.0.0.1:5400', - 'user': 'http://127.0.0.1:5401'} + 'app': 'http://dev.appservice.blocpower.io/', + 'user': 'http://dev.userservice.blocpower.io/'} } # AppService diff --git a/app/config/local.default.py b/app/config/local.default.py new file mode 100644 index 0000000000000000000000000000000000000000..9c69a5fc0a9528f202fb2baef6b7160021522b80 --- /dev/null +++ b/app/config/local.default.py @@ -0,0 +1,29 @@ +SQLALCHEMY_TRACK_MODIFICATIONS = False +SQLALCHEMY_DATABASE_URI = 'postgres:///customer' + +REDIS_URI = 'redis://127.0.0.1:6379/' + +DEBUG = True + +SERVICE_CONFIG = { + 'app_key': '$APP_KEY', + 'app_secret': '$APP_SECRET', + 'headers': { + 'app_key': 'x-blocpower-app-key', + 'app_token': 'x-blocpower-app-token', + 'app_secret': 'x-blocpower-app-secret'}, + 'urls': { + 'app': 'http://127.0.0.1:5400', + 'user': 'http://127.0.0.1:5401'} +} + +# AppService +APP_CACHE_EXPIRY = 60 * 60 # One hour. + +# UserService +GOOGLE_AUTH_HEADER = 'x-blocpower-google-token' +USER_CACHE_EXPIRY = 60 * 60 # One Hour. + +# Blocpower auth information. +HEADER_AUTH_KEY = 'x-blocpower-auth-key' +HEADER_AUTH_TOKEN = 'x-blocpower-auth-token' diff --git a/app/config/staging.default.py b/app/config/staging.default.py index a4b9ead2775012af640da65b3d07e1e190fc0f64..9a06abb8252974d743d088a52ee30e93fb6dea1b 100644 --- a/app/config/staging.default.py +++ b/app/config/staging.default.py @@ -6,7 +6,7 @@ SQLALCHEMY_DATABASE_URI = os.environ['DBURI'] REDIS_URI = os.environ['REDISURI'] -DEBUG = False +DEBUG = True SERVICE_CONFIG = { 'app_key': os.environ['APP_KEY'], diff --git a/app/config/uwsgi.ini b/app/config/uwsgi.ini index 08d76933b88d4ca851eac0750135ca18f54a7d93..c996d6b66659ce79c29f7880619ca4a7ef23aff0 100644 --- a/app/config/uwsgi.ini +++ b/app/config/uwsgi.ini @@ -1,5 +1,5 @@ [uwsgi] -# This is used if no configuration is speicified. +# This is used if no configuration is specified. ini = :base socket = $CODEROOT/uwsgi.sock @@ -7,9 +7,10 @@ socket = $CODEROOT/uwsgi.sock chmod-socket = 666 master = true -# This is a best guess. At some point in time, we should run some experiments -# and get something more concrete. -processes = 1 +# Number of processes and workers should be determined at build time +# Should be based on the type of instance that it is running on +processes = $NUM_PROCESSES +threads = $NUM_THREADS # Set loggers. req-logger = file:$UWSGIREQ diff --git a/app/controllers/base.py b/app/controllers/base.py index 9168871c498ca68863934b39fbbae8a83527b580..a99b010074ff3e675381c33618d1cf080491cd45 100644 --- a/app/controllers/base.py +++ b/app/controllers/base.py @@ -177,5 +177,5 @@ class SalesforceObjectController(RestController): 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/project.py b/app/controllers/project.py index 679b02cb5680060af18404fb41ffdb8573c019b7..ed407fdca5bd8313aa1c34cd0faf81f4926d0f49 100644 --- a/app/controllers/project.py +++ b/app/controllers/project.py @@ -26,8 +26,9 @@ class ProjectController(RestController): filters = { 'q': lambda d: and_(*[ Project.name.ilike('%{}%'.format(term)) - for term in d['q'].split(' ') - ])} + for term in d['q'].split(' ')]), + 'building_id': lambda d: Project.building_id == d['building_id'], + } def get_form(self, filter_data): """Return the project form.""" @@ -100,7 +101,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: @@ -108,7 +109,7 @@ class ProjectController(RestController): place = PlaceController().put(place.id, place_data, {}) else: place = PlaceController().post(place_data, {}) - + data.update({'place_id': place.id}) # Create/modify the project diff --git a/app/forms/project.py b/app/forms/project.py index d69d9bf8789167e485632915787b59c2a6f94131..a46bb06608c798200970d4d5d6883cb375000bf3 100644 --- a/app/forms/project.py +++ b/app/forms/project.py @@ -21,3 +21,4 @@ class ProjectForm(Form): wtf.validators.Required(), wtf.validators.AnyOf(Project.states) ]) + building_id = wtf.IntegerField() diff --git a/app/lib/database.py b/app/lib/database.py index 3de6fb01f9d757822f6327bc7214a086ead56bc8..9bb84f0050838b745d8a1933afdec0c938caebd2 100644 --- a/app/lib/database.py +++ b/app/lib/database.py @@ -21,7 +21,7 @@ def register(app): def commit(): """A wrapper for db.session.commit(). - + This rolls back the database session after a failed commit so that the app can continue to request future commits on the same thread. """ @@ -30,3 +30,68 @@ def commit(): except Exception as e: db.session.rollback() raise e + +class ProcTable: + """ProcTable represents a simplified class of SQLAlchemy db.model""" + + def __init__(self, name, *columns): + self.name = name + self.columns = [column for column in columns] + + def get_columns(self): + """List all columns""" + return [column.key for column in self.columns] + +class ProcColumn: + """ProcColumn represents a column similar to SQLAlchemy column""" + + def __init__(self, key): + self.key = key + +def proc(model, method, limit=None, offset=None, **kwargs): + """ + Run stored procedure + + Args: + model (class): The class of the db table + method (str): Method name for stored procedure + kwargs: Arguments for stored proc + Returns: + list: Results of the query + """ + + params = "" + cols = ','.join(str(i) for i in model.__table__.get_columns()) + # By seperating the args like this sanatation will happen automatically + input_args = {} + for key, value in kwargs.items(): + if value is not None: + params += "in_{} := :{}, ".format(key, key) + input_args[key] = str(value) + else: + params += "in_{} := null, ".format(key) + params = params[:-2] # remove last comma and space + + query = "select {} from {}.{}({})".format( + cols, + model.__table_args__['schema'], + method, + params + ) + if limit: + query += ' limit {}'.format(limit) + if offset: + query += ' offset {}'.format(offset) + + try: + results = db.session.execute(query, input_args) + db.session.commit() + + data = [] + for row in results: + data.append(model(**dict(zip(row.keys(), row)))) + + except Exception as err: + raise err + + return data diff --git a/app/models/base.py b/app/models/base.py index 8381060c7d044d4d4d8e2b77f321e16b2492f44d..529e2b8a7a5d40f2100e88c7536baf923a92792a 100644 --- a/app/models/base.py +++ b/app/models/base.py @@ -13,21 +13,8 @@ from ..lib import geography from . import columns -class Model(object): +class BaseModel: """A base mixin for all models.""" - @declared_attr - def __tablename__(cls): - """Automatically set the database table name to the class name - lower-cased. - """ - return cls.__name__.lower() - - def __str__(self): - """Provide a sane default for model string representation.""" - return '<{} (id={})>'.format(self.__class__.__name__, self.id) - - id = db.Column(db.Integer, primary_key=True) - def get_dictionary(self): """Return a dictionary representation of the model. @@ -54,6 +41,23 @@ class Model(object): return d +class Model(BaseModel): + """A base mixin for SQLAlchemy model.""" + @declared_attr + def __tablename__(cls): + """Automatically set the database table name to the class name + lower-cased. + """ + return cls.__name__.lower() + + __table_args__ = {"schema": "public"} + + def __str__(self): + """Provide a sane default for model string representation.""" + return '<{} (id={})>'.format(self.__class__.__name__, self.id) + + id = db.Column(db.Integer, primary_key=True) + class Tracked(object): """A mixin to include tracking datetime fields.""" created = db.Column(columns.Arrow, default=func.now()) @@ -91,3 +95,22 @@ class External(object): """An external-facing model. These have a UUID key.""" key = db.Column( columns.GUID(), index=True, nullable=False, default=uuid.uuid4) + + +class ProjectSchema(BaseModel): + """A mixin that changes the schema to project_service""" + + @declared_attr + def __tablename__(cls): + """Automatically set the database table name to the class name + lower-cased. + """ + return cls.__name__.lower() + + __table_args__ = {"schema": "project_service"} + + def __str__(self): + """Provide a sane default for model string representation.""" + return '<{} (id={})>'.format(self.__class__.__name__, self.id) + + id = db.Column(db.Integer, primary_key=True) diff --git a/app/models/client.py b/app/models/client.py index afa2543572c453303dd67511d7ee4016a4cb40b3..3a4ada2922a3b40bb758650575f26b280e48b51e 100644 --- a/app/models/client.py +++ b/app/models/client.py @@ -1,8 +1,8 @@ """Models for tracking client information.""" from app.lib.database import db -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Model, Tracked, SalesForce, ProjectSchema -class Client(Model, Tracked, SalesForce, db.Model): +class Client(ProjectSchema, Tracked, SalesForce, db.Model): """A client.""" name = db.Column(db.Unicode(255), nullable=False) diff --git a/app/models/contact.py b/app/models/contact.py index 0fefe9d3306d758dd8d13409363cffd4f8e95244..a16d788db4d868f93b3fc7efb78bf2e4814eeb06 100644 --- a/app/models/contact.py +++ b/app/models/contact.py @@ -1,14 +1,14 @@ """Models for dealing with contacts.""" from app.lib.database import db -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Model, Tracked, SalesForce, ProjectSchema -class Contact(Model, Tracked, SalesForce, db.Model): +class Contact(ProjectSchema, Tracked, SalesForce, db.Model): """A contact (i.e an entity you can reach out to).""" name = db.Column(db.Unicode(255), nullable=False) -class ContactMethod(Model, Tracked, db.Model): +class ContactMethod(ProjectSchema, Tracked, db.Model): """A contact method.""" # TODO SQLAlchemy < 1.1 does not support the use of a python enum in Enum # fields, so for now we have to use a list of strings. When 1.1 leaves @@ -24,12 +24,12 @@ class ContactMethod(Model, Tracked, db.Model): contact_id = db.Column( db.Integer, db.ForeignKey('contact.id'), nullable=False) method = db.Column( - db.Enum(*methods, name='contact_methods'), + db.Enum(*methods, name='contact_methods', inherit_schema=True), nullable=False) value = db.Column(db.Unicode(255), nullable=False) -class ProjectContact(Model, Tracked, db.Model): +class ProjectContact(ProjectSchema, Tracked, db.Model): """A m2m relationship between project and contact.""" project_id = db.Column( db.Integer, db.ForeignKey('project.id'), nullable=False) diff --git a/app/models/document.py b/app/models/document.py index f9fbb2d00d04b857d7bfef6d79f1ec7aed15821d..b604dc424238fd80da262875afa3686dd5de17ab 100644 --- a/app/models/document.py +++ b/app/models/document.py @@ -1,15 +1,15 @@ """Models for handling associated documents.""" from app.lib.database import db -from app.models.base import Model, Tracked +from app.models.base import Model, Tracked, ProjectSchema from app.models.columns import GUID -class DocumentSlot(Model, Tracked, db.Model): +class DocumentSlot(ProjectSchema, 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) + db.Integer, db.ForeignKey('project_service.project.id'), nullable=False) document_key = db.Column(GUID(), nullable=False) # How the document fits into the project. diff --git a/app/models/note.py b/app/models/note.py index 470c1dc24567b0b23f8639e9a56859e8d03ed67e..4385a8e4d6a5b16c5d04020ac528a19edf5c21fc 100644 --- a/app/models/note.py +++ b/app/models/note.py @@ -1,10 +1,10 @@ """Models for making small notes on the project.""" from app.lib.database import db -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Model, Tracked, SalesForce, ProjectSchema from app.models import columns -class Note(Model, Tracked, SalesForce, db.Model): +class Note(ProjectSchema, Tracked, SalesForce, db.Model): """A note.""" project_id = db.Column( db.Integer, diff --git a/app/models/place.py b/app/models/place.py index e06c9532fc8265c4f5a8c661ce87e3a855cd7fa7..625268a16f3fb065cd44d0b380e3ce5f9ff568e7 100644 --- a/app/models/place.py +++ b/app/models/place.py @@ -2,27 +2,27 @@ from sqlalchemy.orm import relationship from app.lib.database import db from app.lib import geography -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Tracked, SalesForce, ProjectSchema -class Place(Model, Tracked, SalesForce, db.Model): - """The Place class""" - name = db.Column(db.Unicode(255), nullable=False) - address_id = db.Column( - db.Integer, db.ForeignKey('address.id'), nullable=False) - - address = relationship('Address', uselist=False) - - -class Address(Model, Tracked, db.Model): +class Address(ProjectSchema, Tracked, db.Model): """The Address class""" 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'), + db.Enum(*geography.states, name='geo_states', inherit_schema=True), nullable=False) country = db.Column( - db.Enum(*geography.countries, name='geo_countries'), + db.Enum(*geography.countries, name='geo_countries', inherit_schema=True), nullable=False) postal_code = db.Column(db.Unicode(10)) + + +class Place(ProjectSchema, Tracked, SalesForce, db.Model): + """The Place class""" + name = db.Column(db.Unicode(255), nullable=False) + address_id = db.Column( + db.INTEGER, db.ForeignKey('project_service.address.id'), nullable=False) + + address = relationship('Address', uselist=False) diff --git a/app/models/project.py b/app/models/project.py index e1203c747e104fe8d1ab1528faa6f76f8ea03ba2..7b71df11ae530b8dbf395831ad2db79a04117837 100644 --- a/app/models/project.py +++ b/app/models/project.py @@ -1,17 +1,18 @@ """Models directly-relating to the top-level project.""" import re from app.lib.database import db -from app.models.base import Model, Tracked, SalesForce +from app.models.base import Model, Tracked, SalesForce, ProjectSchema -class Project(Model, Tracked, SalesForce, db.Model): +class Project(ProjectSchema, Tracked, SalesForce, db.Model): """A project.""" client_id = db.Column( - db.Integer, db.ForeignKey('client.id'), nullable=False) + db.Integer, db.ForeignKey('project_service.client.id'), nullable=False) place_id = db.Column( - db.Integer, db.ForeignKey('place.id'), nullable=False) + db.Integer, db.ForeignKey('project_service.place.id'), nullable=False) name = db.Column(db.Unicode(255), nullable=False) + building_id = db.Column(db.Integer, nullable=False) @property def computed_slug(self): @@ -60,7 +61,7 @@ class Project(Model, Tracked, SalesForce, db.Model): 'constructed', 'verified', 'paid' ] state = db.Column( - db.Enum(*states, name='project_states'), + db.Enum(*states, name='project_states', inherit_schema=True), default='pending', nullable=False) @@ -71,10 +72,11 @@ class Project(Model, Tracked, SalesForce, db.Model): return d -class ProjectStateChange(Model, Tracked, db.Model): +class ProjectStateChange(ProjectSchema, Tracked, db.Model): """A state change of a project.""" - project_id = db.Column(db.Integer, db.ForeignKey('project.id'), + project_id = db.Column( + db.Integer, db.ForeignKey('project_service.project.id'), nullable=False) state = db.Column( - db.Enum(*Project.states, name='project_states'), + db.Enum(*Project.states, name='project_states', inherit_schema=True), nullable=False) diff --git a/app/views/base.py b/app/views/base.py index f37f3d12d92a2b050aba5724e327ca1b8a295f8d..b885c9c33cf93375d5c310d9804ec7c6d009afed 100644 --- a/app/views/base.py +++ b/app/views/base.py @@ -70,6 +70,7 @@ class RestView(UnprotectedRestView): """ decorators = (standard_login_need,) + class SalesforceObjectView(UnprotectedRestView): """A view wrapper for Salesforce object that offers API protection""" def get_controller(self): diff --git a/docs/API/api.md b/docs/API/api.md new file mode 100644 index 0000000000000000000000000000000000000000..1b62768d25d1c33ecad4287c7057524f052ad8d7 --- /dev/null +++ b/docs/API/api.md @@ -0,0 +1,56 @@ +**Title** +---- + <_Additional information about your API call. Try to use verbs that match both request type (fetching vs modifying) and plurality (one vs multiple)._> + +* **URL** + + <_The URL Structure (path only, no root url)_> + +* **Method:** + + <_The request type_> + + `GET` | `POST` | `DELETE` | `PUT` + +* **URL Params** + + <_If URL params exist, specify them in accordance with name mentioned in URL section. Separate into optional and required. Document data constraints._> + + **Required:** + + `id=[integer]` + + **Optional:** + + `photo_id=[alphanumeric]` + +* **Data Params** + + <_If making a post request, what should the body payload look like? URL Params rules apply here too._> + +* **Success Response:** + + <_What should the status code be on success and is there any returned data? This is useful when people need to to know what their callbacks should expect!_> + + * **Code:** 200
+ **Content:** `{ id : 12 }` + +* **Error Response:** + + <_Most endpoints will have many ways they can fail. From unauthorized access, to wrongful parameters etc. All of those should be liste d here. It might seem repetitive, but it helps prevent assumptions from being made where they should be._> + + * **Code:** 401 UNAUTHORIZED
+ **Content:** `{ error : "Log in" }` + + OR + + * **Code:** 422 UNPROCESSABLE ENTRY
+ **Content:** `{ error : "Email Invalid" }` + +* **Sample Call:** + + <_Just a sample call to your endpoint in a runnable format ($.ajax call or a curl request) - this makes life easier and more predictable._> + +* **Notes:** + + <_This is where all uncertainties, commentary, discussion etc. can go. I recommend timestamping and identifying oneself when leaving comments here._> diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000000000000000000000000000000000000..5660f022283738e06640ee95df95506320277c59 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +-r requirements.txt +Flask-Testing==0.4.2 +nose==1.3.7 +pytest==2.9.1 +pylint>=1.6.5 +pylint-flask>=0.5 diff --git a/requirements.txt b/requirements.txt index dfb98eccb50d3c857782738574ae48a6f75ef34f..39b9286885c59c1ef9aa02524e2e2c8fb467358f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,6 @@ docutils==0.12 Flask==0.10.1 Flask-Classy==0.6.10 Flask-SQLAlchemy==2.1 -Flask-Testing==0.4.2 GeoAlchemy2==0.3.0 httplib2==0.9.2 itsdangerous==0.24 @@ -19,7 +18,6 @@ Jinja2==2.8 jmespath==0.9.0 MarkupSafe==0.23 needs==1.0.9 -nose==1.3.7 oauth2client==2.0.1 parse==1.6.6 pathspec==0.3.3 @@ -29,7 +27,6 @@ py==1.4.31 pyasn1==0.1.9 pyasn1-modules==0.0.8 pycparser==2.14 -pytest==2.9.1 python-dateutil==2.4.2 PyYAML==3.11 redis==2.10.5 diff --git a/run.py b/run.py index 217555d390954c046072eaefd5063e810dfa4f1b..9e5615095079b77d6db79e0b2c754298fa737e47 100644 --- a/run.py +++ b/run.py @@ -2,6 +2,6 @@ from app import create_app from app.lib.database import db -app = create_app('config/development.py') +app = create_app('config/local.py') if __name__ == '__main__': app.run(host='0.0.0.0', port=5302) diff --git a/wsgi.py b/wsgi.py index d147c76628e5663775b65db95241715368391318..294e9eb1abfaa5e3fbedf6ca944c8b041490d65a 100644 --- a/wsgi.py +++ b/wsgi.py @@ -9,6 +9,3 @@ env = os.environ['ENVIRONMENT'] # Correctly raise a file not found if the specified environment does not exist. app = create_app('config/{}.py'.format(env)) -with app.app_context(): - # Build the database models. - db.create_all()