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()